Skip to main content

mcpr_core/protocol/schema_manager/
scanner.rs

1//! `SchemaScanner` — active discovery of an upstream MCP server's schema.
2//!
3//! This module defines the trait and its supporting types. No concrete
4//! implementation is provided here; the HTTP-backed scanner lands in a
5//! later step once the `SchemaManager` has consumers.
6
7use std::future::Future;
8
9use serde_json::Value;
10
11/// How the scanner acquires an MCP session for discovery calls.
12#[derive(Debug, Clone)]
13pub enum ScanMode {
14    /// Scanner opens its own MCP session to the upstream.
15    ///
16    /// Used when the upstream accepts anonymous sessions or the proxy
17    /// holds credentials (e.g. a static bearer token in `mcpr.toml`).
18    Standalone,
19    /// Scanner injects discovery requests into an existing client session.
20    ///
21    /// Used when the upstream requires auth that only clients hold.
22    /// The scanner uses reserved JSON-RPC ids (e.g. `__mcpr_scan_<n>`)
23    /// to multiplex without colliding with the client's own requests.
24    Attached { session_id: String },
25}
26
27/// One method's merged `result` payload from a scan.
28///
29/// The scanner is responsible for merging paginated responses before
30/// returning. Consumers pass each `ScanResult` into
31/// `SchemaManager::ingest` as if it were a single-page response.
32#[derive(Debug, Clone)]
33pub struct ScanResult {
34    pub method: String,
35    pub result: Value,
36}
37
38/// Errors that can arise during a scan.
39#[derive(Debug)]
40pub enum ScanError {
41    Transport(String),
42    UnsupportedMode(ScanMode),
43    Aborted(String),
44}
45
46impl std::fmt::Display for ScanError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Transport(m) => write!(f, "upstream transport error: {m}"),
50            Self::UnsupportedMode(mode) => write!(f, "scan mode {mode:?} not supported"),
51            Self::Aborted(m) => write!(f, "scan aborted: {m}"),
52        }
53    }
54}
55
56impl std::error::Error for ScanError {}
57
58/// Drives active schema discovery against an upstream.
59///
60/// Implementations run the discovery handshake (`initialize` →
61/// `tools/list` / `resources/list` / `resources/templates/list` /
62/// `prompts/list`) and return the merged result of each call.
63pub trait SchemaScanner: Send + Sync + 'static {
64    fn scan(
65        &self,
66        upstream_id: &str,
67        mode: ScanMode,
68    ) -> impl Future<Output = Result<Vec<ScanResult>, ScanError>> + Send;
69}
70
71#[cfg(test)]
72#[allow(non_snake_case)]
73mod tests {
74    use super::*;
75
76    struct MockScanner;
77
78    impl SchemaScanner for MockScanner {
79        async fn scan(
80            &self,
81            _upstream_id: &str,
82            _mode: ScanMode,
83        ) -> Result<Vec<ScanResult>, ScanError> {
84            Ok(vec![])
85        }
86    }
87
88    #[tokio::test]
89    async fn scanner_trait__has_at_least_one_impl() {
90        let s = MockScanner;
91        let out = s.scan("my-proxy", ScanMode::Standalone).await.unwrap();
92        assert!(out.is_empty());
93    }
94
95    #[test]
96    fn scan_error__display_covers_all_variants() {
97        assert_eq!(
98            ScanError::Transport("boom".into()).to_string(),
99            "upstream transport error: boom"
100        );
101        assert!(
102            ScanError::UnsupportedMode(ScanMode::Standalone)
103                .to_string()
104                .contains("not supported")
105        );
106        assert_eq!(
107            ScanError::Aborted("cancelled".into()).to_string(),
108            "scan aborted: cancelled"
109        );
110    }
111}