Skip to main content

llm_tool_mcp/
protocol.rs

1//! JSON-RPC 2.0 protocol types for MCP communication.
2//!
3//! These types model the wire format used by MCP's JSON-RPC transport.
4//! Each request/response is a single JSON line on the stream.
5
6use serde::{Deserialize, Serialize};
7
8// ── JSON-RPC 2.0 constants ──────────────────────────────────────────
9
10/// The only valid JSON-RPC protocol version.
11pub const JSONRPC_VERSION: &str = "2.0";
12
13// ── Standard JSON-RPC 2.0 error codes ───────────────────────────────
14
15/// Malformed JSON.
16pub const PARSE_ERROR: i64 = -32700;
17
18/// Valid JSON but not a valid JSON-RPC request.
19pub const INVALID_REQUEST: i64 = -32600;
20
21/// The requested method does not exist.
22pub const METHOD_NOT_FOUND: i64 = -32601;
23
24/// Invalid method parameters.
25pub const INVALID_PARAMS: i64 = -32602;
26
27/// Internal server error.
28pub const INTERNAL_ERROR: i64 = -32603;
29
30// ── MCP JSON-RPC method names ───────────────────────────────────────
31//
32// Every JSON-RPC `method` string the server dispatches on has a named
33// constant here, so the wire protocol is defined in exactly one place and
34// the server match arms never repeat a magic string literal.
35
36/// `initialize` — capability negotiation handshake.
37pub const METHOD_INITIALIZE: &str = "initialize";
38
39/// `ping` — liveness check; server replies with an empty result.
40pub const METHOD_PING: &str = "ping";
41
42/// `logging/setLevel` — client sets the server log level.
43pub const METHOD_LOGGING_SET_LEVEL: &str = "logging/setLevel";
44
45/// `notifications/initialized` — client signals it finished initializing.
46pub const METHOD_NOTIFICATIONS_INITIALIZED: &str = "notifications/initialized";
47
48/// `initialized` — bare alias some clients send instead of the namespaced form.
49pub const METHOD_INITIALIZED: &str = "initialized";
50
51/// `notifications/cancelled` — client cancels an in-flight request.
52pub const METHOD_NOTIFICATIONS_CANCELLED: &str = "notifications/cancelled";
53
54/// `tools/list` — enumerate available tools and their schemas.
55pub const METHOD_TOOLS_LIST: &str = "tools/list";
56
57/// `tools/call` — invoke a named tool with arguments.
58pub const METHOD_TOOLS_CALL: &str = "tools/call";
59
60/// `resources/list` — enumerate concrete resources.
61pub const METHOD_RESOURCES_LIST: &str = "resources/list";
62
63/// `resources/templates/list` — enumerate resource URI templates.
64pub const METHOD_RESOURCES_TEMPLATES_LIST: &str = "resources/templates/list";
65
66/// `resources/read` — read a resource by URI.
67pub const METHOD_RESOURCES_READ: &str = "resources/read";
68
69/// `prompts/list` — enumerate registered prompts.
70pub const METHOD_PROMPTS_LIST: &str = "prompts/list";
71
72/// `prompts/get` — render a prompt with arguments.
73pub const METHOD_PROMPTS_GET: &str = "prompts/get";
74
75/// `completion/complete` — argument-completion request.
76pub const METHOD_COMPLETION_COMPLETE: &str = "completion/complete";
77
78/// `notifications/progress` — progress update notification.
79pub const METHOD_NOTIFICATIONS_PROGRESS: &str = "notifications/progress";
80
81/// `notifications/message` — log-message notification.
82pub const METHOD_NOTIFICATIONS_MESSAGE: &str = "notifications/message";
83
84// ── Request ─────────────────────────────────────────────────────────
85
86/// A JSON-RPC 2.0 request.
87#[derive(Debug, Deserialize)]
88pub struct JsonRpcRequest {
89    /// Protocol version — must be `"2.0"`.
90    #[serde(rename = "jsonrpc")]
91    pub version: String,
92
93    /// Request identifier (number or string). `None` for notifications.
94    pub id: Option<serde_json::Value>,
95
96    /// Method name (e.g. `"initialize"`, `"tools/list"`, `"tools/call"`).
97    pub method: String,
98
99    /// Optional parameters.
100    #[serde(default)]
101    pub params: Option<serde_json::Value>,
102}
103
104// ── Response ────────────────────────────────────────────────────────
105
106/// A JSON-RPC 2.0 response.
107#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
108pub struct JsonRpcResponse {
109    /// Protocol version — always `"2.0"`.
110    pub jsonrpc: &'static str,
111
112    /// Echoed request identifier.
113    pub id: Option<serde_json::Value>,
114
115    /// Present on success.
116    #[serde(skip_serializing_if = "Option::is_none")]
117    pub result: Option<serde_json::Value>,
118
119    /// Present on error.
120    #[serde(skip_serializing_if = "Option::is_none")]
121    pub error: Option<JsonRpcError>,
122}
123
124/// A JSON-RPC 2.0 error object.
125#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
126pub struct JsonRpcError {
127    /// Numeric error code.
128    pub code: i64,
129    /// Human-readable description.
130    pub message: String,
131    /// Optional additional data about the error.
132    #[serde(skip_serializing_if = "Option::is_none")]
133    pub data: Option<serde_json::Value>,
134}
135
136impl JsonRpcResponse {
137    /// Build a success response from any serializable result type.
138    ///
139    /// # Panics
140    ///
141    /// Panics if `result` cannot be serialized to JSON. This should never
142    /// happen for the well-formed MCP structs in this module.
143    #[must_use]
144    pub fn success(id: Option<serde_json::Value>, result: impl Serialize) -> Self {
145        Self {
146            jsonrpc: "2.0",
147            id,
148            result: Some(
149                serde_json::to_value(result).expect("MCP result type must be JSON-serializable"),
150            ),
151            error: None,
152        }
153    }
154
155    /// Build an error response.
156    #[must_use]
157    pub fn error(id: Option<serde_json::Value>, code: i64, message: impl Into<String>) -> Self {
158        Self {
159            jsonrpc: "2.0",
160            id,
161            result: None,
162            error: Some(JsonRpcError {
163                code,
164                message: message.into(),
165                data: None,
166            }),
167        }
168    }
169
170    /// Build an error response with additional structured data.
171    #[must_use]
172    pub fn error_with_data(
173        id: Option<serde_json::Value>,
174        code: i64,
175        message: impl Into<String>,
176        data: serde_json::Value,
177    ) -> Self {
178        Self {
179            jsonrpc: "2.0",
180            id,
181            result: None,
182            error: Some(JsonRpcError {
183                code,
184                message: message.into(),
185                data: Some(data),
186            }),
187        }
188    }
189}
190
191// ── MCP-specific types ──────────────────────────────────────────────
192
193/// Result body for `initialize`.
194#[derive(Debug, Serialize)]
195#[serde(rename_all = "camelCase")]
196pub struct InitializeResult {
197    /// MCP protocol version (e.g. `"2024-11-05"`).
198    pub protocol_version: &'static str,
199    /// Server name and version.
200    pub server_info: ServerInfo,
201    /// Optional instructions describing how to use the server.
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub instructions: Option<String>,
204    /// Advertised capabilities.
205    pub capabilities: Capabilities,
206}
207
208/// Server identification returned in `initialize`.
209#[derive(Debug, Serialize)]
210pub struct ServerInfo {
211    /// Human-readable server name.
212    pub name: String,
213    /// Server version string.
214    pub version: String,
215}
216
217/// Server capabilities advertised during `initialize`.
218#[derive(Debug, Default, Serialize)]
219pub struct Capabilities {
220    /// Tool support — presence signals that `tools/list` and `tools/call`
221    /// are available.
222    #[serde(skip_serializing_if = "Option::is_none")]
223    pub tools: Option<ToolCapabilities>,
224    /// Resource support — presence signals that `resources/list` is available.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub resources: Option<ResourceCapabilities>,
227    /// Prompt support — presence signals that `prompts/list` is available.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub prompts: Option<PromptCapabilities>,
230}
231
232/// Tool-specific capabilities (currently empty per MCP spec).
233#[derive(Debug, Default, Serialize)]
234pub struct ToolCapabilities {}
235
236/// Resource-specific capabilities (currently empty per MCP spec).
237#[derive(Debug, Default, Serialize)]
238pub struct ResourceCapabilities {}
239
240/// Prompt-specific capabilities (currently empty per MCP spec).
241#[derive(Debug, Default, Serialize)]
242pub struct PromptCapabilities {}
243
244/// Result body for `tools/list`.
245#[derive(Clone, Debug, Serialize)]
246pub struct ToolsListResult {
247    /// Available tools.
248    pub tools: Vec<McpToolSchema>,
249}
250
251/// A single tool's schema in the `tools/list` response.
252#[derive(Clone, Debug, Serialize)]
253#[serde(rename_all = "camelCase")]
254pub struct McpToolSchema {
255    /// Tool name.
256    pub name: String,
257    /// Human-readable description.
258    pub description: String,
259    /// JSON Schema for the tool's input parameters.
260    pub input_schema: serde_json::Value,
261}
262
263/// Deserialized `tools/call` request parameters.
264#[derive(Debug, Deserialize)]
265pub struct ToolCallParams {
266    /// Name of the tool to invoke.
267    pub name: String,
268    /// Tool arguments (defaults to `{}` if absent).
269    #[serde(default = "empty_object")]
270    pub arguments: serde_json::Value,
271}
272
273/// Returns an empty JSON object — used as the serde default for
274/// `ToolCallParams::arguments`.
275fn empty_object() -> serde_json::Value {
276    serde_json::Value::Object(serde_json::Map::new())
277}
278
279/// Result body for a successful `tools/call`.
280#[derive(Debug, Serialize)]
281#[serde(rename_all = "camelCase")]
282pub struct ToolCallResult {
283    /// Response content blocks.
284    pub content: Vec<ContentItem>,
285    /// `true` when the tool returned an error (MCP-level, not JSON-RPC).
286    #[serde(skip_serializing_if = "std::ops::Not::not")]
287    pub is_error: bool,
288}
289
290impl ToolCallResult {
291    /// The text of the first content block, if any.
292    ///
293    /// Convenience for callers of [`McpServer::dispatch_tool`] who want the
294    /// tool's textual output without indexing into [`content`](Self::content).
295    ///
296    /// [`McpServer::dispatch_tool`]: crate::McpServer::dispatch_tool
297    #[must_use]
298    pub fn text(&self) -> Option<&str> {
299        self.content.first().map(|item| item.text.as_str())
300    }
301}
302
303/// The `type` value of a text [`ContentItem`] — currently the only content type
304/// this server emits.
305pub const CONTENT_TYPE_TEXT: &str = "text";
306
307/// A single content block in a `tools/call` response.
308#[derive(Debug, Serialize)]
309pub struct ContentItem {
310    /// Content type — currently always [`CONTENT_TYPE_TEXT`].
311    #[serde(rename = "type")]
312    pub content_type: &'static str,
313    /// The text content.
314    pub text: String,
315}
316
317impl ContentItem {
318    /// Build a text content block, tagging it with [`CONTENT_TYPE_TEXT`].
319    ///
320    /// Prefer this over constructing [`ContentItem`] literally so the content
321    /// type is set consistently in one place.
322    pub fn text(text: impl Into<String>) -> Self {
323        Self {
324            content_type: CONTENT_TYPE_TEXT,
325            text: text.into(),
326        }
327    }
328}
329
330// ── Prompts ─────────────────────────────────────────────────────────
331
332/// Result body for `prompts/list`.
333#[derive(Clone, Debug, Serialize)]
334pub struct PromptsListResult {
335    /// Available prompts.
336    pub prompts: Vec<PromptDefinition>,
337}
338
339pub use llm_tool::{PromptArgumentDefinition, PromptDefinition};
340
341/// Parameters for `prompts/get`.
342#[derive(Debug, Deserialize)]
343pub struct GetPromptParams {
344    /// Name of the prompt to retrieve.
345    pub name: String,
346    /// Arguments to substitute into the template.
347    #[serde(default = "empty_object")]
348    pub arguments: serde_json::Value,
349}
350
351/// Result body for `prompts/get`.
352#[derive(Debug, Serialize)]
353pub struct GetPromptResult {
354    /// Optional description of the rendered prompt.
355    #[serde(skip_serializing_if = "Option::is_none")]
356    pub description: Option<String>,
357    /// Rendered messages.
358    pub messages: Vec<PromptMessage>,
359}
360
361/// A rendered message inside `GetPromptResult`.
362#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
363pub struct PromptMessage {
364    /// Role (`"user"` or `"assistant"`).
365    pub role: String,
366    /// Content block.
367    pub content: PromptMessageContent,
368}
369
370/// Content inside a `PromptMessage`.
371#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
372#[serde(tag = "type")]
373pub enum PromptMessageContent {
374    /// Text content.
375    #[serde(rename = "text")]
376    Text {
377        /// Text string.
378        text: String,
379    },
380    /// Embedded resource content.
381    #[serde(rename = "resource")]
382    Resource {
383        /// Resource payload.
384        resource: ResourceContent,
385    },
386}
387
388// ── Resources ───────────────────────────────────────────────────────
389
390/// Wire format for a concrete resource in `resources/list`.
391#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
392#[serde(rename_all = "camelCase")]
393pub struct McpResource {
394    /// Resource URI.
395    pub uri: String,
396    /// Human-readable name.
397    pub name: String,
398    /// Optional description.
399    #[serde(default, skip_serializing_if = "String::is_empty")]
400    pub description: String,
401    /// Optional MIME type.
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub mime_type: Option<String>,
404}
405
406pub use McpResource as Resource;
407
408/// Result body for `resources/list`.
409#[derive(Clone, Debug, Serialize, Deserialize)]
410pub struct ResourcesListResult {
411    /// Available resources.
412    pub resources: Vec<McpResource>,
413}
414
415pub use llm_tool::ResourceDefinition;
416
417/// Parameters for `resources/read`.
418#[derive(Debug, Deserialize)]
419pub struct ReadResourceParams {
420    /// URI of the resource to read.
421    pub uri: String,
422}
423
424/// Result body for `resources/read`.
425#[derive(Debug, Serialize, Deserialize)]
426pub struct ReadResourceResult {
427    /// Resource content blocks.
428    pub contents: Vec<ResourceContent>,
429}
430
431pub use llm_tool::ResourceOutputContent as ResourceContent;
432
433/// An empty JSON object used for responses like ping or logging/setLevel.
434#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
435pub struct EmptyResult {}
436
437/// Result body for `resources/templates/list`.
438#[derive(Clone, Debug, Serialize, Deserialize)]
439#[serde(rename_all = "camelCase")]
440pub struct ResourceTemplatesListResult {
441    /// Available resource templates.
442    pub resource_templates: Vec<ResourceDefinition>,
443}
444
445/// Result body for `completion/complete`.
446#[derive(Clone, Debug, Serialize, Deserialize, Default)]
447pub struct CompletionCompleteResult {
448    /// Completion values and pagination.
449    pub completion: CompletionResultData,
450}
451
452/// Data inside `CompletionCompleteResult`.
453#[derive(Clone, Debug, Serialize, Deserialize, Default)]
454#[serde(rename_all = "camelCase")]
455pub struct CompletionResultData {
456    /// Recommended completion values.
457    pub values: Vec<String>,
458    /// Total number of available completions.
459    pub total: usize,
460    /// Whether more completions are available.
461    pub has_more: bool,
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn deserialize_request_with_params() {
470        let json = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add"}}"#;
471        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
472        assert_eq!(req.version, "2.0");
473        assert_eq!(req.id, Some(serde_json::json!(1)));
474        assert_eq!(req.method, "tools/call");
475        assert!(req.params.is_some());
476    }
477
478    #[test]
479    fn deserialize_request_without_params() {
480        let json = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
481        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
482        assert!(req.params.is_none());
483    }
484
485    #[test]
486    fn deserialize_notification_without_id() {
487        let json = r#"{"jsonrpc":"2.0","method":"initialized"}"#;
488        let req: JsonRpcRequest = serde_json::from_str(json).unwrap();
489        assert!(req.id.is_none());
490    }
491
492    #[test]
493    fn serialize_success_response() {
494        let resp =
495            JsonRpcResponse::success(Some(serde_json::json!(1)), serde_json::json!({"ok": true}));
496        let json = serde_json::to_string(&resp).unwrap();
497        assert!(json.contains(r#""jsonrpc":"2.0""#));
498        assert!(json.contains(r#""result":{""#));
499        assert!(!json.contains("error"));
500    }
501
502    #[test]
503    fn serialize_error_response() {
504        let resp = JsonRpcResponse::error(Some(serde_json::json!(1)), PARSE_ERROR, "bad json");
505        let json = serde_json::to_string(&resp).unwrap();
506        assert!(json.contains(r#""code":-32700"#));
507        assert!(json.contains(r#""message":"bad json""#));
508        assert!(!json.contains("result"));
509    }
510
511    #[test]
512    fn serialize_error_omits_null_id() {
513        let resp = JsonRpcResponse::error(None, METHOD_NOT_FOUND, "no such method");
514        let json = serde_json::to_string(&resp).unwrap();
515        assert!(json.contains(r#""id":null"#));
516    }
517
518    #[test]
519    fn response_jsonrpc_field_is_static() {
520        let resp = JsonRpcResponse::success(None, serde_json::json!(null));
521        // &'static str avoids allocation for every response.
522        assert_eq!(resp.jsonrpc, "2.0");
523    }
524
525    #[test]
526    fn error_without_data_omits_data_field() {
527        let resp = JsonRpcResponse::error(Some(serde_json::json!(1)), PARSE_ERROR, "bad");
528        let json = serde_json::to_string(&resp).unwrap();
529        assert!(!json.contains("data"));
530    }
531
532    #[test]
533    fn error_with_data_includes_data_field() {
534        let resp = JsonRpcResponse::error_with_data(
535            Some(serde_json::json!(1)),
536            INTERNAL_ERROR,
537            "boom",
538            serde_json::json!({"detail": "stack trace"}),
539        );
540        let json = serde_json::to_string(&resp).unwrap();
541        assert!(json.contains(r#""data":{"detail":"stack trace"}"#));
542    }
543
544    #[test]
545    fn jsonrpc_version_constant() {
546        assert_eq!(JSONRPC_VERSION, "2.0");
547    }
548
549    #[test]
550    fn method_consts_match_wire_strings() {
551        assert_eq!(METHOD_INITIALIZE, "initialize");
552        assert_eq!(METHOD_PING, "ping");
553        assert_eq!(METHOD_LOGGING_SET_LEVEL, "logging/setLevel");
554        assert_eq!(
555            METHOD_NOTIFICATIONS_INITIALIZED,
556            "notifications/initialized"
557        );
558        assert_eq!(METHOD_INITIALIZED, "initialized");
559        assert_eq!(METHOD_NOTIFICATIONS_CANCELLED, "notifications/cancelled");
560        assert_eq!(METHOD_TOOLS_LIST, "tools/list");
561        assert_eq!(METHOD_TOOLS_CALL, "tools/call");
562        assert_eq!(METHOD_RESOURCES_LIST, "resources/list");
563        assert_eq!(METHOD_RESOURCES_TEMPLATES_LIST, "resources/templates/list");
564        assert_eq!(METHOD_RESOURCES_READ, "resources/read");
565        assert_eq!(METHOD_PROMPTS_LIST, "prompts/list");
566        assert_eq!(METHOD_PROMPTS_GET, "prompts/get");
567        assert_eq!(METHOD_COMPLETION_COMPLETE, "completion/complete");
568        assert_eq!(METHOD_NOTIFICATIONS_PROGRESS, "notifications/progress");
569        assert_eq!(METHOD_NOTIFICATIONS_MESSAGE, "notifications/message");
570    }
571
572    #[test]
573    fn content_item_text_constructor_sets_type() {
574        let item = ContentItem::text("hello");
575        assert_eq!(item.content_type, CONTENT_TYPE_TEXT);
576        assert_eq!(item.content_type, "text");
577        assert_eq!(item.text, "hello");
578    }
579
580    #[test]
581    fn tool_call_result_text_returns_first_block() {
582        let result = ToolCallResult {
583            content: vec![ContentItem::text("first"), ContentItem::text("second")],
584            is_error: false,
585        };
586        assert_eq!(result.text(), Some("first"));
587
588        let empty = ToolCallResult {
589            content: vec![],
590            is_error: true,
591        };
592        assert_eq!(empty.text(), None);
593    }
594}