Skip to main content

faucet_cli/mcp/
protocol.rs

1//! JSON-RPC 2.0 + MCP protocol types (issue #420).
2//!
3//! A minimal, hand-rolled subset of the [Model Context Protocol](https://modelcontextprotocol.io)
4//! — enough to serve `initialize`, `tools/list`, `tools/call`, `resources/list`,
5//! `resources/read`, and `ping` over any byte transport. Kept dependency-free
6//! (just `serde_json`) so the `mcp` feature adds no new crate tree.
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Value, json};
10
11/// MCP protocol version this server implements/advertises.
12pub const PROTOCOL_VERSION: &str = "2024-11-05";
13
14/// JSON-RPC 2.0 standard error codes.
15pub const PARSE_ERROR: i64 = -32700;
16pub const INVALID_REQUEST: i64 = -32600;
17pub const METHOD_NOT_FOUND: i64 = -32601;
18pub const INVALID_PARAMS: i64 = -32602;
19pub const INTERNAL_ERROR: i64 = -32603;
20
21/// An incoming JSON-RPC request or notification.
22///
23/// A message with no `id` is a *notification* (no response is sent).
24#[derive(Debug, Clone, Deserialize)]
25pub struct JsonRpcRequest {
26    #[serde(default)]
27    pub jsonrpc: String,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub id: Option<Value>,
30    pub method: String,
31    #[serde(default)]
32    pub params: Value,
33}
34
35impl JsonRpcRequest {
36    /// A request carrying an `id` expects a response; a notification does not.
37    pub fn is_notification(&self) -> bool {
38        self.id.is_none()
39    }
40}
41
42/// Build a success response envelope for `id` with `result`.
43pub fn success(id: Value, result: Value) -> Value {
44    json!({ "jsonrpc": "2.0", "id": id, "result": result })
45}
46
47/// Build an error response envelope for `id`.
48pub fn error(id: Value, code: i64, message: impl Into<String>) -> Value {
49    json!({
50        "jsonrpc": "2.0",
51        "id": id,
52        "error": { "code": code, "message": message.into() }
53    })
54}
55
56/// An error whose `id` is unknown (e.g. an unparseable request): JSON-RPC
57/// requires `id: null` in that case.
58pub fn error_no_id(code: i64, message: impl Into<String>) -> Value {
59    error(Value::Null, code, message)
60}
61
62/// A tool definition advertised via `tools/list`.
63#[derive(Debug, Clone, Serialize)]
64pub struct ToolDef {
65    pub name: &'static str,
66    pub description: &'static str,
67    #[serde(rename = "inputSchema")]
68    pub input_schema: Value,
69}
70
71/// Wrap a tool's textual result in the MCP `tools/call` content shape.
72pub fn tool_text(text: impl Into<String>) -> Value {
73    json!({
74        "content": [ { "type": "text", "text": text.into() } ],
75        "isError": false
76    })
77}
78
79/// Wrap a tool error in the MCP `tools/call` content shape (`isError: true`).
80///
81/// Per the MCP spec a *tool* failure is reported as a normal result with
82/// `isError: true` (not a JSON-RPC protocol error), so the model can see and
83/// react to it.
84pub fn tool_error(text: impl Into<String>) -> Value {
85    json!({
86        "content": [ { "type": "text", "text": text.into() } ],
87        "isError": true
88    })
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn parses_request_with_id() {
97        let r: JsonRpcRequest =
98            serde_json::from_str(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#).unwrap();
99        assert_eq!(r.method, "ping");
100        assert!(!r.is_notification());
101        assert_eq!(r.id, Some(json!(1)));
102    }
103
104    #[test]
105    fn parses_notification_without_id() {
106        let r: JsonRpcRequest =
107            serde_json::from_str(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#)
108                .unwrap();
109        assert!(r.is_notification());
110    }
111
112    #[test]
113    fn success_envelope_shape() {
114        let v = success(json!(7), json!({"ok": true}));
115        assert_eq!(v["jsonrpc"], "2.0");
116        assert_eq!(v["id"], 7);
117        assert_eq!(v["result"]["ok"], true);
118    }
119
120    #[test]
121    fn error_envelope_shape() {
122        let v = error(json!(7), METHOD_NOT_FOUND, "nope");
123        assert_eq!(v["error"]["code"], METHOD_NOT_FOUND);
124        assert_eq!(v["error"]["message"], "nope");
125        assert!(v.get("result").is_none());
126    }
127
128    #[test]
129    fn tool_text_and_error_shapes() {
130        let ok = tool_text("hi");
131        assert_eq!(ok["isError"], false);
132        assert_eq!(ok["content"][0]["type"], "text");
133        assert_eq!(ok["content"][0]["text"], "hi");
134        let err = tool_error("boom");
135        assert_eq!(err["isError"], true);
136    }
137}