use crate::config::{Config, SUPPORTED_PROTOCOL_VERSIONS};
use crate::mcp::McpPluginServer;
use serde_json::{Value, json};
use std::sync::Arc;
pub struct McpRequestHandler {
server: Arc<McpPluginServer>,
config: Config,
}
impl McpRequestHandler {
pub const fn new(server: Arc<McpPluginServer>, config: Config) -> Self {
Self { server, config }
}
pub async fn handle(&self, req: &Value) -> Option<Value> {
let is_notification = is_jsonrpc_notification(req);
let id = req.get("id").cloned().unwrap_or(Value::Null);
if req.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
return Some(error_response(
&id,
-32600,
"Invalid request: expected jsonrpc='2.0'",
));
}
let Some(method) = req.get("method").and_then(Value::as_str) else {
return Some(error_response(
&id,
-32600,
"Invalid request: missing string 'method'",
));
};
let response = match method {
"initialize" => self.handle_initialize(&id, req),
"initialized" | "notifications/initialized" | "ping" => ok_response(&id, &json!({})),
"tools/list" => self.handle_tools_list(&id),
"tools/call" => self.handle_tools_call(&id, req).await,
other => error_response(&id, -32601, &format!("Method not found: {other}")),
};
if is_notification {
None
} else {
Some(response)
}
}
fn handle_initialize(&self, id: &Value, req: &Value) -> Value {
let requested_version = req
.get("params")
.and_then(|p| p.get("protocolVersion"))
.and_then(Value::as_str);
let protocol_version = match requested_version {
Some(v) if SUPPORTED_PROTOCOL_VERSIONS.contains(&v) => v,
Some(v) => {
return error_response(
id,
-32602,
&format!(
"Unsupported protocolVersion: {v}. Supported: {}",
SUPPORTED_PROTOCOL_VERSIONS.join(", ")
),
);
}
None => SUPPORTED_PROTOCOL_VERSIONS
.first()
.copied()
.unwrap_or("2024-11-05"),
};
ok_response(
id,
&json!({
"protocolVersion": protocol_version,
"capabilities": {
"tools": { "listChanged": false }
},
"serverInfo": {
"name": self.config.server_name,
"version": env!("CARGO_PKG_VERSION")
}
}),
)
}
fn handle_tools_list(&self, id: &Value) -> Value {
let tools = self.server.tools_list();
ok_response(id, &json!({ "tools": tools }))
}
async fn handle_tools_call(&self, id: &Value, req: &Value) -> Value {
let Some(params) = req.get("params") else {
return error_response(id, -32602, "Missing 'params'");
};
let Some(name) = params.get("name").and_then(Value::as_str) else {
return error_response(id, -32602, "Missing tool 'name'");
};
let name = name.to_string();
let args = params.get("arguments").cloned().unwrap_or(Value::Null);
let result = self.server.handle_tool_call(&name, &args).await;
ok_response(id, &result)
}
}
fn ok_response(id: &Value, result: &Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
fn error_response(id: &Value, code: i32, message: &str) -> Value {
json!({
"jsonrpc": "2.0",
"id": id,
"error": { "code": code, "message": message }
})
}
fn is_jsonrpc_notification(req: &Value) -> bool {
req.is_object()
&& req.get("jsonrpc").and_then(Value::as_str) == Some("2.0")
&& req.get("id").is_none()
&& req.get("method").and_then(Value::as_str).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_jsonrpc_notification() {
let notif = json!({
"jsonrpc": "2.0",
"method": "ping"
});
assert!(is_jsonrpc_notification(¬if));
let request = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "ping"
});
assert!(!is_jsonrpc_notification(&request));
let bad = json!({ "method": "ping" });
assert!(!is_jsonrpc_notification(&bad));
}
#[test]
fn test_ok_response() {
let resp = ok_response(&json!(1), &json!({"status": "ok"}));
assert_eq!(resp.get("jsonrpc").and_then(Value::as_str), Some("2.0"));
assert_eq!(resp.get("id").and_then(Value::as_u64), Some(1));
assert_eq!(
resp.pointer("/result/status").and_then(Value::as_str),
Some("ok")
);
}
#[test]
fn test_error_response() {
let resp = error_response(&json!(2), -32601, "Not found");
assert_eq!(resp.get("jsonrpc").and_then(Value::as_str), Some("2.0"));
assert_eq!(resp.get("id").and_then(Value::as_u64), Some(2));
assert_eq!(
resp.pointer("/error/code").and_then(Value::as_i64),
Some(-32601)
);
assert_eq!(
resp.pointer("/error/message").and_then(Value::as_str),
Some("Not found")
);
}
}