use serde::Deserialize;
const MCP_TOOLS_CALL_METHOD: &str = "tools/call";
const JSONRPC_VERSION: &str = "2.0";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpToolCall {
pub tool_name: String,
pub arguments: serde_json::Value,
}
pub fn parse_mcp_request(body: &[u8]) -> Option<McpToolCall> {
#[derive(Deserialize)]
struct Envelope {
jsonrpc: Option<String>,
method: Option<String>,
params: Option<Params>,
}
#[derive(Deserialize)]
struct Params {
name: Option<String>,
#[serde(default)]
arguments: serde_json::Value,
}
let envelope: Envelope = serde_json::from_slice(body).ok()?;
if envelope.jsonrpc.as_deref() != Some(JSONRPC_VERSION) {
return None;
}
if envelope.method.as_deref() != Some(MCP_TOOLS_CALL_METHOD) {
return None;
}
let params = envelope.params?;
let tool_name = params.name?;
Some(McpToolCall {
tool_name,
arguments: params.arguments,
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn extracts_tool_name_and_arguments_from_tools_call() {
let body = br#"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": { "path": "/etc/passwd" }
}
}"#;
let call = parse_mcp_request(body).expect("valid tools/call must parse");
assert_eq!(call.tool_name, "read_file");
assert_eq!(call.arguments, json!({ "path": "/etc/passwd" }));
}
#[test]
fn returns_none_when_method_is_not_tools_call() {
let body = br#"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}"#;
assert!(parse_mcp_request(body).is_none());
}
#[test]
fn returns_none_when_jsonrpc_version_is_wrong_or_missing() {
let wrong_version = br#"{"jsonrpc":"1.0","id":1,"method":"tools/call","params":{"name":"x"}}"#;
assert!(parse_mcp_request(wrong_version).is_none());
let no_version = br#"{"id":1,"method":"tools/call","params":{"name":"x"}}"#;
assert!(parse_mcp_request(no_version).is_none());
}
#[test]
fn returns_none_when_params_name_is_missing() {
let body = br#"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "arguments": { "path": "/etc/passwd" } }
}"#;
assert!(parse_mcp_request(body).is_none());
}
#[test]
fn returns_none_on_malformed_json() {
assert!(parse_mcp_request(b"not json at all").is_none());
assert!(parse_mcp_request(b"{\"jsonrpc\":\"2.0\",\"method\":").is_none());
assert!(parse_mcp_request(b"").is_none());
}
#[test]
fn missing_arguments_defaults_to_json_null() {
let body = br#"{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "ping" }
}"#;
let call = parse_mcp_request(body).expect("missing arguments must still parse");
assert_eq!(call.tool_name, "ping");
assert_eq!(call.arguments, serde_json::Value::Null);
}
}