stygian_plugin/mcp/
handler.rs1use crate::config::{Config, SUPPORTED_PROTOCOL_VERSIONS};
7use crate::mcp::McpPluginServer;
8use serde_json::{Value, json};
9use std::sync::Arc;
10
11pub struct McpRequestHandler {
13 server: Arc<McpPluginServer>,
14 config: Config,
15}
16
17impl McpRequestHandler {
18 pub const fn new(server: Arc<McpPluginServer>, config: Config) -> Self {
20 Self { server, config }
21 }
22
23 pub async fn handle(&self, req: &Value) -> Option<Value> {
28 let is_notification = is_jsonrpc_notification(req);
30 let id = req.get("id").cloned().unwrap_or(Value::Null);
32
33 if req.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
35 return Some(error_response(
36 &id,
37 -32600,
38 "Invalid request: expected jsonrpc='2.0'",
39 ));
40 }
41
42 let Some(method) = req.get("method").and_then(Value::as_str) else {
43 return Some(error_response(
44 &id,
45 -32600,
46 "Invalid request: missing string 'method'",
47 ));
48 };
49
50 let response = match method {
52 "initialize" => self.handle_initialize(&id, req),
53 "initialized" | "notifications/initialized" | "ping" => ok_response(&id, &json!({})),
54 "tools/list" => self.handle_tools_list(&id),
55 "tools/call" => self.handle_tools_call(&id, req).await,
56 other => error_response(&id, -32601, &format!("Method not found: {other}")),
57 };
58
59 if is_notification {
61 None
62 } else {
63 Some(response)
64 }
65 }
66
67 fn handle_initialize(&self, id: &Value, req: &Value) -> Value {
69 let requested_version = req
70 .get("params")
71 .and_then(|p| p.get("protocolVersion"))
72 .and_then(Value::as_str);
73
74 let protocol_version = match requested_version {
75 Some(v) if SUPPORTED_PROTOCOL_VERSIONS.contains(&v) => v,
76 Some(v) => {
77 return error_response(
78 id,
79 -32602,
80 &format!(
81 "Unsupported protocolVersion: {v}. Supported: {}",
82 SUPPORTED_PROTOCOL_VERSIONS.join(", ")
83 ),
84 );
85 }
86 None => SUPPORTED_PROTOCOL_VERSIONS
87 .first()
88 .copied()
89 .unwrap_or("2024-11-05"),
90 };
91
92 ok_response(
93 id,
94 &json!({
95 "protocolVersion": protocol_version,
96 "capabilities": {
97 "tools": { "listChanged": false }
98 },
99 "serverInfo": {
100 "name": self.config.server_name,
101 "version": env!("CARGO_PKG_VERSION")
102 }
103 }),
104 )
105 }
106
107 fn handle_tools_list(&self, id: &Value) -> Value {
109 let tools = self.server.tools_list();
110 ok_response(id, &json!({ "tools": tools }))
111 }
112
113 async fn handle_tools_call(&self, id: &Value, req: &Value) -> Value {
115 let Some(params) = req.get("params") else {
116 return error_response(id, -32602, "Missing 'params'");
117 };
118
119 let Some(name) = params.get("name").and_then(Value::as_str) else {
120 return error_response(id, -32602, "Missing tool 'name'");
121 };
122 let name = name.to_string();
123
124 let args = params.get("arguments").cloned().unwrap_or(Value::Null);
125
126 let result = self.server.handle_tool_call(&name, &args).await;
128
129 ok_response(id, &result)
131 }
132}
133
134fn ok_response(id: &Value, result: &Value) -> Value {
138 json!({ "jsonrpc": "2.0", "id": id, "result": result })
139}
140
141fn error_response(id: &Value, code: i32, message: &str) -> Value {
143 json!({
144 "jsonrpc": "2.0",
145 "id": id,
146 "error": { "code": code, "message": message }
147 })
148}
149
150fn is_jsonrpc_notification(req: &Value) -> bool {
152 req.is_object()
153 && req.get("jsonrpc").and_then(Value::as_str) == Some("2.0")
154 && req.get("id").is_none()
155 && req.get("method").and_then(Value::as_str).is_some()
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_is_jsonrpc_notification() {
164 let notif = json!({
166 "jsonrpc": "2.0",
167 "method": "ping"
168 });
169 assert!(is_jsonrpc_notification(¬if));
170
171 let request = json!({
173 "jsonrpc": "2.0",
174 "id": 1,
175 "method": "ping"
176 });
177 assert!(!is_jsonrpc_notification(&request));
178
179 let bad = json!({ "method": "ping" });
181 assert!(!is_jsonrpc_notification(&bad));
182 }
183
184 #[test]
185 fn test_ok_response() {
186 let resp = ok_response(&json!(1), &json!({"status": "ok"}));
187 assert_eq!(resp.get("jsonrpc").and_then(Value::as_str), Some("2.0"));
188 assert_eq!(resp.get("id").and_then(Value::as_u64), Some(1));
189 assert_eq!(
190 resp.pointer("/result/status").and_then(Value::as_str),
191 Some("ok")
192 );
193 }
194
195 #[test]
196 fn test_error_response() {
197 let resp = error_response(&json!(2), -32601, "Not found");
198 assert_eq!(resp.get("jsonrpc").and_then(Value::as_str), Some("2.0"));
199 assert_eq!(resp.get("id").and_then(Value::as_u64), Some(2));
200 assert_eq!(
201 resp.pointer("/error/code").and_then(Value::as_i64),
202 Some(-32601)
203 );
204 assert_eq!(
205 resp.pointer("/error/message").and_then(Value::as_str),
206 Some("Not found")
207 );
208 }
209}