1use serde::{Deserialize, Serialize};
5
6pub(crate) const MCP_PROTOCOL_VERSION: &str = "2026-07-28";
9
10const JSONRPC_VERSION: &str = "2.0";
12const DISCOVER: &str = "server/discover";
13const TOOLS_LIST: &str = "tools/list";
14const TOOLS_CALL: &str = "tools/call";
15const RESULT_COMPLETE: &str = "complete";
16const PROTOCOL_VERSION_KEY: &str = "io.modelcontextprotocol/protocolVersion";
17const CLIENT_CAPABILITIES_KEY: &str = "io.modelcontextprotocol/clientCapabilities";
18const SERVER_INFO_KEY: &str = "io.modelcontextprotocol/serverInfo";
19
20const PARSE_ERROR: i64 = -32700;
21const INVALID_REQUEST: i64 = -32600;
22const METHOD_NOT_FOUND: i64 = -32601;
23const INVALID_PARAMS: i64 = -32602;
24const INTERNAL_ERROR: i64 = -32603;
25const UNSUPPORTED_PROTOCOL_VERSION: i64 = -32022;
26
27const TOOLS_TTL_MS: u64 = 24 * 60 * 60 * 1000;
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(untagged)]
34pub(crate) enum RequestId {
35 Number(i64),
36 String(String),
37}
38
39pub(crate) enum Incoming {
41 Discover {
42 id: RequestId,
43 },
44 ListTools {
45 id: RequestId,
46 },
47 Call(ToolsCall),
48 Ignored,
50 Refused(Refusal),
52}
53
54pub(crate) struct ToolsCall {
56 pub(crate) id: RequestId,
57 pub(crate) name: String,
58 pub(crate) arguments: serde_json::Value,
59}
60
61#[derive(Deserialize)]
64struct RawMessage {
65 jsonrpc: String,
66 #[serde(default)]
67 id: Option<RequestId>,
68 #[serde(default)]
69 method: Option<String>,
70 #[serde(default)]
71 params: Option<serde_json::Value>,
72 #[serde(default)]
73 result: Option<serde_json::Value>,
74 #[serde(default)]
75 error: Option<serde_json::Value>,
76}
77
78#[derive(Deserialize)]
79struct ToolsCallParams {
80 name: String,
81 #[serde(default = "no_arguments")]
83 arguments: serde_json::Value,
84}
85
86fn no_arguments() -> serde_json::Value {
87 serde_json::Value::Object(serde_json::Map::new())
88}
89
90pub(crate) struct Refusal {
92 id: Option<RequestId>,
94 code: i64,
95 message: String,
96 data: Option<Box<serde_json::Value>>,
97}
98
99impl Refusal {
100 fn new(id: Option<RequestId>, code: i64, message: String) -> Refusal {
101 Refusal {
102 id,
103 code,
104 message,
105 data: None,
106 }
107 }
108
109 pub(crate) fn unreadable(error: &std::io::Error) -> Refusal {
110 Refusal::new(None, INTERNAL_ERROR, format!("stdin unreadable: {error}"))
111 }
112
113 pub(crate) fn line(&self) -> Vec<u8> {
114 let error = serde_json::json!({"code": self.code, "message": self.message});
115 let error = match &self.data {
116 None => error,
117 Some(data) => {
118 serde_json::json!({"code": self.code, "message": self.message, "data": data})
119 }
120 };
121 line(&serde_json::json!({"jsonrpc": JSONRPC_VERSION, "id": self.id, "error": error}))
122 }
123}
124
125pub(crate) fn read(line: &[u8]) -> Incoming {
127 let refused = |id, code, message| Incoming::Refused(Refusal::new(id, code, message));
128 let value: serde_json::Value = match serde_json::from_slice(line) {
129 Ok(value) => value,
130 Err(e) => return refused(None, PARSE_ERROR, format!("not JSON: {e}")),
131 };
132 let message: RawMessage = match serde_json::from_value(value) {
133 Ok(message) => message,
134 Err(e) => return refused(None, INVALID_REQUEST, format!("not a message: {e}")),
135 };
136 match message.jsonrpc.as_str() {
137 JSONRPC_VERSION => {}
138 version => {
139 return refused(
140 message.id,
141 INVALID_REQUEST,
142 format!("unknown JSON-RPC version: {version}"),
143 );
144 }
145 }
146 let (id, method) = match (message.id, message.method) {
147 (Some(id), Some(method)) => (id, method),
148 (None, Some(_)) => return Incoming::Ignored,
149 (Some(_), None) if message.result.is_some() || message.error.is_some() => {
150 return Incoming::Ignored;
151 }
152 (id, None) => {
153 return refused(
154 id,
155 INVALID_REQUEST,
156 "neither a request, a notification, nor a response".to_string(),
157 );
158 }
159 };
160 let params = message.params.unwrap_or(serde_json::Value::Null);
161 if let Err(refusal) = check_meta(&id, ¶ms) {
162 return Incoming::Refused(refusal);
163 }
164 match method.as_str() {
165 DISCOVER => Incoming::Discover { id },
166 TOOLS_LIST => match params.get("cursor") {
167 Some(_) => refused(Some(id), INVALID_PARAMS, "no such cursor".to_string()),
169 None => Incoming::ListTools { id },
170 },
171 TOOLS_CALL => match serde_json::from_value::<ToolsCallParams>(params) {
172 Ok(params) => Incoming::Call(ToolsCall {
173 id,
174 name: params.name,
175 arguments: params.arguments,
176 }),
177 Err(e) => refused(
178 Some(id),
179 INVALID_PARAMS,
180 format!("invalid tools/call params: {e}"),
181 ),
182 },
183 other => refused(
184 Some(id),
185 METHOD_NOT_FOUND,
186 format!("unknown method: {other}"),
187 ),
188 }
189}
190
191fn check_meta(id: &RequestId, params: &serde_json::Value) -> Result<(), Refusal> {
194 let meta = params.get("_meta");
195 let field = |key| meta.and_then(|meta| meta.get(key));
196 let missing = |key| {
197 Refusal::new(
198 Some(id.clone()),
199 INVALID_PARAMS,
200 format!("missing _meta {key}; this server speaks MCP {MCP_PROTOCOL_VERSION}"),
201 )
202 };
203 let version = match field(PROTOCOL_VERSION_KEY) {
204 Some(serde_json::Value::String(version)) => version,
205 Some(_) | None => return Err(missing(PROTOCOL_VERSION_KEY)),
206 };
207 match version.as_str() {
208 MCP_PROTOCOL_VERSION => {}
209 other => {
210 return Err(Refusal {
211 id: Some(id.clone()),
212 code: UNSUPPORTED_PROTOCOL_VERSION,
213 message: "Unsupported protocol version".to_string(),
214 data: Some(Box::new(serde_json::json!({
215 "supported": [MCP_PROTOCOL_VERSION],
216 "requested": other,
217 }))),
218 });
219 }
220 }
221 match field(CLIENT_CAPABILITIES_KEY) {
222 Some(serde_json::Value::Object(_)) => Ok(()),
223 Some(_) | None => Err(missing(CLIENT_CAPABILITIES_KEY)),
224 }
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
229pub struct ServerInfo<'a> {
230 pub name: &'a str,
231 pub version: &'a str,
232}
233
234pub(crate) fn discover_line(id: &RequestId, server: &ServerInfo<'_>) -> Vec<u8> {
237 result_line(
238 id,
239 server,
240 [
241 (
242 "supportedVersions",
243 serde_json::json!([MCP_PROTOCOL_VERSION]),
244 ),
245 ("capabilities", serde_json::json!({"tools": {}})),
246 ]
247 .into_iter(),
248 )
249}
250
251pub(crate) fn tools_line(
252 id: &RequestId,
253 server: &ServerInfo<'_>,
254 tools: Vec<serde_json::Value>,
255) -> Vec<u8> {
256 result_line(
257 id,
258 server,
259 [
260 ("tools", serde_json::json!(tools)),
261 ("ttlMs", serde_json::json!(TOOLS_TTL_MS)),
262 ("cacheScope", serde_json::json!("public")),
263 ]
264 .into_iter(),
265 )
266}
267
268pub(crate) fn result_line<'k>(
271 id: &RequestId,
272 server: &ServerInfo<'_>,
273 fields: impl Iterator<Item = (&'k str, serde_json::Value)>,
274) -> Vec<u8> {
275 let signed = [
276 ("resultType", serde_json::json!(RESULT_COMPLETE)),
277 (
278 "_meta",
279 serde_json::json!({
280 SERVER_INFO_KEY: {"name": server.name, "version": server.version},
281 }),
282 ),
283 ];
284 let result: serde_json::Map<String, serde_json::Value> = fields
285 .chain(signed)
286 .map(|(key, value)| (key.to_string(), value))
287 .collect();
288 line(&serde_json::json!({"jsonrpc": JSONRPC_VERSION, "id": id, "result": result}))
289}
290
291fn line(message: &serde_json::Value) -> Vec<u8> {
293 let mut line = serde_json::to_vec(message).unwrap_or_default();
295 line.push(b'\n');
296 line
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 #[test]
304 fn an_unreadable_stdin_is_an_internal_error_answering_no_id() {
305 let line = Refusal::unreadable(&std::io::Error::other("gone")).line();
306 let message: serde_json::Value = serde_json::from_slice(&line).unwrap();
307 assert_eq!(
308 message,
309 serde_json::json!({
310 "jsonrpc": "2.0",
311 "id": null,
312 "error": {"code": INTERNAL_ERROR, "message": "stdin unreadable: gone"},
313 })
314 );
315 }
316}