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