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,
})
}
pub fn is_unenforceable_tool_call(body: &[u8]) -> bool {
let Ok(value) = serde_json::from_slice::<serde_json::Value>(body) else {
return false;
};
match &value {
serde_json::Value::Array(elements) => elements.iter().any(mentions_tools_call),
serde_json::Value::Object(_) => mentions_tools_call(&value),
_ => false,
}
}
fn mentions_tools_call(value: &serde_json::Value) -> bool {
value.get("method").and_then(serde_json::Value::as_str) == Some(MCP_TOOLS_CALL_METHOD)
}
#[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);
}
#[test]
fn one_element_batch_tools_call_is_flagged_unenforceable() {
let body = br#"[
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "execute_bash", "arguments": { "cmd": "rm -rf /" } }
}
]"#;
assert!(
parse_mcp_request(body).is_none(),
"batch must not parse as a single call"
);
assert!(
is_unenforceable_tool_call(body),
"a one-element batch tools/call must be flagged so it is fail-closed, not forwarded"
);
}
#[test]
fn multi_element_batch_with_a_tools_call_is_flagged() {
let body = br#"[
{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}},
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_file"}}
]"#;
assert!(is_unenforceable_tool_call(body));
}
#[test]
fn object_tools_call_that_fails_strict_extraction_is_flagged() {
let wrong_version = br#"{"jsonrpc":"1.0","id":1,"method":"tools/call","params":{"name":"x"}}"#;
assert!(parse_mcp_request(wrong_version).is_none());
assert!(is_unenforceable_tool_call(wrong_version));
let missing_name = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{}}"#;
assert!(parse_mcp_request(missing_name).is_none());
assert!(is_unenforceable_tool_call(missing_name));
}
#[test]
fn non_tool_call_traffic_is_not_flagged() {
assert!(!is_unenforceable_tool_call(
br#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#
));
assert!(!is_unenforceable_tool_call(
br#"[{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}]"#
));
assert!(!is_unenforceable_tool_call(br#"{"hello":"world"}"#));
assert!(!is_unenforceable_tool_call(b"not json at all"));
assert!(!is_unenforceable_tool_call(b""));
assert!(!is_unenforceable_tool_call(b"[1, 2, 3]"));
}
}