Skip to main content

llm_kernel/mcp/
transport.rs

1//! JSON-RPC 2.0 transport and dispatch for MCP.
2//!
3//! Reads JSON-RPC requests from stdin (stdio transport) and routes them
4//! to the appropriate [`McpServer`] handler. Writes responses to stdout.
5
6use std::io::{self, BufRead, Write};
7
8use crate::mcp::server::{
9    McpServer, SUPPORTED_PROTOCOL_VERSIONS, request_protocol_version, shape_modern_result,
10};
11
12/// Cap on a single stdio JSON-RPC line. Reading is bounded by
13/// [`read_limited_line`], so a peer that never sends `\n` can never make the
14/// process buffer more than this many bytes — fatal under a
15/// `panic = "abort"` release profile otherwise.
16const MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
17
18/// JSON-RPC error code for a request sent before `initialize` (MCP lifecycle).
19const ERR_NOT_INITIALIZED: i32 = -32002;
20
21/// Read one line (up to and including `\n`) into `buf`, capping input at
22/// `max` bytes per line.
23///
24/// Unlike `BufRead::lines`, memory use is bounded *before* the newline
25/// arrives: once `max` bytes accumulate without one, the read fails instead of
26/// the buffer growing. Returns bytes read (`0` at EOF); fails with
27/// `InvalidData` on an over-long line.
28fn read_limited_line<R: BufRead>(
29    reader: &mut R,
30    max: usize,
31    buf: &mut Vec<u8>,
32) -> io::Result<usize> {
33    buf.clear();
34    let mut total = 0usize;
35    loop {
36        let available = reader.fill_buf()?;
37        if available.is_empty() {
38            return Ok(total); // EOF
39        }
40        let newline = available.iter().position(|&b| b == b'\n');
41        let take = newline.map_or(available.len(), |i| i + 1);
42        if total + take > max {
43            return Err(io::Error::new(
44                io::ErrorKind::InvalidData,
45                format!("JSON-RPC line exceeds {max} bytes"),
46            ));
47        }
48        buf.extend_from_slice(&available[..take]);
49        reader.consume(take);
50        total += take;
51        if newline.is_some() {
52            return Ok(total);
53        }
54    }
55}
56
57/// JSON-RPC 2.0 dispatcher for MCP stdio transport.
58pub struct JsonRpcDispatcher<'a> {
59    server: &'a McpServer,
60}
61
62impl<'a> JsonRpcDispatcher<'a> {
63    /// Create a new dispatcher wrapping an MCP server.
64    pub fn new(server: &'a McpServer) -> Self {
65        Self { server }
66    }
67
68    /// Dispatch a request, checking the `Authorization` header if auth is configured.
69    ///
70    /// Returns a JSON-RPC `-32001` error response when auth fails.
71    /// Pass `None` for `auth_header` when no header is present (stdio transport).
72    pub fn dispatch_authenticated(
73        &self,
74        request: &str,
75        auth_header: Option<&str>,
76    ) -> Option<String> {
77        let provided = auth_header.unwrap_or("");
78        if !self.server.check_auth(provided) {
79            // Echo back the request id (string or number) when we can parse one,
80            // else a null id per JSON-RPC.
81            let id = serde_json::from_str::<serde_json::Value>(request.trim())
82                .ok()
83                .and_then(|v| v.get("id").cloned())
84                .unwrap_or(serde_json::Value::Null);
85            return Some(self.error_response(id, -32001, "Unauthorized"));
86        }
87        self.dispatch(request)
88    }
89
90    /// Dispatch a JSON-RPC request (single or batch) and return the response.
91    ///
92    /// JSON-RPC batches were removed from the MCP spec in `2025-06-18`; they
93    /// are still accepted here for clients negotiating `2024-11-05`.
94    pub fn dispatch(&self, request: &str) -> Option<String> {
95        let trimmed = request.trim();
96        if trimmed.starts_with('[') {
97            let reqs: Vec<serde_json::Value> = match serde_json::from_str(trimmed) {
98                Ok(v) => v,
99                Err(e) => {
100                    return Some(self.error_response(
101                        serde_json::Value::Null,
102                        -32700,
103                        &format!("Parse error: {e}"),
104                    ));
105                }
106            };
107            // Batches were removed from the MCP spec in `2025-06-18`; they are
108            // tolerated for `2024-11-05` clients only. A modern request inside
109            // a batch is a protocol violation and rejects the whole batch.
110            if reqs.iter().any(|r| request_protocol_version(r).is_some()) {
111                return Some(self.error_response(
112                    serde_json::Value::Null,
113                    -32600,
114                    "Invalid Request: JSON-RPC batches are not supported by protocol revisions after 2024-11-05",
115                ));
116            }
117            let responses: Vec<String> = reqs
118                .iter()
119                .filter_map(|req| self.dispatch_single(req))
120                .collect();
121            if responses.is_empty() {
122                None
123            } else {
124                Some(format!("[{}]", responses.join(",")))
125            }
126        } else {
127            let req: serde_json::Value = match serde_json::from_str(trimmed) {
128                Ok(v) => v,
129                Err(e) => {
130                    return Some(self.error_response(
131                        serde_json::Value::Null,
132                        -32700,
133                        &format!("Parse error: {e}"),
134                    ));
135                }
136            };
137            self.dispatch_single(&req)
138        }
139    }
140
141    /// Dispatch a single pre-parsed JSON-RPC request.
142    fn dispatch_single(&self, req: &serde_json::Value) -> Option<String> {
143        // A non-object is never a valid request — answering with silence
144        // (the notification path) leaves the client waiting forever.
145        if !req.is_object() {
146            return Some(self.error_response(
147                serde_json::Value::Null,
148                -32600,
149                "Invalid Request: expected a JSON object",
150            ));
151        }
152        // Notifications (the `id` member is absent) don't get responses. This is
153        // distinct from a null id, which is a request that must be answered.
154        req.get("id")?;
155        // Preserve the id verbatim (JSON-RPC ids may be a string or a number).
156        let id = req.get("id").cloned().unwrap_or(serde_json::Value::Null);
157        let method = req.get("method").and_then(|v| v.as_str()).unwrap_or("");
158
159        // Era selection (dual-era server): a request declaring its protocol
160        // version in `_meta` is served statelessly; `initialize` always
161        // selects legacy handshake semantics, even when it carries `_meta`.
162        let modern = if method == "initialize" {
163            None
164        } else {
165            request_protocol_version(req)
166        };
167        if let Some(version) = modern {
168            if let Some(err) = self.check_modern_version(&id, version) {
169                return Some(err);
170            }
171            // Methods that exist only in modern revisions (or were removed
172            // there) are answered before the shared handler table.
173            match method {
174                "server/discover" => {
175                    let mut result = self.server.discover_response();
176                    shape_modern_result(version, method, &mut result);
177                    return Some(self.success_response(id, result));
178                }
179                "subscriptions/listen" => return Some(self.subscription_close(&id)),
180                // `ping` was removed in 2026-07-28; it is legacy-only.
181                "ping" => {
182                    return Some(self.error_response(
183                        id,
184                        -32601,
185                        "Method not found: ping (removed in protocol 2026-07-28)",
186                    ));
187                }
188                _ => {}
189            }
190        }
191
192        let result = match method {
193            "initialize" => {
194                let requested = req
195                    .get("params")
196                    .and_then(|p| p.get("protocolVersion"))
197                    .and_then(|v| v.as_str());
198                Ok(self.server.initialize_response(requested))
199            }
200            "ping" => Ok(serde_json::json!({})),
201            "tools/list" => Ok(serde_json::json!({
202                "tools": self.server.tools()
203            })),
204            "resources/list" => Ok(serde_json::json!({
205                "resources": self.server.resources()
206            })),
207            "resources/templates/list" => Ok(serde_json::json!({
208                "resourceTemplates": []
209            })),
210            "prompts/list" => Ok(serde_json::json!({
211                "prompts": self.server.prompts()
212            })),
213            "prompts/get" => self.handle_prompt_get(req),
214            "tools/call" => return Some(self.handle_tool_call(&id, req, modern)),
215            "resources/read" => self.handle_resource_read(req),
216            _ => Err((-32601, format!("Method not found: {method}"))),
217        };
218
219        match result {
220            Ok(mut value) => {
221                if let Some(version) = modern {
222                    shape_modern_result(version, method, &mut value);
223                }
224                Some(self.success_response(id, value))
225            }
226            Err((code, message)) => Some(self.error_response(id, code, &message)),
227        }
228    }
229
230    /// Version gate for modern requests: a version outside
231    /// [`SUPPORTED_PROTOCOL_VERSIONS`] is rejected with `-32022`
232    /// (UnsupportedProtocolVersion) listing what this server does support,
233    /// so the client can retry with a mutually supported version.
234    fn check_modern_version(&self, id: &serde_json::Value, requested: &str) -> Option<String> {
235        if SUPPORTED_PROTOCOL_VERSIONS.contains(&requested) {
236            return None;
237        }
238        let mut error = self.error_response(id.clone(), -32022, "Unsupported protocol version");
239        // Splice a `data` member into the serialized error response.
240        if let Ok(mut v) = serde_json::from_str::<serde_json::Value>(&error) {
241            v["error"]["data"] = serde_json::json!({
242                "supported": SUPPORTED_PROTOCOL_VERSIONS,
243                "requested": requested,
244            });
245            error = serde_json::to_string(&v).unwrap_or(error);
246        }
247        Some(error)
248    }
249
250    /// Answer `subscriptions/listen` on stdio: the acknowledgment
251    /// notification and the graceful-closure result, one JSON-RPC message per
252    /// line (the stdio framing is newline-delimited).
253    fn subscription_close(&self, id: &serde_json::Value) -> String {
254        let (ack, close) = self.server.subscription_ack_and_close(id);
255        format!(
256            "{}\n{}",
257            serde_json::to_string(&ack).unwrap_or_default(),
258            self.success_response(id.clone(), close)
259        )
260    }
261
262    /// Dispatch a JSON-RPC request, awaiting async tool handlers.
263    ///
264    /// Identical to [`JsonRpcDispatcher::dispatch`] except that `tools/call`
265    /// resolves through [`McpServer::call_tool_async`], so tools registered
266    /// with `set_async_handler` work. The synchronous [`Self::dispatch`]
267    /// cannot invoke them — prefer this one whenever the server has any
268    /// async handler.
269    pub async fn dispatch_async(&self, request: &str) -> Option<String> {
270        let trimmed = request.trim();
271        if trimmed.starts_with('[') {
272            let reqs: Vec<serde_json::Value> = match serde_json::from_str(trimmed) {
273                Ok(v) => v,
274                Err(e) => {
275                    return Some(self.error_response(
276                        serde_json::Value::Null,
277                        -32700,
278                        &format!("Parse error: {e}"),
279                    ));
280                }
281            };
282            let mut responses: Vec<String> = Vec::with_capacity(reqs.len());
283            for req in &reqs {
284                if let Some(r) = self.dispatch_single_async(req).await {
285                    responses.push(r);
286                }
287            }
288            if responses.is_empty() {
289                None
290            } else {
291                Some(format!("[{}]", responses.join(",")))
292            }
293        } else {
294            let req: serde_json::Value = match serde_json::from_str(trimmed) {
295                Ok(v) => v,
296                Err(e) => {
297                    return Some(self.error_response(
298                        serde_json::Value::Null,
299                        -32700,
300                        &format!("Parse error: {e}"),
301                    ));
302                }
303            };
304            self.dispatch_single_async(&req).await
305        }
306    }
307
308    /// [`Self::dispatch_authenticated`] with async tool handler support.
309    pub async fn dispatch_authenticated_async(
310        &self,
311        request: &str,
312        auth_header: Option<&str>,
313    ) -> Option<String> {
314        if !self.server.check_auth(auth_header.unwrap_or("")) {
315            let id = serde_json::from_str::<serde_json::Value>(request.trim())
316                .ok()
317                .and_then(|v| v.get("id").cloned())
318                .unwrap_or(serde_json::Value::Null);
319            return Some(self.error_response(id, -32001, "Unauthorized"));
320        }
321        self.dispatch_async(request).await
322    }
323
324    async fn dispatch_single_async(&self, req: &serde_json::Value) -> Option<String> {
325        // Only `tools/call` can reach an async handler; everything else is
326        // pure metadata and shares the synchronous path.
327        if req.get("method").and_then(|v| v.as_str()) == Some("tools/call") {
328            req.get("id")?;
329            let id = req.get("id").cloned().unwrap_or(serde_json::Value::Null);
330            let modern = request_protocol_version(req);
331            if let Some(version) = modern
332                && let Some(err) = self.check_modern_version(&id, version)
333            {
334                return Some(err);
335            }
336            return Some(self.handle_tool_call_async(&id, req, modern).await);
337        }
338        self.dispatch_single(req)
339    }
340
341    /// Lifecycle gate for connection-oriented transports: the MCP spec
342    /// requires clients to send no requests other than `initialize` before
343    /// the server has answered initialization.
344    ///
345    /// Returns the `-32002` error response to send, or `None` when the
346    /// request may proceed. Modern (stateless, `_meta`-carrying) requests
347    /// bypass the gate — 2026-07-28 has no handshake at all. Notifications
348    /// never trigger the gate, and a pre-initialization batch is likewise
349    /// passed through, as batches are a `2024-11-05` compatibility feature
350    /// this dispatcher accepts leniently.
351    fn check_initialized(&self, parsed: &serde_json::Value, initialized: bool) -> Option<String> {
352        if initialized || !parsed.is_object() {
353            return None;
354        }
355        if request_protocol_version(parsed).is_some() {
356            return None; // modern era: stateless, no handshake to await
357        }
358        if parsed.get("method").and_then(|v| v.as_str()) == Some("initialize") {
359            return None;
360        }
361        // Notifications (no id) get no response — nothing to reject.
362        parsed.get("id")?;
363        let id = parsed.get("id").cloned().unwrap_or(serde_json::Value::Null);
364        Some(self.error_response(
365            id,
366            ERR_NOT_INITIALIZED,
367            "Server not initialized: send an initialize request first",
368        ))
369    }
370
371    /// Run the stdio transport loop: read lines from stdin, dispatch, write to stdout.
372    ///
373    /// Enforces the MCP lifecycle: requests arriving before `initialize` are
374    /// rejected with `-32002`.
375    ///
376    /// Tools registered with `set_async_handler` are NOT callable from this
377    /// loop — use [`Self::run_stdio_async`] when the server has any.
378    ///
379    /// # Errors
380    ///
381    /// Fails immediately (`InvalidInput`) when the server has any async-only
382    /// tool: every call to it would otherwise return `isError` at runtime and
383    /// look like a handler bug.
384    pub fn run_stdio(&self) -> io::Result<()> {
385        if let Some(name) = self.server.async_only_tools().first() {
386            return Err(io::Error::new(
387                io::ErrorKind::InvalidInput,
388                format!(
389                    "tool '{name}' has only an async handler; use run_stdio_async instead of run_stdio"
390                ),
391            ));
392        }
393        let stdin = io::stdin();
394        let mut reader = io::BufReader::new(stdin.lock());
395        let mut stdout = io::stdout().lock();
396        let mut initialized = false;
397        let mut raw = Vec::new();
398
399        loop {
400            let n = read_limited_line(&mut reader, MAX_LINE_BYTES, &mut raw)?;
401            if n == 0 {
402                break; // EOF
403            }
404            let line = std::str::from_utf8(&raw).map_err(|_| {
405                io::Error::new(
406                    io::ErrorKind::InvalidData,
407                    "JSON-RPC line is not valid UTF-8",
408                )
409            })?;
410            let trimmed = line.trim();
411            if trimmed.is_empty() {
412                continue;
413            }
414            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(trimmed) {
415                if let Some(err) = self.check_initialized(&parsed, initialized) {
416                    writeln!(stdout, "{err}")?;
417                    stdout.flush()?;
418                    continue;
419                }
420                if parsed.get("method").and_then(|m| m.as_str()) == Some("initialize") {
421                    initialized = true;
422                }
423            }
424            if let Some(response) = self.dispatch(trimmed) {
425                writeln!(stdout, "{response}")?;
426                stdout.flush()?;
427            }
428        }
429        Ok(())
430    }
431
432    /// [`Self::run_stdio`] for servers with async tool handlers.
433    ///
434    /// Enforces the MCP lifecycle like [`Self::run_stdio`] does. Reads stdin
435    /// with blocking I/O between awaits — an MCP stdio server is a dedicated
436    /// process, so occupying the calling task is intended. Drive it from a
437    /// runtime's blocking-friendly context (e.g. a dedicated task).
438    pub async fn run_stdio_async(&self) -> io::Result<()> {
439        let stdin = io::stdin();
440        let mut reader = io::BufReader::new(stdin.lock());
441        let mut stdout = io::stdout().lock();
442        let mut initialized = false;
443        let mut raw = Vec::new();
444
445        loop {
446            let n = read_limited_line(&mut reader, MAX_LINE_BYTES, &mut raw)?;
447            if n == 0 {
448                break; // EOF
449            }
450            let line = std::str::from_utf8(&raw).map_err(|_| {
451                io::Error::new(
452                    io::ErrorKind::InvalidData,
453                    "JSON-RPC line is not valid UTF-8",
454                )
455            })?;
456            let trimmed = line.trim();
457            if trimmed.is_empty() {
458                continue;
459            }
460            if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(trimmed) {
461                if let Some(err) = self.check_initialized(&parsed, initialized) {
462                    writeln!(stdout, "{err}")?;
463                    stdout.flush()?;
464                    continue;
465                }
466                if parsed.get("method").and_then(|m| m.as_str()) == Some("initialize") {
467                    initialized = true;
468                }
469            }
470            if let Some(response) = self.dispatch_async(trimmed).await {
471                writeln!(stdout, "{response}")?;
472                stdout.flush()?;
473            }
474        }
475        Ok(())
476    }
477
478    /// [`Self::handle_tool_call`] resolving through `call_tool_async`.
479    async fn handle_tool_call_async(
480        &self,
481        id: &serde_json::Value,
482        req: &serde_json::Value,
483        modern: Option<&str>,
484    ) -> String {
485        let tool_name = req
486            .get("params")
487            .and_then(|p| p.get("name"))
488            .and_then(|n| n.as_str())
489            .unwrap_or("");
490        let params = req
491            .get("params")
492            .and_then(|p| p.get("arguments"))
493            .cloned()
494            .unwrap_or(serde_json::json!(null));
495
496        if !self.server.has_tool(tool_name) {
497            return self.error_response(id.clone(), -32602, &format!("Unknown tool: {tool_name}"));
498        }
499        if let Err(e) = self.server.validate_tool_args(tool_name, &params) {
500            return self.error_response(id.clone(), -32602, &e);
501        }
502
503        let mut result = match self.server.call_tool_async(tool_name, params).await {
504            Ok(result) => serde_json::json!({
505                "content": [{ "type": "text", "text": result.to_string() }],
506                "isError": false
507            }),
508            Err(e) => serde_json::json!({
509                "content": [{ "type": "text", "text": e.to_string() }],
510                "isError": true
511            }),
512        };
513        if let Some(version) = modern {
514            shape_modern_result(version, "tools/call", &mut result);
515        }
516        self.success_response(id.clone(), result)
517    }
518
519    /// Handle `tools/call`. Returns a full JSON-RPC response string.
520    ///
521    /// An **unknown tool** is a protocol error (`-32602`, invalid params). A
522    /// tool that runs and **fails** is reported in-band as a successful result
523    /// with `isError: true`, per the MCP spec — so the model sees the error and
524    /// can adapt rather than the whole request failing at the transport layer.
525    fn handle_tool_call(
526        &self,
527        id: &serde_json::Value,
528        req: &serde_json::Value,
529        modern: Option<&str>,
530    ) -> String {
531        let tool_name = req
532            .get("params")
533            .and_then(|p| p.get("name"))
534            .and_then(|n| n.as_str())
535            .unwrap_or("");
536
537        let params = req
538            .get("params")
539            .and_then(|p| p.get("arguments"))
540            .cloned()
541            .unwrap_or(serde_json::json!(null));
542
543        if !self.server.has_tool(tool_name) {
544            return self.error_response(id.clone(), -32602, &format!("Unknown tool: {tool_name}"));
545        }
546        if let Err(e) = self.server.validate_tool_args(tool_name, &params) {
547            return self.error_response(id.clone(), -32602, &e);
548        }
549
550        let mut result = match self.server.call_tool(tool_name, params) {
551            Ok(result) => serde_json::json!({
552                "content": [{ "type": "text", "text": result.to_string() }],
553                "isError": false
554            }),
555            Err(e) => serde_json::json!({
556                "content": [{ "type": "text", "text": e.to_string() }],
557                "isError": true
558            }),
559        };
560        if let Some(version) = modern {
561            shape_modern_result(version, "tools/call", &mut result);
562        }
563        self.success_response(id.clone(), result)
564    }
565
566    /// Handle `prompts/get`: render a registered prompt with the given
567    /// arguments. An unknown prompt is an invalid-params error.
568    fn handle_prompt_get(
569        &self,
570        req: &serde_json::Value,
571    ) -> std::result::Result<serde_json::Value, (i32, String)> {
572        let name = req
573            .get("params")
574            .and_then(|p| p.get("name"))
575            .and_then(|n| n.as_str())
576            .unwrap_or("");
577        let args = req
578            .get("params")
579            .and_then(|p| p.get("arguments"))
580            .cloned()
581            .unwrap_or(serde_json::json!({}));
582        self.server
583            .get_prompt(name, args)
584            .map_err(|e| (-32602, e.to_string()))
585    }
586
587    fn handle_resource_read(
588        &self,
589        req: &serde_json::Value,
590    ) -> std::result::Result<serde_json::Value, (i32, String)> {
591        let uri = req
592            .get("params")
593            .and_then(|p| p.get("uri"))
594            .and_then(|u| u.as_str())
595            .unwrap_or("");
596        self.server
597            .read_resource(uri, serde_json::json!({}))
598            .map(|content| {
599                serde_json::json!({
600                    "contents": [{
601                        "uri": uri,
602                        "text": content.to_string()
603                    }]
604                })
605            })
606            .map_err(|e| (-32602, e.to_string()))
607    }
608
609    fn success_response(&self, id: serde_json::Value, result: serde_json::Value) -> String {
610        serde_json::to_string(&serde_json::json!({
611            "jsonrpc": "2.0",
612            "id": id,
613            "result": result,
614        }))
615        .unwrap_or_default()
616    }
617
618    fn error_response(&self, id: serde_json::Value, code: i32, message: &str) -> String {
619        serde_json::to_string(&serde_json::json!({
620            "jsonrpc": "2.0",
621            "id": id,
622            "error": {
623                "code": code,
624                "message": message,
625            }
626        }))
627        .unwrap_or_default()
628    }
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634    use crate::mcp::schema::ToolDescription;
635
636    /// A server whose tool is registered ONLY via `set_async_handler` — the
637    /// shape real consumers use when their handlers await I/O.
638    fn async_only_server() -> McpServer {
639        let mut server = McpServer::new("async-server", "0.1.0");
640        server.register_tool(ToolDescription {
641            name: "search".into(),
642            description: "async search".into(),
643            input_schema: serde_json::json!({"type": "object"}),
644        });
645        server.set_async_handler("search", |p: serde_json::Value| async move { Ok(p) });
646        server
647    }
648
649    #[tokio::test]
650    async fn async_only_tool_runs_over_dispatch_async() {
651        let server = async_only_server();
652        let dispatcher = JsonRpcDispatcher::new(&server);
653        let req = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search","arguments":{"q":"x"}}}"#;
654        let resp = dispatcher.dispatch_async(req).await.expect("response");
655        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
656        assert_eq!(parsed["result"]["isError"], false, "{parsed}");
657        assert!(
658            parsed["result"]["content"][0]["text"]
659                .as_str()
660                .unwrap()
661                .contains("\"q\""),
662            "{parsed}"
663        );
664    }
665
666    #[tokio::test]
667    async fn dispatch_async_still_serves_metadata_methods() {
668        let server = async_only_server();
669        let dispatcher = JsonRpcDispatcher::new(&server);
670        let resp = dispatcher
671            .dispatch_async(r#"{"jsonrpc":"2.0","id":7,"method":"tools/list"}"#)
672            .await
673            .expect("response");
674        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
675        assert_eq!(parsed["id"], 7);
676        assert_eq!(parsed["result"]["tools"][0]["name"], "search");
677    }
678
679    #[tokio::test]
680    async fn missing_required_argument_is_rejected_not_defaulted() {
681        let mut server = McpServer::new("s", "1.0");
682        server.register_tool(ToolDescription {
683            name: "search".into(),
684            description: "d".into(),
685            input_schema: serde_json::json!({
686                "type": "object",
687                "properties": {"query": {"type": "string"}},
688                "required": ["query"]
689            }),
690        });
691        server.set_async_handler("search", |p: serde_json::Value| async move { Ok(p) });
692        let dispatcher = JsonRpcDispatcher::new(&server);
693
694        // No arguments at all — the handler would see null and search for "".
695        let resp = dispatcher
696            .dispatch_async(
697                r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search"}}"#,
698            )
699            .await
700            .expect("response");
701        assert!(resp.contains("-32602"), "{resp}");
702        assert!(resp.contains("query"), "{resp}");
703
704        // Supplying it dispatches normally.
705        let ok = dispatcher
706            .dispatch_async(
707                r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search","arguments":{"query":"x"}}}"#,
708            )
709            .await
710            .expect("response");
711        let parsed: serde_json::Value = serde_json::from_str(&ok).unwrap();
712        assert_eq!(parsed["result"]["isError"], false, "{ok}");
713    }
714
715    #[test]
716    fn non_object_request_gets_invalid_request_not_silence() {
717        let server = async_only_server();
718        let dispatcher = JsonRpcDispatcher::new(&server);
719        let resp = dispatcher
720            .dispatch("[1, 2]")
721            .expect("must answer, not hang");
722        assert!(resp.contains("-32600"), "{resp}");
723    }
724
725    #[test]
726    fn sync_call_of_async_only_tool_says_so() {
727        let server = async_only_server();
728        let err = server
729            .call_tool("search", serde_json::json!({}))
730            .unwrap_err()
731            .to_string();
732        assert!(err.contains("async"), "misleading error: {err}");
733    }
734
735    fn test_server() -> McpServer {
736        let mut server = McpServer::new("test-server", "0.1.0");
737        server.register_tool(ToolDescription {
738            name: "echo".into(),
739            description: "Echo input".into(),
740            input_schema: serde_json::json!({"type": "object"}),
741        });
742        server.set_handler("echo", Ok);
743        server
744    }
745
746    #[test]
747    fn dispatch_initialize() {
748        let server = test_server();
749        let dispatcher = JsonRpcDispatcher::new(&server);
750        let resp = dispatcher
751            .dispatch(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#)
752            .unwrap();
753        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
754        assert_eq!(parsed["result"]["serverInfo"]["name"], "test-server");
755    }
756
757    #[test]
758    fn dispatch_tools_list() {
759        let server = test_server();
760        let dispatcher = JsonRpcDispatcher::new(&server);
761        let resp = dispatcher
762            .dispatch(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#)
763            .unwrap();
764        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
765        assert_eq!(parsed["result"]["tools"].as_array().unwrap().len(), 1);
766    }
767
768    #[test]
769    fn dispatch_tools_call() {
770        let server = test_server();
771        let dispatcher = JsonRpcDispatcher::new(&server);
772        let req = r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hello"}}}"#;
773        let resp = dispatcher.dispatch(req).unwrap();
774        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
775        let text = parsed["result"]["content"][0]["text"].as_str().unwrap();
776        assert!(text.contains("hello"));
777    }
778
779    #[test]
780    fn dispatch_unknown_method() {
781        let server = test_server();
782        let dispatcher = JsonRpcDispatcher::new(&server);
783        let resp = dispatcher
784            .dispatch(r#"{"jsonrpc":"2.0","id":4,"method":"nonexistent"}"#)
785            .unwrap();
786        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
787        assert_eq!(parsed["error"]["code"], -32601);
788    }
789
790    #[test]
791    fn dispatch_invalid_json() {
792        let server = test_server();
793        let dispatcher = JsonRpcDispatcher::new(&server);
794        let resp = dispatcher.dispatch("not json").unwrap();
795        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
796        assert_eq!(parsed["error"]["code"], -32700);
797    }
798
799    #[test]
800    fn dispatch_unknown_tool_is_invalid_params() {
801        // An unknown tool is a protocol error (-32602), not an in-band failure.
802        let server = test_server();
803        let dispatcher = JsonRpcDispatcher::new(&server);
804        let req = r#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"missing","arguments":{}}}"#;
805        let resp = dispatcher.dispatch(req).unwrap();
806        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
807        assert_eq!(parsed["error"]["code"], -32602);
808    }
809
810    #[test]
811    fn dispatch_ping_returns_empty_result() {
812        let server = test_server();
813        let dispatcher = JsonRpcDispatcher::new(&server);
814        let resp = dispatcher
815            .dispatch(r#"{"jsonrpc":"2.0","id":9,"method":"ping"}"#)
816            .unwrap();
817        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
818        assert_eq!(parsed["id"], 9);
819        assert!(parsed["result"].is_object());
820        assert_eq!(parsed["result"].as_object().unwrap().len(), 0);
821    }
822
823    #[test]
824    fn dispatch_preserves_string_id() {
825        let server = test_server();
826        let dispatcher = JsonRpcDispatcher::new(&server);
827        let resp = dispatcher
828            .dispatch(r#"{"jsonrpc":"2.0","id":"req-abc","method":"tools/list"}"#)
829            .unwrap();
830        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
831        assert_eq!(parsed["id"], "req-abc");
832    }
833
834    #[test]
835    fn initialize_echoes_client_protocol_version() {
836        let server = test_server();
837        let dispatcher = JsonRpcDispatcher::new(&server);
838        let resp = dispatcher
839            .dispatch(
840                r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}"#,
841            )
842            .unwrap();
843        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
844        assert_eq!(parsed["result"]["protocolVersion"], "2024-11-05");
845    }
846
847    #[test]
848    fn tool_execution_error_reported_in_band() {
849        // A registered tool whose handler fails → result with isError: true,
850        // NOT a JSON-RPC error object.
851        let mut server = McpServer::new("t", "1.0");
852        server.register_tool(ToolDescription {
853            name: "boom".into(),
854            description: "always fails".into(),
855            input_schema: serde_json::json!({"type": "object"}),
856        });
857        server.set_handler("boom", |_| {
858            Err(crate::error::KernelError::Config("kaboom".into()))
859        });
860        let dispatcher = JsonRpcDispatcher::new(&server);
861        let req = r#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"boom","arguments":{}}}"#;
862        let resp = dispatcher.dispatch(req).unwrap();
863        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
864        assert!(
865            parsed.get("error").is_none(),
866            "should not be a protocol error"
867        );
868        assert_eq!(parsed["result"]["isError"], true);
869        assert!(
870            parsed["result"]["content"][0]["text"]
871                .as_str()
872                .unwrap()
873                .contains("kaboom")
874        );
875    }
876
877    #[test]
878    fn dispatch_prompts_list_and_get() {
879        let mut server = McpServer::new("t", "1.0");
880        server.register_prompt(crate::mcp::schema::PromptDescription {
881            name: "greet".into(),
882            description: Some("Greet".into()),
883            arguments: Vec::new(),
884        });
885        server.set_prompt_handler("greet", |_| {
886            Ok(serde_json::json!({
887                "messages": [{ "role": "user", "content": { "type": "text", "text": "hi" } }]
888            }))
889        });
890        let dispatcher = JsonRpcDispatcher::new(&server);
891
892        let list = dispatcher
893            .dispatch(r#"{"jsonrpc":"2.0","id":1,"method":"prompts/list"}"#)
894            .unwrap();
895        let list: serde_json::Value = serde_json::from_str(&list).unwrap();
896        assert_eq!(list["result"]["prompts"][0]["name"], "greet");
897
898        let got = dispatcher
899            .dispatch(r#"{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"greet","arguments":{}}}"#)
900            .unwrap();
901        let got: serde_json::Value = serde_json::from_str(&got).unwrap();
902        assert_eq!(got["result"]["messages"][0]["content"]["text"], "hi");
903    }
904
905    #[test]
906    fn dispatch_resource_templates_list_is_empty() {
907        let server = test_server();
908        let dispatcher = JsonRpcDispatcher::new(&server);
909        let resp = dispatcher
910            .dispatch(r#"{"jsonrpc":"2.0","id":1,"method":"resources/templates/list"}"#)
911            .unwrap();
912        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
913        assert!(parsed["result"]["resourceTemplates"].is_array());
914    }
915
916    #[test]
917    fn notification_without_id_gets_no_response() {
918        let server = test_server();
919        let dispatcher = JsonRpcDispatcher::new(&server);
920        // `notifications/initialized` is a notification (no id) → no response.
921        assert!(
922            dispatcher
923                .dispatch(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#)
924                .is_none()
925        );
926    }
927
928    #[test]
929    fn dispatch_batch_request() {
930        let server = test_server();
931        let dispatcher = JsonRpcDispatcher::new(&server);
932        let batch = r#"[
933            {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}},
934            {"jsonrpc":"2.0","id":2,"method":"tools/list"}
935        ]"#;
936        let resp = dispatcher.dispatch(batch).unwrap();
937        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
938        let arr = parsed.as_array().unwrap();
939        assert_eq!(arr.len(), 2);
940        // First response: initialize
941        assert_eq!(arr[0]["result"]["serverInfo"]["name"], "test-server");
942        // Second response: tools/list
943        assert_eq!(arr[1]["result"]["tools"].as_array().unwrap().len(), 1);
944    }
945
946    #[test]
947    fn dispatch_batch_with_error() {
948        let server = test_server();
949        let dispatcher = JsonRpcDispatcher::new(&server);
950        let batch = r#"[
951            {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}},
952            {"jsonrpc":"2.0","id":2,"method":"nonexistent"}
953        ]"#;
954        let resp = dispatcher.dispatch(batch).unwrap();
955        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
956        let arr = parsed.as_array().unwrap();
957        assert_eq!(arr[0]["result"]["serverInfo"]["name"], "test-server");
958        assert_eq!(arr[1]["error"]["code"], -32601);
959    }
960
961    #[test]
962    fn dispatch_authenticated_passes_with_no_auth_configured() {
963        let server = test_server();
964        let dispatcher = JsonRpcDispatcher::new(&server);
965        let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
966        let resp = dispatcher.dispatch_authenticated(req, None).unwrap();
967        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
968        assert!(parsed["result"]["serverInfo"].is_object());
969    }
970
971    #[test]
972    fn dispatch_authenticated_rejects_missing_token() {
973        let server = McpServer::new("secured", "1.0").with_bearer_auth("secret-token");
974        let dispatcher = JsonRpcDispatcher::new(&server);
975        let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
976        let resp = dispatcher.dispatch_authenticated(req, None).unwrap();
977        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
978        assert_eq!(parsed["error"]["code"], -32001);
979    }
980
981    #[test]
982    fn dispatch_authenticated_rejects_wrong_token() {
983        let server = McpServer::new("secured", "1.0").with_bearer_auth("correct-token");
984        let dispatcher = JsonRpcDispatcher::new(&server);
985        let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
986        let resp = dispatcher
987            .dispatch_authenticated(req, Some("Bearer wrong-token"))
988            .unwrap();
989        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
990        assert_eq!(parsed["error"]["code"], -32001);
991    }
992
993    #[test]
994    fn dispatch_authenticated_passes_with_correct_token() {
995        let server = McpServer::new("secured", "1.0").with_bearer_auth("correct-token");
996        let dispatcher = JsonRpcDispatcher::new(&server);
997        let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
998        let resp = dispatcher
999            .dispatch_authenticated(req, Some("Bearer correct-token"))
1000            .unwrap();
1001        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
1002        assert!(parsed["result"]["serverInfo"].is_object());
1003    }
1004
1005    #[test]
1006    fn read_limited_line_bounds_memory_before_newline() {
1007        // A 40-byte "line" with no newline against a 10-byte cap must fail —
1008        // the buffer never grows past the cap waiting for `\n`.
1009        let mut reader = io::Cursor::new(vec![b'x'; 40]);
1010        let mut buf = Vec::new();
1011        let err = read_limited_line(&mut reader, 10, &mut buf).unwrap_err();
1012        assert_eq!(err.kind(), io::ErrorKind::InvalidData, "{err}");
1013        assert!(buf.len() <= 10, "buffer grew past the cap: {}", buf.len());
1014    }
1015
1016    #[test]
1017    fn read_limited_line_reads_normal_lines() {
1018        let mut reader = io::Cursor::new(b"{\"a\":1}\n{\"b\":2}\ntrailing".to_vec());
1019        let mut buf = Vec::new();
1020        assert_eq!(read_limited_line(&mut reader, 1024, &mut buf).unwrap(), 8);
1021        assert_eq!(buf, b"{\"a\":1}\n");
1022        assert_eq!(read_limited_line(&mut reader, 1024, &mut buf).unwrap(), 8);
1023        assert_eq!(buf, b"{\"b\":2}\n");
1024        // Final unterminated line, then EOF (0).
1025        assert_eq!(read_limited_line(&mut reader, 1024, &mut buf).unwrap(), 8);
1026        assert_eq!(buf, b"trailing");
1027        assert_eq!(read_limited_line(&mut reader, 1024, &mut buf).unwrap(), 0);
1028    }
1029
1030    #[test]
1031    fn pre_initialize_requests_are_rejected() {
1032        let server = test_server();
1033        let dispatcher = JsonRpcDispatcher::new(&server);
1034        let parsed: serde_json::Value =
1035            serde_json::from_str(r#"{"jsonrpc":"2.0","id":5,"method":"tools/list"}"#).unwrap();
1036        let err = dispatcher
1037            .check_initialized(&parsed, false)
1038            .expect("gate must reject");
1039        assert!(err.contains("-32002"), "{err}");
1040
1041        // initialize itself always passes …
1042        let init: serde_json::Value =
1043            serde_json::from_str(r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#).unwrap();
1044        assert!(dispatcher.check_initialized(&init, false).is_none());
1045        // … and everything passes once initialized.
1046        assert!(dispatcher.check_initialized(&parsed, true).is_none());
1047
1048        // Notifications (no id) are never answered, so the gate ignores them.
1049        let notif: serde_json::Value =
1050            serde_json::from_str(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#)
1051                .unwrap();
1052        assert!(dispatcher.check_initialized(&notif, false).is_none());
1053
1054        // Modern (stateless) requests bypass the gate: 2026-07-28 has no
1055        // handshake to await.
1056        let modern: serde_json::Value = serde_json::from_str(
1057            r#"{"jsonrpc":"2.0","id":9,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}"#,
1058        )
1059        .unwrap();
1060        assert!(dispatcher.check_initialized(&modern, false).is_none());
1061    }
1062
1063    /// A modern (stateless, `_meta`-carrying) request, one per test constant.
1064    fn modern_request(method: &str, id: i64, version: &str) -> String {
1065        format!(
1066            r#"{{"jsonrpc":"2.0","id":{id},"method":"{method}","params":{{"_meta":{{"io.modelcontextprotocol/protocolVersion":"{version}"}}}}}}"#
1067        )
1068    }
1069
1070    #[test]
1071    fn modern_server_discover_answers_versions_and_identity() {
1072        let server = test_server();
1073        let dispatcher = JsonRpcDispatcher::new(&server);
1074        let resp = dispatcher
1075            .dispatch(&modern_request("server/discover", 1, "2026-07-28"))
1076            .unwrap();
1077        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
1078        assert_eq!(
1079            parsed["result"]["supportedVersions"][0],
1080            crate::mcp::server::LATEST_PROTOCOL_VERSION
1081        );
1082        assert_eq!(
1083            parsed["result"]["_meta"]["io.modelcontextprotocol/serverInfo"]["name"],
1084            "test-server"
1085        );
1086        // CacheableResult: discover responses carry ttlMs + cacheScope.
1087        assert!(parsed["result"]["ttlMs"].is_u64(), "{parsed}");
1088        assert_eq!(parsed["result"]["resultType"], "complete");
1089    }
1090
1091    #[test]
1092    fn modern_unsupported_version_lists_supported() {
1093        let server = test_server();
1094        let dispatcher = JsonRpcDispatcher::new(&server);
1095        let resp = dispatcher
1096            .dispatch(&modern_request("tools/list", 2, "1999-01-01"))
1097            .unwrap();
1098        let parsed: serde_json::Value = serde_json::from_str(&resp).unwrap();
1099        assert_eq!(parsed["error"]["code"], -32022, "{parsed}");
1100        assert_eq!(parsed["error"]["data"]["requested"], "1999-01-01");
1101        assert!(
1102            parsed["error"]["data"]["supported"]
1103                .as_array()
1104                .unwrap()
1105                .contains(&serde_json::json!("2025-06-18"))
1106        );
1107    }
1108
1109    #[test]
1110    fn modern_ping_is_method_not_found() {
1111        let server = test_server();
1112        let dispatcher = JsonRpcDispatcher::new(&server);
1113        let resp = dispatcher
1114            .dispatch(&modern_request("ping", 3, "2026-07-28"))
1115            .unwrap();
1116        assert!(resp.contains("-32601"), "{resp}");
1117        // Legacy ping still works.
1118        let resp = dispatcher
1119            .dispatch(r#"{"jsonrpc":"2.0","id":4,"method":"ping"}"#)
1120            .unwrap();
1121        assert!(resp.contains("result"), "{resp}");
1122    }
1123
1124    #[test]
1125    fn modern_results_carry_result_type_and_cache_fields() {
1126        let mut server = test_server();
1127        server.set_handler("echo", Ok);
1128        let dispatcher = JsonRpcDispatcher::new(&server);
1129
1130        let list = dispatcher
1131            .dispatch(&modern_request("tools/list", 1, "2026-07-28"))
1132            .unwrap();
1133        let parsed: serde_json::Value = serde_json::from_str(&list).unwrap();
1134        assert_eq!(parsed["result"]["resultType"], "complete");
1135        assert!(parsed["result"]["ttlMs"].is_u64());
1136        assert_eq!(parsed["result"]["cacheScope"], "private");
1137
1138        let call = format!(
1139            r#"{{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{{"name":"echo","arguments":{{}},"_meta":{{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}}}}"#
1140        );
1141        let call = dispatcher.dispatch(&call).unwrap();
1142        let parsed: serde_json::Value = serde_json::from_str(&call).unwrap();
1143        assert_eq!(parsed["result"]["resultType"], "complete", "{parsed}");
1144        assert!(
1145            parsed["result"].get("ttlMs").is_none(),
1146            "tools/call is not cacheable"
1147        );
1148
1149        // A modern request naming a LEGACY version is served without modern
1150        // result shaping (the client speaks that revision's schema).
1151        let legacy_shaped = dispatcher
1152            .dispatch(&modern_request("tools/list", 3, "2025-06-18"))
1153            .unwrap();
1154        let parsed: serde_json::Value = serde_json::from_str(&legacy_shaped).unwrap();
1155        assert!(parsed["result"].get("resultType").is_none(), "{parsed}");
1156    }
1157
1158    #[test]
1159    fn modern_subscriptions_listen_acks_then_closes() {
1160        let server = test_server();
1161        let dispatcher = JsonRpcDispatcher::new(&server);
1162        let resp = dispatcher
1163            .dispatch(&modern_request("subscriptions/listen", 7, "2026-07-28"))
1164            .unwrap();
1165        // Two newline-delimited messages: the acknowledgment notification,
1166        // then the graceful-closure result response.
1167        let lines: Vec<&str> = resp.lines().collect();
1168        assert_eq!(lines.len(), 2, "{resp}");
1169        let ack: serde_json::Value = serde_json::from_str(lines[0]).unwrap();
1170        assert_eq!(ack["method"], "notifications/subscriptions/acknowledged");
1171        // The agreed subset is empty: nothing was requested that we support.
1172        assert_eq!(ack["params"]["notifications"], serde_json::json!({}));
1173        assert_eq!(
1174            ack["params"]["_meta"]["io.modelcontextprotocol/subscriptionId"],
1175            7
1176        );
1177        let close: serde_json::Value = serde_json::from_str(lines[1]).unwrap();
1178        assert_eq!(close["id"], 7);
1179        assert_eq!(close["result"]["resultType"], "complete");
1180        assert_eq!(
1181            close["result"]["_meta"]["io.modelcontextprotocol/subscriptionId"],
1182            7
1183        );
1184    }
1185
1186    #[test]
1187    fn modern_request_inside_batch_rejects_the_batch() {
1188        let server = test_server();
1189        let dispatcher = JsonRpcDispatcher::new(&server);
1190        let batch = format!("[{}]", modern_request("tools/list", 1, "2026-07-28"));
1191        let resp = dispatcher.dispatch(&batch).unwrap();
1192        assert!(resp.contains("-32600"), "{resp}");
1193    }
1194}