1use serde_json::Value;
34use std::io::{self, BufRead, Write};
35use std::time::Duration;
36
37#[derive(Clone)]
39pub struct ServiceConfig {
40 pub subject_prefix: String,
42 pub tool_prefix: String,
44}
45
46impl ServiceConfig {
47 pub fn new(subject_prefix: &str, tool_prefix: &str) -> Self {
48 ServiceConfig {
49 subject_prefix: subject_prefix.to_string(),
50 tool_prefix: tool_prefix.to_string(),
51 }
52 }
53}
54
55pub struct McpBridge {
59 nats_url: String,
60 services: Vec<ServiceConfig>,
61 timeout_ms: u64,
63}
64
65impl McpBridge {
66 pub fn new(nats_url: &str) -> Self {
67 McpBridge {
68 nats_url: nats_url.to_string(),
69 services: Vec::new(),
70 timeout_ms: 5000,
71 }
72 }
73
74 pub fn service(mut self, subject_prefix: &str, tool_prefix: &str) -> Self {
76 self.services
77 .push(ServiceConfig::new(subject_prefix, tool_prefix));
78 self
79 }
80
81 pub fn timeout_ms(mut self, ms: u64) -> Self {
83 self.timeout_ms = ms;
84 self
85 }
86
87 pub async fn run_stdio(self) -> Result<(), String> {
91 let client = async_nats::connect(&self.nats_url)
92 .await
93 .map_err(|e| format!("NATS connection failed: {e}"))?;
94
95 tracing::info!(url = %self.nats_url, services = self.services.len(), "MCP bridge ready");
96
97 let stdin = io::stdin();
98 let stdout = io::stdout();
99 let mut out = io::BufWriter::new(stdout.lock());
100
101 for line in stdin.lock().lines() {
102 let line = line.map_err(|e| format!("stdin read: {e}"))?;
103 let line = line.trim();
104 if line.is_empty() {
105 continue;
106 }
107
108 let request: Value = match serde_json::from_str(line) {
109 Ok(v) => v,
110 Err(e) => {
111 let err_resp = json_error(Value::Null, -32700, &format!("parse error: {e}"));
112 writeln!(
113 out,
114 "{}",
115 serde_json::to_string(&err_resp).unwrap_or_default()
116 )
117 .ok();
118 out.flush().ok();
119 continue;
120 }
121 };
122
123 let id = request.get("id").cloned().unwrap_or(Value::Null);
124 let method = request.get("method").and_then(|m| m.as_str()).unwrap_or("");
125 let params = request.get("params").cloned().unwrap_or(Value::Null);
126
127 let response = match method {
128 "initialize" => handle_initialize(id.clone()),
129 "tools/list" => self.handle_tools_list(&client, id.clone()).await,
130 "tools/call" => self.handle_tools_call(&client, id.clone(), ¶ms).await,
131 other => json_error(id.clone(), -32601, &format!("method not found: {other}")),
132 };
133
134 writeln!(
135 out,
136 "{}",
137 serde_json::to_string(&response).unwrap_or_default()
138 )
139 .map_err(|e| format!("stdout write: {e}"))?;
140 out.flush().map_err(|e| format!("stdout flush: {e}"))?;
141 }
142
143 Ok(())
144 }
145
146 async fn handle_tools_list(&self, client: &async_nats::Client, id: Value) -> Value {
148 let mut all_tools: Vec<Value> = Vec::new();
149
150 for svc in &self.services {
151 let tools_subject = format!("{}.tools", svc.subject_prefix);
152 let timeout = Duration::from_millis(self.timeout_ms);
153
154 match tokio::time::timeout(timeout, client.request(tools_subject, "{}".into())).await {
155 Ok(Ok(msg)) => {
156 if let Ok(resp) = serde_json::from_slice::<Value>(&msg.payload)
158 && let Some(tools) = resp.get("tools").and_then(|t| t.as_array())
159 {
160 for tool in tools {
161 let mcp_tool = serde_json::json!({
163 "name": tool["name"],
164 "description": tool["description"],
165 "inputSchema": tool["input_schema"]
166 });
167 all_tools.push(mcp_tool);
168 }
169 }
170 }
171 Ok(Err(e)) => {
172 tracing::warn!(subject = %svc.subject_prefix, error = %e, "tools fetch failed");
173 }
174 Err(_) => {
175 tracing::warn!(subject = %svc.subject_prefix, "tools fetch timed out");
176 }
177 }
178 }
179
180 json_result(id, serde_json::json!({ "tools": all_tools }))
181 }
182
183 async fn handle_tools_call(
185 &self,
186 client: &async_nats::Client,
187 id: Value,
188 params: &Value,
189 ) -> Value {
190 let tool_name = match params.get("name").and_then(|n| n.as_str()) {
191 Some(n) => n,
192 None => return json_error(id, -32602, "missing 'name' in params"),
193 };
194 let arguments = params
195 .get("arguments")
196 .cloned()
197 .unwrap_or(serde_json::json!({}));
198
199 let route = self.route_tool(tool_name);
201 let (subject, _svc) = match route {
202 Some(r) => r,
203 None => {
204 return json_error(id, -32602, &format!("unknown tool: {tool_name}"));
205 }
206 };
207
208 let payload = serde_json::to_vec(&arguments).unwrap_or_else(|_| b"{}".to_vec());
209 let timeout = Duration::from_millis(self.timeout_ms);
210
211 match tokio::time::timeout(timeout, client.request(subject.clone(), payload.into())).await {
212 Ok(Ok(msg)) => {
213 let content_text = String::from_utf8_lossy(&msg.payload).to_string();
214 json_result(
215 id,
216 serde_json::json!({
217 "content": [{"type": "text", "text": content_text}],
218 "isError": false
219 }),
220 )
221 }
222 Ok(Err(e)) => json_error(id, -32603, &format!("NATS error: {e}")),
223 Err(_) => json_error(id, -32603, &format!("timeout calling {subject}")),
224 }
225 }
226
227 fn route_tool(&self, tool_name: &str) -> Option<(String, &ServiceConfig)> {
231 for svc in &self.services {
232 if tool_name.starts_with(&svc.tool_prefix) {
233 let suffix = tool_name
234 .strip_prefix(&svc.tool_prefix)
235 .unwrap_or(tool_name);
236 let subject = format!("{}.{}", svc.subject_prefix, suffix);
237 return Some((subject, svc));
238 }
239 }
240 None
241 }
242}
243
244fn handle_initialize(id: Value) -> Value {
247 json_result(
248 id,
249 serde_json::json!({
250 "protocolVersion": "2024-11-05",
251 "capabilities": {
252 "tools": {}
253 },
254 "serverInfo": {
255 "name": "nusy-mcp-bridge",
256 "version": env!("CARGO_PKG_VERSION")
257 }
258 }),
259 )
260}
261
262fn json_result(id: Value, result: Value) -> Value {
263 serde_json::json!({
264 "jsonrpc": "2.0",
265 "id": id,
266 "result": result
267 })
268}
269
270fn json_error(id: Value, code: i64, message: &str) -> Value {
271 serde_json::json!({
272 "jsonrpc": "2.0",
273 "id": id,
274 "error": {
275 "code": code,
276 "message": message
277 }
278 })
279}
280
281#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn route_codegraph_tool() {
289 let bridge = McpBridge::new("nats://localhost:4222").service("codegraph.cmd", "code_");
290
291 let (subject, _) = bridge.route_tool("code_search").unwrap();
292 assert_eq!(subject, "codegraph.cmd.search");
293
294 let (subject, _) = bridge.route_tool("code_dependencies").unwrap();
295 assert_eq!(subject, "codegraph.cmd.dependencies");
296
297 let (subject, _) = bridge.route_tool("code_build").unwrap();
298 assert_eq!(subject, "codegraph.cmd.build");
299 }
300
301 #[test]
302 fn route_unknown_tool_returns_none() {
303 let bridge = McpBridge::new("nats://localhost:4222").service("codegraph.cmd", "code_");
304 assert!(bridge.route_tool("unknown_tool").is_none());
305 }
306
307 #[test]
308 fn route_multiple_services() {
309 let bridge = McpBridge::new("nats://localhost:4222")
310 .service("codegraph.cmd", "code_")
311 .service("kanban.cmd", "kb_");
312
313 let (subj, _) = bridge.route_tool("code_read").unwrap();
314 assert_eq!(subj, "codegraph.cmd.read");
315
316 let (subj, _) = bridge.route_tool("kb_create").unwrap();
317 assert_eq!(subj, "kanban.cmd.create");
318 }
319
320 #[test]
321 fn handle_initialize_returns_correct_version() {
322 let resp = handle_initialize(serde_json::json!(1));
323 assert_eq!(resp["jsonrpc"], "2.0");
324 assert_eq!(resp["id"], 1);
325 assert_eq!(resp["result"]["protocolVersion"], "2024-11-05");
326 assert!(resp["result"]["capabilities"]["tools"].is_object());
327 }
328
329 #[test]
330 fn json_result_shape() {
331 let r = json_result(serde_json::json!(42), serde_json::json!({"key": "val"}));
332 assert_eq!(r["jsonrpc"], "2.0");
333 assert_eq!(r["id"], 42);
334 assert_eq!(r["result"]["key"], "val");
335 }
336
337 #[test]
338 fn json_error_shape() {
339 let e = json_error(serde_json::json!(1), -32601, "not found");
340 assert_eq!(e["error"]["code"], -32601);
341 assert_eq!(e["error"]["message"], "not found");
342 }
343}