Skip to main content

llm_tool_mcp/server/
dispatch.rs

1//! JSON-RPC request dispatch and the [`RpcOutcome`] wire type.
2
3use std::fmt;
4
5use llm_tool::{ToolContext, ToolRegistry};
6use tracing::{debug, info};
7
8use super::{Connection, McpServer};
9use crate::protocol::{
10    self, Capabilities, ContentItem, InitializeResult, JSONRPC_VERSION, JsonRpcRequest,
11    JsonRpcResponse, PromptCapabilities, ResourceCapabilities, ServerInfo, ToolCallParams,
12    ToolCallResult, ToolCapabilities,
13};
14
15impl McpServer {
16    /// Handle exactly one JSON-RPC request string, always producing a response.
17    ///
18    /// A **test-only** convenience over
19    /// [`handle_request_conn`](Self::handle_request_conn) that uses the shared
20    /// server identity (no per-connection caller). Production transports drive
21    /// `handle_request_conn` so each connection keeps its own negotiated
22    /// identity. Public callers should use [`handle_message`](Self::handle_message),
23    /// which additionally understands batches and notification-only input.
24    ///
25    /// Safe to call from within an existing tokio runtime.
26    #[cfg(test)]
27    pub(crate) async fn handle_request(&self, line: &str) -> JsonRpcResponse {
28        self.handle_request_conn(line, &mut Connection::default())
29            .await
30    }
31
32    /// Connection-aware variant of [`handle_request`](Self::handle_request).
33    ///
34    /// `conn` carries the caller identity and per-caller registry view
35    /// negotiated for this connection's `initialize` handshake; both are threaded
36    /// into dispatch so per-connection identity and per-caller tool sets (when
37    /// enabled) apply to `tools/list` and `tools/call`.
38    async fn handle_request_conn(&self, line: &str, conn: &mut Connection) -> JsonRpcResponse {
39        // Detect batch requests (JSON arrays) — redirect to handle_message.
40        if let Some(first_non_ws) = line.trim_start().as_bytes().first() {
41            if *first_non_ws == b'[' {
42                return JsonRpcResponse::error(
43                    None,
44                    protocol::INVALID_REQUEST,
45                    "batch requests must be processed via handle_message or run/run_async",
46                );
47            }
48        }
49
50        let val: serde_json::Value = match serde_json::from_str(line) {
51            Ok(r) => r,
52            Err(e) => {
53                return JsonRpcResponse::error(
54                    None,
55                    protocol::PARSE_ERROR,
56                    format!("invalid JSON: {e}"),
57                );
58            }
59        };
60
61        let Some(obj) = val.as_object() else {
62            return JsonRpcResponse::error(
63                None,
64                protocol::INVALID_REQUEST,
65                "expected JSON-RPC request object",
66            );
67        };
68
69        let id = obj.get("id").cloned();
70        let request: JsonRpcRequest = match serde_json::from_value(val) {
71            Ok(r) => r,
72            Err(e) => {
73                return JsonRpcResponse::error(
74                    id,
75                    protocol::INVALID_REQUEST,
76                    format!("invalid JSON-RPC request: {e}"),
77                );
78            }
79        };
80
81        // JSON-RPC 2.0 §4: the "jsonrpc" field MUST be exactly "2.0".
82        if request.version != JSONRPC_VERSION {
83            return JsonRpcResponse::error(
84                request.id,
85                protocol::INVALID_REQUEST,
86                format!(
87                    "invalid jsonrpc version: expected \"2.0\", got \"{}\"",
88                    request.version
89                ),
90            );
91        }
92
93        self.dispatch_method(request, conn).await
94    }
95
96    /// Handle one JSON-RPC *message* and return the response to send back.
97    ///
98    /// This is **the** entry point for building a custom transport (Axum HTTP,
99    /// `WebSockets`, a message queue, etc.). The [`run`](Self::run) family is
100    /// built on top of it. A JSON-RPC message is either:
101    ///
102    /// - a single request/notification object (`{ ... }`), or
103    /// - a batch array of them (`[ { ... }, ... ]`).
104    ///
105    /// Each request's `method` is dispatched against this server's
106    /// [`ToolRegistry`] (`tools/list`, `tools/call`) and any registered prompts
107    /// (`prompts/*`) and resources (`resources/*`); see the crate docs for the
108    /// full method table.
109    ///
110    /// The result is a structured [`RpcOutcome`] you inspect or render to the
111    /// wire in a single pass via [`to_wire`](RpcOutcome::to_wire) /
112    /// [`Display`](core::fmt::Display) / [`write_json`](RpcOutcome::write_json):
113    ///
114    /// - <code>Some([Single](RpcOutcome::Single))</code> — one request → one response.
115    /// - <code>Some([Batch](RpcOutcome::Batch))</code> — a batch → one response each,
116    ///   for the non-notification members.
117    /// - `None` — the input was purely notification(s); send nothing back
118    ///   (e.g. reply `202 Accepted` with no body over HTTP).
119    pub async fn handle_message(&self, line: &str) -> Option<RpcOutcome> {
120        self.handle_message_conn(line, &mut Connection::default())
121            .await
122    }
123
124    /// Connection-aware variant of [`handle_message`](Self::handle_message).
125    ///
126    /// `conn` is this connection's negotiated state: its caller identity slot
127    /// and per-caller registry view are updated by the `initialize` handshake
128    /// (when per-connection identity / a registry factory are enabled) and read
129    /// on subsequent `tools/list` and `tools/call`s. The transport run loops own
130    /// one such [`Connection`] per connection.
131    pub async fn handle_message_conn(
132        &self,
133        line: &str,
134        conn: &mut Connection,
135    ) -> Option<RpcOutcome> {
136        let trimmed = line.trim();
137        if trimmed.is_empty() {
138            return None;
139        }
140
141        if trimmed.as_bytes().first() == Some(&b'[') {
142            return self.dispatch_batch(trimmed, conn).await;
143        }
144
145        let response = self.handle_request_conn(trimmed, conn).await;
146        // JSON-RPC 2.0 §4.1: notifications (requests without an ID that succeeded)
147        // must not produce a response. Protocol-level errors with null IDs must be sent.
148        if response.id.is_none() && response.error.is_none() {
149            None
150        } else {
151            Some(RpcOutcome::Single(response))
152        }
153    }
154
155    /// Handle a JSON-RPC 2.0 batch request array, collecting one response per
156    /// non-notification member.
157    async fn dispatch_batch(&self, line: &str, conn: &mut Connection) -> Option<RpcOutcome> {
158        let val: serde_json::Value = match serde_json::from_str(line) {
159            Ok(v) => v,
160            Err(e) => {
161                return Some(RpcOutcome::Single(JsonRpcResponse::error(
162                    None,
163                    protocol::PARSE_ERROR,
164                    format!("invalid JSON: {e}"),
165                )));
166            }
167        };
168
169        let Some(arr) = val.as_array() else {
170            return Some(RpcOutcome::Single(JsonRpcResponse::error(
171                None,
172                protocol::INVALID_REQUEST,
173                "expected JSON array for batch request",
174            )));
175        };
176
177        if arr.is_empty() {
178            return Some(RpcOutcome::Single(JsonRpcResponse::error(
179                None,
180                protocol::INVALID_REQUEST,
181                "batch request array cannot be empty",
182            )));
183        }
184
185        let mut responses = Vec::with_capacity(arr.len());
186        for item in arr {
187            let resp_opt = match serde_json::from_value::<JsonRpcRequest>(item.clone()) {
188                Ok(request) => {
189                    if request.version == JSONRPC_VERSION {
190                        let resp = self.dispatch_method(request, conn).await;
191                        if resp.id.is_none() { None } else { Some(resp) }
192                    } else {
193                        Some(JsonRpcResponse::error(
194                            request.id,
195                            protocol::INVALID_REQUEST,
196                            format!(
197                                "invalid jsonrpc version: expected \"2.0\", got \"{}\"",
198                                request.version
199                            ),
200                        ))
201                    }
202                }
203                Err(e) => {
204                    let id = item.as_object().and_then(|o| o.get("id").cloned());
205                    Some(JsonRpcResponse::error(
206                        id,
207                        protocol::INVALID_REQUEST,
208                        format!("invalid request object in batch: {e}"),
209                    ))
210                }
211            };
212
213            if let Some(resp) = resp_opt {
214                responses.push(resp);
215            }
216        }
217
218        if responses.is_empty() {
219            None
220        } else {
221            Some(RpcOutcome::Batch(responses))
222        }
223    }
224
225    /// Dispatch a validated JSON-RPC request to the appropriate method handler.
226    async fn dispatch_method(
227        &self,
228        request: JsonRpcRequest,
229        conn: &mut Connection,
230    ) -> JsonRpcResponse {
231        let id = request.id.clone();
232
233        match request.method.as_str() {
234            protocol::METHOD_INITIALIZE => {
235                self.handle_initialize(id, request.params.as_ref(), conn)
236            }
237            // No-op acknowledgements: liveness/log-level, plus the `initialized`
238            // notification some clients send (both namespaced and bare forms).
239            protocol::METHOD_PING
240            | protocol::METHOD_LOGGING_SET_LEVEL
241            | protocol::METHOD_NOTIFICATIONS_INITIALIZED
242            | protocol::METHOD_INITIALIZED => {
243                JsonRpcResponse::success(id, protocol::EmptyResult {})
244            }
245            // Cancellation notifications — acknowledge silently.
246            protocol::METHOD_NOTIFICATIONS_CANCELLED => {
247                debug!("received cancellation notification");
248                JsonRpcResponse::success(id, protocol::EmptyResult {})
249            }
250            protocol::METHOD_TOOLS_LIST => self.handle_tools_list(id, conn),
251            protocol::METHOD_TOOLS_CALL => {
252                let ctx = conn.ctx.as_ref().unwrap_or(&self.context);
253                let registry = conn
254                    .view
255                    .as_ref()
256                    .map_or_else(|| self.registry.as_ref(), |v| v.registry.as_ref());
257                self.handle_tools_call(id, request.params, ctx, registry)
258                    .await
259            }
260            protocol::METHOD_RESOURCES_LIST => {
261                let list = protocol::ResourcesListResult {
262                    resources: self
263                        .resources
264                        .definitions()
265                        .into_iter()
266                        .map(|def| protocol::Resource {
267                            uri: def.uri_template,
268                            name: def.name,
269                            description: def.description,
270                            mime_type: def.mime_type,
271                        })
272                        .collect(),
273                };
274                JsonRpcResponse::success(id, list)
275            }
276            protocol::METHOD_RESOURCES_TEMPLATES_LIST => {
277                let list = protocol::ResourceTemplatesListResult {
278                    resource_templates: self.resources.definitions(),
279                };
280                JsonRpcResponse::success(id, list)
281            }
282            protocol::METHOD_RESOURCES_READ => self.handle_resources_read(id, request.params).await,
283            protocol::METHOD_PROMPTS_LIST => {
284                let list = protocol::PromptsListResult {
285                    prompts: self.prompts.definitions(),
286                };
287                JsonRpcResponse::success(id, list)
288            }
289            protocol::METHOD_PROMPTS_GET => self.handle_prompts_get(id, request.params).await,
290            protocol::METHOD_COMPLETION_COMPLETE => {
291                JsonRpcResponse::success(id, protocol::CompletionCompleteResult::default())
292            }
293            protocol::METHOD_NOTIFICATIONS_PROGRESS | protocol::METHOD_NOTIFICATIONS_MESSAGE => {
294                debug!("received progress/message notification");
295                JsonRpcResponse::success(id, protocol::EmptyResult {})
296            }
297            other => JsonRpcResponse::error(
298                id,
299                protocol::METHOD_NOT_FOUND,
300                format!("unknown method: {other}"),
301            ),
302        }
303    }
304
305    // ── Method handlers ─────────────────────────────────────────────
306
307    /// MCP protocol version supported by this server.
308    const PROTOCOL_VERSION: &str = "2024-11-05";
309
310    fn handle_initialize(
311        &self,
312        id: Option<serde_json::Value>,
313        params: Option<&serde_json::Value>,
314        conn: &mut Connection,
315    ) -> JsonRpcResponse {
316        info!(server = %self.name, version = %self.version, "MCP initialize");
317
318        // Protocol version negotiation: if the client sends a
319        // `protocolVersion` in params, we report our supported version.
320        // The server always responds with the version it actually supports.
321        let mut caller: Option<&str> = None;
322        if let Some(p) = params {
323            if let Some(client_ver) = p.get("protocolVersion").and_then(|v| v.as_str()) {
324                debug!(client_version = %client_ver, server_version = Self::PROTOCOL_VERSION, "protocol version negotiation");
325            }
326
327            // When per-connection identity is enabled, adopt the caller name the
328            // client announced in `clientInfo.name` for this connection. The
329            // derived context shares the server's state and typed extensions, so
330            // every connection sees the same session while acting as itself.
331            if self.per_connection_identity {
332                if let Some(name) = p
333                    .get("clientInfo")
334                    .and_then(|c| c.get("name"))
335                    .and_then(|n| n.as_str())
336                    .map(str::trim)
337                    .filter(|n| !n.is_empty())
338                {
339                    info!(caller = %name, "adopting per-connection caller identity");
340                    conn.ctx = Some(self.context.with_caller(name));
341                    caller = Some(name);
342                } else {
343                    debug!(
344                        "per-connection identity enabled but client sent no \
345                         usable clientInfo.name; using shared server identity"
346                    );
347                }
348            }
349        }
350
351        // Resolve this caller's registry view. A no-op (leaves the shared
352        // registry in play) when no [`RegistryFactory`] is configured.
353        conn.view = self.resolve_view(caller);
354
355        let tools_cap = Some(ToolCapabilities {});
356        let prompts_cap = if self.prompts.is_empty() {
357            None
358        } else {
359            Some(PromptCapabilities {})
360        };
361        let resources_cap = if self.resources.is_empty() {
362            None
363        } else {
364            Some(ResourceCapabilities {})
365        };
366
367        JsonRpcResponse::success(
368            id,
369            InitializeResult {
370                protocol_version: Self::PROTOCOL_VERSION,
371                server_info: ServerInfo {
372                    name: self.name.clone(),
373                    version: self.version.clone(),
374                },
375                instructions: self.instructions.clone(),
376                capabilities: Capabilities {
377                    tools: tools_cap,
378                    resources: resources_cap,
379                    prompts: prompts_cap,
380                },
381            },
382        )
383    }
384
385    fn handle_tools_list(
386        &self,
387        id: Option<serde_json::Value>,
388        conn: &Connection,
389    ) -> JsonRpcResponse {
390        // Per-caller registry view when negotiated; otherwise the shared list.
391        let (count, tools_list) = conn.view.as_ref().map_or_else(
392            || (self.registry.len(), &self.cached_tools_list),
393            |v| (v.registry.len(), &v.tools_list),
394        );
395        info!(count, "tools/list");
396        // The tools/list body is pre-serialized (per caller, at first use);
397        // clone the cached JSON value instead of re-serializing the schema tree.
398        JsonRpcResponse {
399            jsonrpc: JSONRPC_VERSION,
400            id,
401            result: Some((**tools_list).clone()),
402            error: None,
403        }
404    }
405
406    /// Dispatch a `tools/call` programmatically and return the typed result.
407    ///
408    /// This runs the tool through the **exact same** code path as the JSON-RPC
409    /// wire handler (`tools/call`), so the not-found and error mapping is
410    /// identical: an unknown tool or a failing handler both yield a
411    /// [`ToolCallResult`] with [`is_error`](ToolCallResult::is_error) set to
412    /// `true` and the message surfaced in the content — never a panic or a
413    /// JSON-RPC-level error.
414    ///
415    /// Prefer this over hand-building JSON-RPC frames when calling a tool from
416    /// Rust (e.g. in tests): you get the typed [`ToolCallResult`] directly and
417    /// can read [`ToolCallResult::text`] instead of indexing into JSON.
418    ///
419    /// # Example
420    ///
421    /// ```rust
422    /// # use llm_tool::{ToolContext, ToolError, ToolRegistry, llm_tool};
423    /// # use llm_tool_mcp::McpServer;
424    /// /// Adds two numbers.
425    /// #[llm_tool]
426    /// fn add(
427    ///     /// First operand.
428    ///     a: i64,
429    ///     /// Second operand.
430    ///     b: i64,
431    /// ) -> Result<String, ToolError> {
432    ///     Ok(format!("{}", a + b))
433    /// }
434    ///
435    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
436    /// let server = McpServer::new("srv", "0.1.0", ToolRegistry::new().with_tool(Add));
437    /// let result = server.dispatch_tool("add", serde_json::json!({"a": 2, "b": 3})).await;
438    /// assert!(!result.is_error);
439    /// assert_eq!(result.text(), Some("5"));
440    /// # })
441    /// ```
442    pub async fn dispatch_tool(&self, name: &str, arguments: serde_json::Value) -> ToolCallResult {
443        self.call_tool(name, arguments, &self.context, &self.registry)
444            .await
445    }
446
447    /// Shared tool-call core: dispatch against the registry and map the outcome
448    /// to a [`ToolCallResult`].
449    ///
450    /// Both the wire handler ([`handle_tools_call`](Self::handle_tools_call))
451    /// and the programmatic entry point ([`dispatch_tool`](Self::dispatch_tool))
452    /// funnel through here so the success / error / not-found mapping lives in
453    /// exactly one place.
454    ///
455    /// Per the MCP spec, tool execution errors — and unknown tools — are
456    /// reported as a result with `isError=true`, not as JSON-RPC errors, so the
457    /// model can recover within the turn. JSON-RPC errors are reserved for
458    /// protocol-level failures (handled by the caller).
459    async fn call_tool(
460        &self,
461        name: &str,
462        arguments: serde_json::Value,
463        ctx: &ToolContext,
464        registry: &ToolRegistry,
465    ) -> ToolCallResult {
466        debug!(tool = %name, caller = ?ctx.conversation_id(), "tools/call");
467        match registry.dispatch(name, arguments, ctx).await {
468            Ok(output) => ToolCallResult {
469                content: vec![ContentItem::text(output.into_content())],
470                is_error: false,
471            },
472            Err(e) => ToolCallResult {
473                content: vec![ContentItem::text(e.to_string())],
474                is_error: true,
475            },
476        }
477    }
478
479    async fn handle_tools_call(
480        &self,
481        id: Option<serde_json::Value>,
482        params: Option<serde_json::Value>,
483        ctx: &ToolContext,
484        registry: &ToolRegistry,
485    ) -> JsonRpcResponse {
486        let Some(raw_params) = params else {
487            return JsonRpcResponse::error(
488                id,
489                protocol::INVALID_PARAMS,
490                "tools/call requires params with 'name' and 'arguments'",
491            );
492        };
493
494        let call_params: ToolCallParams = match serde_json::from_value(raw_params) {
495            Ok(p) => p,
496            Err(e) => {
497                return JsonRpcResponse::error(
498                    id,
499                    protocol::INVALID_PARAMS,
500                    format!("invalid tools/call params: {e}"),
501                );
502            }
503        };
504
505        let result = self
506            .call_tool(&call_params.name, call_params.arguments, ctx, registry)
507            .await;
508        JsonRpcResponse::success(id, result)
509    }
510
511    async fn handle_prompts_get(
512        &self,
513        id: Option<serde_json::Value>,
514        params: Option<serde_json::Value>,
515    ) -> JsonRpcResponse {
516        let Some(p_val) = params else {
517            return JsonRpcResponse::error(
518                id,
519                protocol::INVALID_PARAMS,
520                "missing params for prompts/get",
521            );
522        };
523        let get_params: protocol::GetPromptParams = match serde_json::from_value(p_val) {
524            Ok(v) => v,
525            Err(e) => {
526                return JsonRpcResponse::error(
527                    id,
528                    protocol::INVALID_PARAMS,
529                    format!("invalid params: {e}"),
530                );
531            }
532        };
533        match self
534            .prompts
535            .render(&get_params.name, get_params.arguments)
536            .await
537        {
538            Ok(output) => {
539                let messages = output
540                    .messages
541                    .into_iter()
542                    .map(|m| protocol::PromptMessage {
543                        role: m.role.to_string(),
544                        content: protocol::PromptMessageContent::Text { text: m.content },
545                    })
546                    .collect();
547                let res = protocol::GetPromptResult {
548                    description: None,
549                    messages,
550                };
551                JsonRpcResponse::success(id, res)
552            }
553            Err(err) => JsonRpcResponse::error(id, protocol::INVALID_PARAMS, err.message),
554        }
555    }
556
557    async fn handle_resources_read(
558        &self,
559        id: Option<serde_json::Value>,
560        params: Option<serde_json::Value>,
561    ) -> JsonRpcResponse {
562        let Some(p_val) = params else {
563            return JsonRpcResponse::error(
564                id,
565                protocol::INVALID_PARAMS,
566                "missing params for resources/read",
567            );
568        };
569        let read_params: protocol::ReadResourceParams = match serde_json::from_value(p_val) {
570            Ok(v) => v,
571            Err(e) => {
572                return JsonRpcResponse::error(
573                    id,
574                    protocol::INVALID_PARAMS,
575                    format!("invalid params: {e}"),
576                );
577            }
578        };
579        match self.resources.read(&read_params.uri).await {
580            Ok(output) => {
581                let res = protocol::ReadResourceResult {
582                    contents: output.contents,
583                };
584                JsonRpcResponse::success(id, res)
585            }
586            Err(err) => JsonRpcResponse::error(id, protocol::INVALID_PARAMS, err.message),
587        }
588    }
589}
590
591// ── Response serialization ──────────────────────────────────────────
592
593/// A dispatched JSON-RPC result, ready to be inspected or rendered.
594///
595/// Returned by [`McpServer::handle_message`]. A `None` from that method means
596/// there is nothing to send (a notification, or a batch of only
597/// notifications); a `Some` is either a [`Single`](Self::Single) response
598/// object or a [`Batch`](Self::Batch) array.
599///
600/// `RpcOutcome` implements [`Serialize`](serde::Serialize) and
601/// [`Display`](core::fmt::Display), both rendering a `Single` as a JSON object
602/// and a `Batch` as a JSON array. Use
603/// [`to_wire`](Self::to_wire) (or `.to_string()`) for a `String`, or
604/// [`write_json`](Self::write_json) to append directly to a byte buffer — each
605/// serializes in a single pass with no intermediate [`serde_json::Value`].
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub enum RpcOutcome {
608    /// A single JSON-RPC response object.
609    Single(JsonRpcResponse),
610    /// A JSON-RPC batch response array (always non-empty).
611    Batch(Vec<JsonRpcResponse>),
612}
613
614impl serde::Serialize for RpcOutcome {
615    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
616        match self {
617            Self::Single(response) => response.serialize(serializer),
618            Self::Batch(responses) => responses.serialize(serializer),
619        }
620    }
621}
622
623impl RpcOutcome {
624    /// Render this outcome to a single JSON wire string.
625    ///
626    /// # Panics
627    ///
628    /// Panics only if a well-formed MCP response fails to serialize, which
629    /// would indicate a bug in this crate.
630    #[must_use]
631    pub fn to_wire(&self) -> String {
632        serde_json::to_string(self).expect("MCP response must be JSON-serializable")
633    }
634
635    /// Render this outcome's JSON wire form by appending it to `buf`.
636    ///
637    /// Lets callers reuse a single buffer across many responses, avoiding a
638    /// fresh allocation per message.
639    ///
640    /// # Panics
641    ///
642    /// Panics only if a well-formed MCP response fails to serialize, which
643    /// would indicate a bug in this crate.
644    pub fn write_json(&self, buf: &mut Vec<u8>) {
645        serde_json::to_writer(buf, self).expect("MCP response must be JSON-serializable");
646    }
647
648    /// Returns `true` if this is a [`Batch`](Self::Batch) of responses.
649    #[must_use]
650    pub const fn is_batch(&self) -> bool {
651        matches!(self, Self::Batch(_))
652    }
653
654    /// Consume the outcome into a flat list of responses.
655    ///
656    /// A [`Single`](Self::Single) yields a one-element `Vec`; a
657    /// [`Batch`](Self::Batch) yields its responses unchanged.
658    #[must_use]
659    pub fn into_responses(self) -> Vec<JsonRpcResponse> {
660        match self {
661            Self::Single(response) => vec![response],
662            Self::Batch(responses) => responses,
663        }
664    }
665}
666
667impl fmt::Display for RpcOutcome {
668    /// Renders the JSON wire form (object for `Single`, array for `Batch`).
669    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
670        f.write_str(&self.to_wire())
671    }
672}