Skip to main content

dravr_tronc/mcp/
server.rs

1// ABOUTME: Generic MCP server that routes JSON-RPC requests to protocol handlers and tools
2// ABOUTME: Implements initialize, tools/list, tools/call, and ping — parameterized over state S
3//
4// SPDX-License-Identifier: MIT OR Apache-2.0
5// Copyright (c) 2026 dravr.ai
6
7use std::sync::Arc;
8
9use serde::Serialize;
10use serde_json::Value;
11use tracing::debug;
12
13use crate::error::{
14    INTERNAL_ERROR, INVALID_PARAMS, INVALID_REQUEST, METHOD_NOT_FOUND, PARSE_ERROR,
15    UNSUPPORTED_PROTOCOL_VERSION,
16};
17use crate::mcp::auth::{AuthError, AuthHook};
18use crate::mcp::host::{MethodHandler, ToolDispatcher};
19use crate::mcp::modern::{
20    DiscoverResult, ModernMeta, ModernRequestMeta, PROTOCOL_VERSION_2026_07_28,
21};
22use crate::mcp::protocol::{JsonRpcRequest, JsonRpcResponse, JSONRPC_VERSION, PROTOCOL_VERSION};
23use crate::mcp::schema::{
24    InitializeRequest, InitializeResponse, ServerCapabilities, ServerInfo, ToolCall, ToolResponse,
25};
26use crate::mcp::tool::{ToolContext, ToolRegistry};
27
28/// The protocol revisions a default [`McpServer`] advertises, in preference
29/// order: the modern stateless era first, then the current legacy revision.
30fn default_supported_versions() -> Vec<String> {
31    vec![
32        PROTOCOL_VERSION_2026_07_28.to_owned(),
33        PROTOCOL_VERSION.to_owned(),
34    ]
35}
36
37/// MCP server that dispatches JSON-RPC requests to the appropriate handler
38///
39/// Generic over `S` — the project-specific server state type, shared as
40/// `Arc<S>`. `S` is `?Sized`, so a host may parameterize it with a resource
41/// façade trait object (`dyn HostRuntime`); a host needing interior mutability
42/// parameterizes `S` with it (e.g. `RwLock<Inner>`).
43/// Owns the shared state and tool registry. Transport layers feed parsed
44/// requests into `handle_request` and send the returned responses.
45pub struct McpServer<S: Send + Sync + ?Sized> {
46    name: String,
47    version: String,
48    state: Arc<S>,
49    tools: ToolRegistry<S>,
50    capabilities: ServerCapabilities,
51    instructions: Option<String>,
52    supported_versions: Vec<String>,
53    auth_hook: Option<Arc<dyn AuthHook<S>>>,
54    allowed_origins: Vec<String>,
55    tool_dispatcher: Option<Arc<dyn ToolDispatcher<S>>>,
56    method_handler: Option<Arc<dyn MethodHandler<S>>>,
57}
58
59impl<S: Send + Sync + ?Sized + 'static> McpServer<S> {
60    /// Create a server with the given name, version, tool registry, and shared state
61    ///
62    /// Defaults to advertising tool support only, no instructions, and the
63    /// modern + current legacy protocol revisions. Use the `with_*` builders to
64    /// override the advertised capabilities, instructions, or supported versions.
65    pub fn new(
66        name: impl Into<String>,
67        version: impl Into<String>,
68        tools: ToolRegistry<S>,
69        state: Arc<S>,
70    ) -> Self {
71        Self {
72            name: name.into(),
73            version: version.into(),
74            state,
75            tools,
76            capabilities: ServerCapabilities::tools_only(),
77            instructions: None,
78            supported_versions: default_supported_versions(),
79            auth_hook: None,
80            allowed_origins: Vec::new(),
81            tool_dispatcher: None,
82            method_handler: None,
83        }
84    }
85
86    /// Override the capabilities advertised in `initialize` and `server/discover`.
87    #[must_use]
88    pub fn with_capabilities(mut self, capabilities: ServerCapabilities) -> Self {
89        self.capabilities = capabilities;
90        self
91    }
92
93    /// Set the natural-language instructions advertised to clients.
94    #[must_use]
95    pub fn with_instructions(mut self, instructions: impl Into<String>) -> Self {
96        self.instructions = Some(instructions.into());
97        self
98    }
99
100    /// Override the protocol revisions the server advertises and accepts, in
101    /// preference order.
102    #[must_use]
103    pub fn with_supported_versions(mut self, versions: Vec<String>) -> Self {
104        self.supported_versions = versions;
105        self
106    }
107
108    /// Install a host authentication hook for the HTTP transport. With a hook,
109    /// the transport authenticates every request (rejecting with 401/403); with
110    /// none, every request runs as the default anonymous context.
111    #[must_use]
112    pub fn with_auth_hook(mut self, auth_hook: Arc<dyn AuthHook<S>>) -> Self {
113        self.auth_hook = Some(auth_hook);
114        self
115    }
116
117    /// Restrict the `Origin`s the HTTP transport accepts. An empty list (the
118    /// default) or one containing `"*"` allows any origin; a request whose
119    /// `Origin` header is present and not listed is rejected with 403.
120    #[must_use]
121    pub fn with_allowed_origins(mut self, origins: Vec<String>) -> Self {
122        self.allowed_origins = origins;
123        self
124    }
125
126    /// The `Origin` allowlist the HTTP transport enforces.
127    pub fn allowed_origins(&self) -> &[String] {
128        &self.allowed_origins
129    }
130
131    /// Install a host [`ToolDispatcher`] that owns `tools/list` and `tools/call`
132    /// (per-caller views, quota, execution, usage). When installed it replaces
133    /// the built-in registry for both tool methods.
134    #[must_use]
135    pub fn with_tool_dispatcher(mut self, dispatcher: Arc<dyn ToolDispatcher<S>>) -> Self {
136        self.tool_dispatcher = Some(dispatcher);
137        self
138    }
139
140    /// Install a host [`MethodHandler`] for methods the engine doesn't natively
141    /// serve (`resources/*`, `prompts/*`, `sampling/*`, …). Unknown methods are
142    /// offered to it before falling through to method-not-found.
143    #[must_use]
144    pub fn with_method_handler(mut self, handler: Arc<dyn MethodHandler<S>>) -> Self {
145        self.method_handler = Some(handler);
146        self
147    }
148
149    /// Authenticate a request via the configured [`AuthHook`], or yield the
150    /// default anonymous [`ToolContext`] when no hook is installed.
151    ///
152    /// # Errors
153    /// Returns the hook's [`AuthError`] (401/403) when authentication fails.
154    pub async fn authenticate(&self, request: &JsonRpcRequest) -> Result<ToolContext, AuthError> {
155        match &self.auth_hook {
156            Some(hook) => hook.authenticate(request, &self.state).await,
157            None => Ok(ToolContext::default()),
158        }
159    }
160
161    /// Route a raw JSON string to the appropriate MCP handler
162    ///
163    /// Parses the string as a `JsonRpcRequest`, dispatches it, and returns
164    /// the serialized response. Returns `None` for notifications.
165    pub async fn handle_raw(&self, raw: &str) -> Option<JsonRpcResponse> {
166        let request: JsonRpcRequest = match serde_json::from_str(raw) {
167            Ok(req) => req,
168            Err(e) => {
169                return Some(JsonRpcResponse::error(
170                    None,
171                    PARSE_ERROR,
172                    format!("Parse error: {e}"),
173                ));
174            }
175        };
176        self.handle_request(request).await
177    }
178
179    /// Route a parsed JSON-RPC request under the default anonymous context.
180    ///
181    /// Convenience for transports without authentication (e.g. stdio). See
182    /// [`Self::handle_request_with_context`] for the authenticated path.
183    pub async fn handle_request(&self, request: JsonRpcRequest) -> Option<JsonRpcResponse> {
184        self.handle_request_with_context(request, &ToolContext::default())
185            .await
186    }
187
188    /// Route a parsed JSON-RPC request, dispatching tool calls under the given
189    /// per-call [`ToolContext`] (resolved by the transport's auth hook).
190    ///
191    /// Performs era detection on each request: one carrying modern per-request
192    /// `_meta` (revision 2026-07-28) is served statelessly via
193    /// [`Self::process_modern`]; otherwise it follows the legacy
194    /// `initialize`/session path. Returns `None` for notifications (no id).
195    pub async fn handle_request_with_context(
196        &self,
197        request: JsonRpcRequest,
198        ctx: &ToolContext,
199    ) -> Option<JsonRpcResponse> {
200        // Validate JSON-RPC protocol version
201        if request.jsonrpc != JSONRPC_VERSION {
202            return Some(JsonRpcResponse::error(
203                request.id,
204                INVALID_REQUEST,
205                format!("Unsupported JSON-RPC version: {}", request.jsonrpc),
206            ));
207        }
208
209        // Notifications have no id and expect no response
210        if request.id.is_none() {
211            debug!(method = %request.method, "Received notification, no response");
212            return None;
213        }
214
215        // Era detection — see `mcp::modern` + the dual-era spec.
216        let response = match ModernRequestMeta::from_params(request.params.as_ref()) {
217            ModernMeta::Malformed(reason) => {
218                JsonRpcResponse::error(request.id, INVALID_PARAMS, reason)
219            }
220            ModernMeta::Modern(meta) => self.process_modern(request, *meta, ctx).await,
221            ModernMeta::Legacy => self.process_legacy(request, ctx).await,
222        };
223
224        Some(response)
225    }
226
227    /// Dispatch a legacy (`initialize`/session) request.
228    async fn process_legacy(&self, request: JsonRpcRequest, ctx: &ToolContext) -> JsonRpcResponse {
229        match request.method.as_str() {
230            "initialize" => self.handle_initialize(request.id, request.params.as_ref()),
231            "tools/list" => self.handle_tools_list(request.id, ctx).await,
232            "tools/call" => {
233                self.handle_tools_call(request.id, request.params, ctx)
234                    .await
235            }
236            "server/discover" => self.handle_server_discover(request.id),
237            "ping" => JsonRpcResponse::success(request.id, Value::Object(serde_json::Map::new())),
238            other => {
239                self.handle_unknown_method(other, request.id, request.params, ctx)
240                    .await
241            }
242        }
243    }
244
245    /// Dispatch a modern (2026-07-28, stateless per-request `_meta`) request.
246    ///
247    /// Rejects unsupported protocol versions with `UnsupportedProtocolVersionError`
248    /// (-32004), routes the operation through the shared handlers, and frames a
249    /// successful result with `resultType`. Legacy-only lifecycle methods
250    /// (`initialize`, `ping`) are not valid here and fall through to
251    /// method-not-found.
252    async fn process_modern(
253        &self,
254        request: JsonRpcRequest,
255        meta: ModernRequestMeta,
256        ctx: &ToolContext,
257    ) -> JsonRpcResponse {
258        if !self.supports_version(&meta.protocol_version) {
259            return self.unsupported_version_error(request.id, &meta.protocol_version);
260        }
261
262        let response = match request.method.as_str() {
263            "server/discover" => self.handle_server_discover(request.id),
264            "tools/list" => self.handle_tools_list(request.id, ctx).await,
265            "tools/call" => {
266                self.handle_tools_call(request.id, request.params, ctx)
267                    .await
268            }
269            other => {
270                self.handle_unknown_method(other, request.id, request.params, ctx)
271                    .await
272            }
273        };
274
275        Self::frame_modern_result(response)
276    }
277
278    /// Whether the server advertises and accepts the given protocol revision.
279    fn supports_version(&self, version: &str) -> bool {
280        self.supported_versions.iter().any(|v| v == version)
281    }
282
283    /// Handle `initialize` — negotiate the protocol version and advertise the
284    /// server's identity, capabilities, and instructions.
285    ///
286    /// Echoes the client's requested version when supported; otherwise responds
287    /// with the server's current legacy revision (the spec lets the client then
288    /// decide whether to proceed).
289    fn handle_initialize(&self, id: Option<Value>, params: Option<&Value>) -> JsonRpcResponse {
290        let init = params.and_then(|p| serde_json::from_value::<InitializeRequest>(p.clone()).ok());
291
292        if let Some(req) = &init {
293            debug!(
294                client = %req.client_info.name,
295                version = %req.client_info.version,
296                protocol = %req.protocol_version,
297                "MCP client connected"
298            );
299        }
300
301        let negotiated_version = match &init {
302            Some(req) if self.supports_version(&req.protocol_version) => {
303                req.protocol_version.clone()
304            }
305            _ => PROTOCOL_VERSION.to_owned(),
306        };
307
308        let result = InitializeResponse::new(
309            negotiated_version,
310            ServerInfo::new(self.name.clone(), self.version.clone()),
311            self.capabilities.clone(),
312            self.instructions.clone(),
313        );
314
315        Self::success_or_error(id, &result)
316    }
317
318    /// Handle the modern `server/discover` RPC — advertise the supported
319    /// protocol versions, capabilities, and identity. Answerable on either era
320    /// and carries no session state.
321    fn handle_server_discover(&self, id: Option<Value>) -> JsonRpcResponse {
322        let discover = DiscoverResult::new(
323            self.supported_versions.clone(),
324            self.capabilities.clone(),
325            ServerInfo::new(self.name.clone(), self.version.clone()),
326            self.instructions.clone(),
327        );
328        Self::success_or_error(id, &discover)
329    }
330
331    /// Build an `UnsupportedProtocolVersionError` (-32004) listing the versions
332    /// the server supports and echoing the requested one.
333    fn unsupported_version_error(&self, id: Option<Value>, requested: &str) -> JsonRpcResponse {
334        let data = serde_json::json!({
335            "supported": self.supported_versions,
336            "requested": requested,
337        });
338        JsonRpcResponse::error_with_data(
339            id,
340            UNSUPPORTED_PROTOCOL_VERSION,
341            "Unsupported protocol version",
342            data,
343        )
344    }
345
346    /// Frame a modern response's successful result with a `resultType` (defaults
347    /// to `"complete"`), as required by revision 2026-07-28. Error responses and
348    /// non-object results pass through unchanged.
349    fn frame_modern_result(mut response: JsonRpcResponse) -> JsonRpcResponse {
350        if let Some(obj) = response.result.as_mut().and_then(Value::as_object_mut) {
351            obj.entry("resultType")
352                .or_insert_with(|| Value::String("complete".to_owned()));
353        }
354        response
355    }
356
357    /// Serialize a result value into a success response, or an internal error.
358    fn success_or_error<T: Serialize>(id: Option<Value>, value: &T) -> JsonRpcResponse {
359        match serde_json::to_value(value) {
360            Ok(val) => JsonRpcResponse::success(id, val),
361            Err(e) => {
362                JsonRpcResponse::error(id, INTERNAL_ERROR, format!("Serialization error: {e}"))
363            }
364        }
365    }
366
367    /// Offer a method the engine doesn't natively serve to the host
368    /// [`MethodHandler`], falling through to method-not-found when there is no
369    /// handler or it declines the method.
370    async fn handle_unknown_method(
371        &self,
372        method: &str,
373        id: Option<Value>,
374        params: Option<Value>,
375        ctx: &ToolContext,
376    ) -> JsonRpcResponse {
377        if let Some(handler) = &self.method_handler {
378            if let Some(response) = handler
379                .handle(method, id.clone(), params, &self.state, ctx)
380                .await
381            {
382                return response;
383            }
384        }
385        debug!(method, "Unknown MCP method");
386        JsonRpcResponse::error(id, METHOD_NOT_FOUND, format!("Method not found: {method}"))
387    }
388
389    /// Handle `tools/list` — return the tool definitions visible to the caller.
390    ///
391    /// Uses the host [`ToolListProvider`] when installed (e.g. for tenant
392    /// scoping); otherwise lists every registered tool.
393    async fn handle_tools_list(&self, id: Option<Value>, ctx: &ToolContext) -> JsonRpcResponse {
394        let definitions = match &self.tool_dispatcher {
395            Some(dispatcher) => dispatcher.list_tools(&self.state, ctx).await,
396            None => self.tools.list_definitions(),
397        };
398        match serde_json::to_value(definitions) {
399            Ok(tools) => {
400                let mut result = serde_json::Map::new();
401                result.insert("tools".to_owned(), tools);
402                JsonRpcResponse::success(id, Value::Object(result))
403            }
404            Err(e) => {
405                JsonRpcResponse::error(id, INTERNAL_ERROR, format!("Serialization error: {e}"))
406            }
407        }
408    }
409
410    /// Handle `tools/call` — dispatch to the named tool handler under `ctx`
411    async fn handle_tools_call(
412        &self,
413        id: Option<Value>,
414        params: Option<Value>,
415        ctx: &ToolContext,
416    ) -> JsonRpcResponse {
417        let call: ToolCall = match params {
418            Some(p) => match serde_json::from_value(p) {
419                Ok(cp) => cp,
420                Err(e) => {
421                    return JsonRpcResponse::error(
422                        id,
423                        INVALID_PARAMS,
424                        format!("Invalid params: {e}"),
425                    );
426                }
427            },
428            None => {
429                return JsonRpcResponse::error(
430                    id,
431                    INVALID_PARAMS,
432                    "Missing params for tools/call".to_owned(),
433                );
434            }
435        };
436
437        let arguments = call
438            .arguments
439            .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
440
441        // A host dispatcher owns the whole call (quota, exec, usage); otherwise
442        // execute against the built-in registry.
443        let result = match &self.tool_dispatcher {
444            Some(dispatcher) => {
445                dispatcher
446                    .call_tool(&call.name, &self.state, ctx, arguments)
447                    .await
448            }
449            None => {
450                self.tools
451                    .execute(&call.name, &self.state, ctx, arguments)
452                    .await
453            }
454        };
455
456        Self::tool_response_result(id, &result)
457    }
458
459    /// Serialize a [`ToolResponse`] into a `tools/call` success response, or an
460    /// internal error if serialization fails.
461    fn tool_response_result(id: Option<Value>, result: &ToolResponse) -> JsonRpcResponse {
462        match serde_json::to_value(result) {
463            Ok(val) => JsonRpcResponse::success(id, val),
464            Err(e) => JsonRpcResponse::error(
465                id,
466                INTERNAL_ERROR,
467                format!("Result serialization error: {e}"),
468            ),
469        }
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::mcp::schema::{Tool, ToolResponse};
477    use crate::mcp::tool::McpTool;
478    use serde_json::json;
479
480    struct TestState;
481
482    struct PingTool;
483
484    #[async_trait::async_trait]
485    impl McpTool<TestState> for PingTool {
486        fn definition(&self) -> Tool {
487            Tool {
488                name: "ping_tool".to_owned(),
489                description: "Returns pong".to_owned(),
490                input_schema: json!({"type": "object"}),
491                annotations: None,
492            }
493        }
494
495        async fn execute(
496            &self,
497            _state: &Arc<TestState>,
498            _ctx: &ToolContext,
499            _arguments: Value,
500        ) -> ToolResponse {
501            ToolResponse::text("pong".to_owned())
502        }
503    }
504
505    fn make_server() -> McpServer<TestState> {
506        let mut registry = ToolRegistry::new();
507        registry.register(Box::new(PingTool));
508        let state = Arc::new(TestState);
509        McpServer::new("test-server", "0.1.0", registry, state)
510    }
511
512    #[tokio::test]
513    async fn handle_initialize() {
514        let server = make_server();
515        let raw = r#"{
516            "jsonrpc": "2.0",
517            "id": 1,
518            "method": "initialize",
519            "params": {
520                "protocolVersion": "2024-11-05",
521                "capabilities": {},
522                "clientInfo": { "name": "test-client" }
523            }
524        }"#;
525        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
526        let result = resp.result.expect("result"); // Safe: test assertion
527        assert_eq!(result["protocolVersion"], PROTOCOL_VERSION);
528        assert_eq!(result["serverInfo"]["name"], "test-server");
529        assert_eq!(result["serverInfo"]["version"], "0.1.0");
530    }
531
532    #[tokio::test]
533    async fn handle_initialize_without_params() {
534        let server = make_server();
535        let raw = r#"{"jsonrpc": "2.0", "id": 1, "method": "initialize"}"#;
536        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
537        assert!(resp.result.is_some());
538        assert!(resp.error.is_none());
539    }
540
541    #[tokio::test]
542    async fn handle_tools_list() {
543        let server = make_server();
544        let raw = r#"{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}"#;
545        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
546        let result = resp.result.expect("result"); // Safe: test assertion
547        let tools = result["tools"].as_array().expect("tools array"); // Safe: test assertion
548        assert_eq!(tools.len(), 1);
549        assert_eq!(tools[0]["name"], "ping_tool");
550    }
551
552    #[tokio::test]
553    async fn handle_tools_call() {
554        let server = make_server();
555        let raw = r#"{
556            "jsonrpc": "2.0",
557            "id": 3,
558            "method": "tools/call",
559            "params": { "name": "ping_tool", "arguments": {} }
560        }"#;
561        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
562        let result = resp.result.expect("result"); // Safe: test assertion
563        assert_eq!(result["content"][0]["text"], "pong");
564    }
565
566    #[tokio::test]
567    async fn handle_tools_call_unknown_tool() {
568        let server = make_server();
569        let raw = r#"{
570            "jsonrpc": "2.0",
571            "id": 4,
572            "method": "tools/call",
573            "params": { "name": "nonexistent" }
574        }"#;
575        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
576        let result = resp.result.expect("result"); // Safe: test assertion
577        assert_eq!(result["isError"], true);
578        assert!(result["content"][0]["text"]
579            .as_str()
580            .expect("text") // Safe: test assertion
581            .contains("Unknown tool"));
582    }
583
584    #[tokio::test]
585    async fn handle_tools_call_missing_params() {
586        let server = make_server();
587        let raw = r#"{"jsonrpc": "2.0", "id": 5, "method": "tools/call"}"#;
588        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
589        let err = resp.error.expect("error"); // Safe: test assertion
590        assert_eq!(err.code, INVALID_PARAMS);
591    }
592
593    #[tokio::test]
594    async fn handle_ping() {
595        let server = make_server();
596        let raw = r#"{"jsonrpc": "2.0", "id": 6, "method": "ping"}"#;
597        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
598        assert!(resp.result.is_some());
599        assert!(resp.error.is_none());
600    }
601
602    #[tokio::test]
603    async fn handle_unknown_method() {
604        let server = make_server();
605        let raw = r#"{"jsonrpc": "2.0", "id": 7, "method": "bogus/method"}"#;
606        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
607        let err = resp.error.expect("error"); // Safe: test assertion
608        assert_eq!(err.code, METHOD_NOT_FOUND);
609        assert!(err.message.contains("bogus/method"));
610    }
611
612    #[tokio::test]
613    async fn handle_invalid_json() {
614        let server = make_server();
615        let resp = server
616            .handle_raw("not json at all")
617            .await
618            .expect("response"); // Safe: test assertion
619        let err = resp.error.expect("error"); // Safe: test assertion
620        assert_eq!(err.code, PARSE_ERROR);
621    }
622
623    #[tokio::test]
624    async fn handle_wrong_jsonrpc_version() {
625        let server = make_server();
626        let raw = r#"{"jsonrpc": "1.0", "id": 8, "method": "ping"}"#;
627        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
628        let err = resp.error.expect("error"); // Safe: test assertion
629        assert_eq!(err.code, INVALID_REQUEST);
630    }
631
632    #[tokio::test]
633    async fn notification_returns_none() {
634        let server = make_server();
635        let raw = r#"{"jsonrpc": "2.0", "method": "notifications/cancelled"}"#;
636        let resp = server.handle_raw(raw).await;
637        assert!(resp.is_none());
638    }
639
640    #[tokio::test]
641    async fn response_id_matches_request_id() {
642        let server = make_server();
643        let raw = r#"{"jsonrpc": "2.0", "id": 999, "method": "ping"}"#;
644        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
645        assert_eq!(resp.id, Some(Value::from(999)));
646    }
647
648    #[tokio::test]
649    async fn tools_call_with_no_arguments_defaults_to_empty_object() {
650        let server = make_server();
651        let raw = r#"{
652            "jsonrpc": "2.0",
653            "id": 10,
654            "method": "tools/call",
655            "params": { "name": "ping_tool" }
656        }"#;
657        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
658        let result = resp.result.expect("result"); // Safe: test assertion
659        assert_eq!(result["content"][0]["text"], "pong");
660    }
661
662    #[tokio::test]
663    async fn tools_call_with_invalid_params_structure() {
664        let server = make_server();
665        let raw = r#"{
666            "jsonrpc": "2.0",
667            "id": 11,
668            "method": "tools/call",
669            "params": "not an object"
670        }"#;
671        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
672        let err = resp.error.expect("error"); // Safe: test assertion
673        assert_eq!(err.code, INVALID_PARAMS);
674    }
675
676    #[tokio::test]
677    async fn server_discover_advertises_versions_and_tools() {
678        let server = make_server();
679        let raw = r#"{"jsonrpc": "2.0", "id": 20, "method": "server/discover"}"#;
680        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
681        let result = resp.result.expect("result"); // Safe: test assertion
682        assert_eq!(result["resultType"], "complete");
683        let versions: Vec<&str> = result["supportedVersions"]
684            .as_array()
685            .expect("versions") // Safe: test assertion
686            .iter()
687            .filter_map(Value::as_str)
688            .collect();
689        assert!(versions.contains(&"2026-07-28"));
690        assert!(versions.contains(&"2025-11-25"));
691        assert!(result["capabilities"]["tools"].is_object());
692        assert_eq!(result["serverInfo"]["name"], "test-server");
693    }
694
695    #[tokio::test]
696    async fn initialize_echoes_supported_client_version() {
697        let server = make_server();
698        let raw = r#"{
699            "jsonrpc": "2.0",
700            "id": 21,
701            "method": "initialize",
702            "params": {
703                "protocolVersion": "2026-07-28",
704                "capabilities": {},
705                "clientInfo": { "name": "c", "version": "1" }
706            }
707        }"#;
708        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
709        let result = resp.result.expect("result"); // Safe: test assertion
710        assert_eq!(result["protocolVersion"], "2026-07-28");
711    }
712
713    #[tokio::test]
714    async fn modern_tools_list_frames_result_type() {
715        let server = make_server();
716        let raw = r#"{
717            "jsonrpc": "2.0",
718            "id": 22,
719            "method": "tools/list",
720            "params": {
721                "_meta": {
722                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
723                    "io.modelcontextprotocol/clientInfo": { "name": "c", "version": "1" },
724                    "io.modelcontextprotocol/clientCapabilities": {}
725                }
726            }
727        }"#;
728        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
729        let result = resp.result.expect("result"); // Safe: test assertion
730        assert_eq!(result["resultType"], "complete");
731        assert_eq!(result["tools"][0]["name"], "ping_tool");
732    }
733
734    #[tokio::test]
735    async fn modern_unsupported_version_returns_minus_32004() {
736        let server = make_server();
737        let raw = r#"{
738            "jsonrpc": "2.0",
739            "id": 23,
740            "method": "tools/list",
741            "params": {
742                "_meta": {
743                    "io.modelcontextprotocol/protocolVersion": "1999-01-01",
744                    "io.modelcontextprotocol/clientInfo": { "name": "c", "version": "1" },
745                    "io.modelcontextprotocol/clientCapabilities": {}
746                }
747            }
748        }"#;
749        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
750        let err = resp.error.expect("error"); // Safe: test assertion
751        assert_eq!(err.code, UNSUPPORTED_PROTOCOL_VERSION);
752        let data = err.data.expect("data"); // Safe: test assertion
753        assert_eq!(data["requested"], "1999-01-01");
754        assert!(data["supported"]
755            .as_array()
756            .expect("supported") // Safe: test assertion
757            .iter()
758            .any(|v| v == "2026-07-28"));
759    }
760
761    #[tokio::test]
762    async fn modern_malformed_meta_returns_invalid_params() {
763        let server = make_server();
764        // protocolVersion present (modern) but clientInfo missing => malformed.
765        let raw = r#"{
766            "jsonrpc": "2.0",
767            "id": 24,
768            "method": "tools/list",
769            "params": {
770                "_meta": {
771                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
772                    "io.modelcontextprotocol/clientCapabilities": {}
773                }
774            }
775        }"#;
776        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
777        let err = resp.error.expect("error"); // Safe: test assertion
778        assert_eq!(err.code, INVALID_PARAMS);
779    }
780
781    // ---- Host-integration seams (host.rs) ----
782
783    use crate::mcp::host::{MethodHandler, ToolDispatcher};
784
785    /// Host dispatcher that owns both tool methods: a tenant-scoped `tools/list`
786    /// view and a `tools/call` that routes entirely host-side (no registry).
787    struct ScopedDispatcher;
788
789    #[async_trait::async_trait]
790    impl ToolDispatcher<TestState> for ScopedDispatcher {
791        async fn list_tools(&self, _state: &Arc<TestState>, ctx: &ToolContext) -> Vec<Tool> {
792            if ctx.tenant_id.is_some() {
793                vec![Tool {
794                    name: "scoped_tool".to_owned(),
795                    description: "tenant-scoped".to_owned(),
796                    input_schema: json!({"type": "object"}),
797                    annotations: None,
798                }]
799            } else {
800                Vec::new()
801            }
802        }
803
804        async fn call_tool(
805            &self,
806            name: &str,
807            _state: &Arc<TestState>,
808            _ctx: &ToolContext,
809            _arguments: Value,
810        ) -> ToolResponse {
811            match name {
812                "scoped_tool" => ToolResponse::text("dispatched".to_owned()),
813                other => ToolResponse::error(format!("quota exceeded for {other}")),
814            }
815        }
816    }
817
818    /// Serves `resources/list` and declines everything else (returns `None`).
819    struct ResourcesMethodHandler;
820
821    #[async_trait::async_trait]
822    impl MethodHandler<TestState> for ResourcesMethodHandler {
823        async fn handle(
824            &self,
825            method: &str,
826            id: Option<Value>,
827            _params: Option<Value>,
828            _state: &Arc<TestState>,
829            _ctx: &ToolContext,
830        ) -> Option<JsonRpcResponse> {
831            if method == "resources/list" {
832                Some(JsonRpcResponse::success(id, json!({ "resources": [] })))
833            } else {
834                None
835            }
836        }
837    }
838
839    #[tokio::test]
840    async fn dispatcher_list_empty_without_tenant() {
841        let server = make_server().with_tool_dispatcher(Arc::new(ScopedDispatcher));
842        // No tenant in the default context → dispatcher returns an empty list.
843        let raw = r#"{"jsonrpc": "2.0", "id": 30, "method": "tools/list"}"#;
844        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
845        let result = resp.result.expect("result"); // Safe: test assertion
846        let tools = result["tools"].as_array().expect("tools array"); // Safe: test assertion
847        assert!(tools.is_empty(), "no tenant → dispatcher yields no tools");
848    }
849
850    #[tokio::test]
851    async fn dispatcher_list_scoped_with_tenant() {
852        let server = make_server().with_tool_dispatcher(Arc::new(ScopedDispatcher));
853        let request: JsonRpcRequest =
854            serde_json::from_str(r#"{"jsonrpc": "2.0", "id": 31, "method": "tools/list"}"#)
855                .expect("request"); // Safe: test assertion
856        let ctx = ToolContext::new().with_tenant("tenant-1");
857        let resp = server
858            .handle_request_with_context(request, &ctx)
859            .await
860            .expect("response"); // Safe: test assertion
861        let result = resp.result.expect("result"); // Safe: test assertion
862        let tools = result["tools"].as_array().expect("tools array"); // Safe: test assertion
863        assert_eq!(tools.len(), 1);
864        assert_eq!(tools[0]["name"], "scoped_tool");
865    }
866
867    #[tokio::test]
868    async fn method_handler_serves_non_tool_method() {
869        let server = make_server().with_method_handler(Arc::new(ResourcesMethodHandler));
870        let raw = r#"{"jsonrpc": "2.0", "id": 32, "method": "resources/list"}"#;
871        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
872        let result = resp.result.expect("result"); // Safe: test assertion
873        assert!(result["resources"].is_array());
874        assert!(resp.error.is_none());
875    }
876
877    #[tokio::test]
878    async fn method_handler_declines_falls_through_to_method_not_found() {
879        let server = make_server().with_method_handler(Arc::new(ResourcesMethodHandler));
880        let raw = r#"{"jsonrpc": "2.0", "id": 33, "method": "prompts/list"}"#;
881        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
882        let err = resp.error.expect("error"); // Safe: test assertion
883        assert_eq!(err.code, METHOD_NOT_FOUND);
884        assert!(err.message.contains("prompts/list"));
885    }
886
887    #[tokio::test]
888    async fn dispatcher_call_routes_host_side() {
889        // With a dispatcher installed, tools/call bypasses the registry entirely
890        // and runs the host's call_tool (here: echo for the known tool).
891        let server = make_server().with_tool_dispatcher(Arc::new(ScopedDispatcher));
892        let raw = r#"{
893            "jsonrpc": "2.0",
894            "id": 34,
895            "method": "tools/call",
896            "params": { "name": "scoped_tool", "arguments": {} }
897        }"#;
898        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
899        let result = resp.result.expect("result"); // Safe: test assertion
900        assert_eq!(result["content"][0]["text"], "dispatched");
901    }
902
903    #[tokio::test]
904    async fn dispatcher_call_reports_host_error() {
905        // The dispatcher decides errors host-side (e.g. quota); even the registry's
906        // own `ping_tool` is invisible to the dispatcher path.
907        let server = make_server().with_tool_dispatcher(Arc::new(ScopedDispatcher));
908        let raw = r#"{
909            "jsonrpc": "2.0",
910            "id": 35,
911            "method": "tools/call",
912            "params": { "name": "ping_tool", "arguments": {} }
913        }"#;
914        let resp = server.handle_raw(raw).await.expect("response"); // Safe: test assertion
915        let result = resp.result.expect("result"); // Safe: test assertion
916        assert_eq!(result["isError"], true);
917        assert!(result["content"][0]["text"]
918            .as_str()
919            .expect("text") // Safe: test assertion
920            .contains("quota exceeded"));
921    }
922}