1use crate::agent_registry::McpTransport;
14use std::io::{BufRead, BufReader, Write};
15use std::time::Duration;
16
17pub const DEFAULT_INFER_TOOL: &str = "llm_infer";
19
20fn build_infer_request(
25 infer_tool: &str,
26 model: Option<&str>,
27 system: Option<&str>,
28 prompt: &str,
29) -> serde_json::Value {
30 let full = match system {
31 Some(sys) if !sys.trim().is_empty() => format!("{sys}\n\n{prompt}"),
32 _ => prompt.to_string(),
33 };
34 let mut args = serde_json::Map::new();
35 args.insert("prompt".into(), serde_json::json!(full));
36 if let Some(m) = model {
37 if !m.is_empty() {
38 args.insert("model".into(), serde_json::json!(m));
39 }
40 }
41 serde_json::json!({
42 "jsonrpc": "2.0",
43 "id": 1,
44 "method": "tools/call",
45 "params": { "name": infer_tool, "arguments": serde_json::Value::Object(args) }
46 })
47}
48
49fn parse_infer_response(resp: &serde_json::Value) -> Result<String, String> {
51 if let Some(err) = resp.get("error") {
52 let msg = err
53 .get("message")
54 .and_then(|v| v.as_str())
55 .unwrap_or("remote MCP error");
56 return Err(format!("remote MCP error: {msg}"));
57 }
58 let result = resp
59 .get("result")
60 .ok_or_else(|| "remote MCP response missing `result`".to_string())?;
61
62 if let Some(content) = result.get("content").and_then(|c| c.as_array()) {
64 let mut out = String::new();
65 for part in content {
66 if let Some(t) = part.get("text").and_then(|v| v.as_str()) {
67 out.push_str(t);
68 }
69 }
70 if !out.is_empty() {
71 return Ok(out);
72 }
73 }
74 if let Some(t) = result.as_str() {
76 return Ok(t.to_string());
77 }
78 for k in ["text", "output", "completion", "response"] {
79 if let Some(t) = result.get(k).and_then(|v| v.as_str()) {
80 return Ok(t.to_string());
81 }
82 }
83 if result
84 .get("isError")
85 .and_then(|v| v.as_bool())
86 .unwrap_or(false)
87 {
88 return Err("remote MCP tool reported an error".to_string());
89 }
90 Err("remote MCP response had no text content".to_string())
91}
92
93pub fn remote_mcp_infer(
98 transport: &McpTransport,
99 infer_tool: Option<&str>,
100 model: Option<&str>,
101 system: Option<&str>,
102 prompt: &str,
103) -> Result<String, String> {
104 let tool = infer_tool.unwrap_or(DEFAULT_INFER_TOOL);
105 let req = build_infer_request(tool, model, system, prompt);
106 let resp = match transport {
107 McpTransport::Tcp { host, port } => call_tcp(host, *port, &req)?,
108 McpTransport::Stdio { command, args } => call_stdio(command, args, &req)?,
109 McpTransport::Http { url, credential_id } => call_http(url, credential_id.as_deref(), &req)?,
110 };
111 parse_infer_response(&resp)
112}
113
114fn call_tcp(host: &str, port: u16, req: &serde_json::Value) -> Result<serde_json::Value, String> {
115 use std::net::TcpStream;
116 let stream =
117 TcpStream::connect((host, port)).map_err(|e| format!("connect {host}:{port}: {e}"))?;
118 stream.set_read_timeout(Some(Duration::from_secs(120))).ok();
119 stream.set_write_timeout(Some(Duration::from_secs(30))).ok();
120 let mut writer = stream.try_clone().map_err(|e| e.to_string())?;
121 let mut reader = BufReader::new(stream);
122 let line = serde_json::to_string(req).map_err(|e| e.to_string())?;
123 writer
124 .write_all(line.as_bytes())
125 .map_err(|e| e.to_string())?;
126 writer.write_all(b"\n").map_err(|e| e.to_string())?;
127 writer.flush().ok();
128 let mut buf = String::new();
130 for _ in 0..100 {
131 buf.clear();
132 let n = reader
133 .read_line(&mut buf)
134 .map_err(|e| format!("read: {e}"))?;
135 if n == 0 {
136 break;
137 }
138 let t = buf.trim();
139 if t.is_empty() {
140 continue;
141 }
142 if let Ok(v) = serde_json::from_str::<serde_json::Value>(t) {
143 if v.get("result").is_some() || v.get("error").is_some() {
144 return Ok(v);
145 }
146 }
147 }
148 Err("no JSON-RPC response from remote MCP (TCP)".into())
149}
150
151fn call_stdio(
152 command: &str,
153 args: &[String],
154 req: &serde_json::Value,
155) -> Result<serde_json::Value, String> {
156 use std::process::{Command, Stdio};
157 let mut child = Command::new(command)
158 .args(args)
159 .stdin(Stdio::piped())
160 .stdout(Stdio::piped())
161 .stderr(Stdio::null())
162 .spawn()
163 .map_err(|e| format!("spawn {command}: {e}"))?;
164 {
165 let stdin = child.stdin.as_mut().ok_or("no stdin on MCP child")?;
166 let line = serde_json::to_string(req).map_err(|e| e.to_string())?;
167 stdin
168 .write_all(line.as_bytes())
169 .map_err(|e| e.to_string())?;
170 stdin.write_all(b"\n").map_err(|e| e.to_string())?;
171 stdin.flush().ok();
172 }
173 let stdout = child.stdout.take().ok_or("no stdout on MCP child")?;
174 let mut reader = BufReader::new(stdout);
175 let mut buf = String::new();
176 let mut found = None;
177 for _ in 0..200 {
178 buf.clear();
179 let n = reader
180 .read_line(&mut buf)
181 .map_err(|e| format!("read: {e}"))?;
182 if n == 0 {
183 break;
184 }
185 let t = buf.trim();
186 if t.is_empty() {
187 continue;
188 }
189 if let Ok(v) = serde_json::from_str::<serde_json::Value>(t) {
190 if v.get("result").is_some() || v.get("error").is_some() {
191 found = Some(v);
192 break;
193 }
194 }
195 }
196 let _ = child.kill();
197 let _ = child.wait();
198 found.ok_or_else(|| "no JSON-RPC response from stdio MCP server".to_string())
199}
200
201fn call_http(
202 url: &str,
203 credential_id: Option<&str>,
204 req: &serde_json::Value,
205) -> Result<serde_json::Value, String> {
206 let client = reqwest::blocking::Client::builder()
207 .timeout(Duration::from_secs(180))
208 .build()
209 .map_err(|e| e.to_string())?;
210 let mut request = client
211 .post(url)
212 .header("content-type", "application/json")
213 .header("accept", "application/json")
214 .json(req);
215 if let Some(connection) = credential_id.filter(|value| !value.trim().is_empty()) {
216 let secret = crate::provider_credentials::bearer_credential(connection)?;
219 request = request.bearer_auth(secret);
220 }
221 let resp = request.send()
222 .map_err(|e| format!("http post: {e}"))?;
223 let status = resp.status();
224 let text = resp.text().map_err(|e| e.to_string())?;
225 if !status.is_success() {
226 let snippet: String = text.chars().take(240).collect();
227 return Err(format!("remote MCP HTTP {status}: {snippet}"));
228 }
229 serde_json::from_str(&text).map_err(|e| format!("parse http json: {e}"))
230}
231
232pub fn remote_mcp_probe(transport: &McpTransport) -> Result<usize, String> {
235 let req = serde_json::json!({
236 "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}
237 });
238 let response = match transport {
239 McpTransport::Tcp { host, port } => call_tcp(host, *port, &req)?,
240 McpTransport::Stdio { command, args } => call_stdio(command, args, &req)?,
241 McpTransport::Http { url, credential_id } => call_http(url, credential_id.as_deref(), &req)?,
242 };
243 if let Some(error) = response.get("error") {
244 let message = error.get("message").and_then(|value| value.as_str()).unwrap_or("MCP error");
245 return Err(format!("MCP tools/list failed: {message}"));
246 }
247 let tools = response
248 .get("result")
249 .and_then(|result| result.get("tools"))
250 .and_then(|tools| tools.as_array())
251 .ok_or_else(|| "MCP tools/list response had no tools array".to_string())?;
252 Ok(tools.len())
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn request_is_valid_tools_call() {
261 let req = build_infer_request("llm_chat", Some("phi-3"), Some("Be terse."), "hi");
262 assert_eq!(req["jsonrpc"], "2.0");
263 assert_eq!(req["method"], "tools/call");
264 assert_eq!(req["params"]["name"], "llm_chat");
265 assert_eq!(req["params"]["arguments"]["model"], "phi-3");
266 let prompt = req["params"]["arguments"]["prompt"].as_str().unwrap();
267 assert!(prompt.starts_with("Be terse."));
268 assert!(prompt.ends_with("hi"));
269 }
270
271 #[test]
272 fn request_omits_empty_model_and_system() {
273 let req = build_infer_request(DEFAULT_INFER_TOOL, None, None, "just this");
274 assert!(req["params"]["arguments"].get("model").is_none());
275 assert_eq!(req["params"]["arguments"]["prompt"], "just this");
276 }
277
278 #[test]
279 fn parses_mcp_content_array() {
280 let resp = serde_json::json!({
281 "jsonrpc": "2.0", "id": 1,
282 "result": { "content": [ {"type":"text","text":"Hello"}, {"type":"text","text":", world"} ] }
283 });
284 assert_eq!(parse_infer_response(&resp).unwrap(), "Hello, world");
285 }
286
287 #[test]
288 fn parses_simple_fallbacks() {
289 let a = serde_json::json!({ "result": "plain string" });
290 assert_eq!(parse_infer_response(&a).unwrap(), "plain string");
291 let b = serde_json::json!({ "result": { "text": "keyed text" } });
292 assert_eq!(parse_infer_response(&b).unwrap(), "keyed text");
293 }
294
295 #[test]
296 fn surfaces_errors() {
297 let e = serde_json::json!({ "error": { "code": -32000, "message": "boom" } });
298 assert!(parse_infer_response(&e).unwrap_err().contains("boom"));
299 let ie = serde_json::json!({ "result": { "isError": true, "content": [] } });
300 assert!(parse_infer_response(&ie).is_err());
301 }
302}