1use std::io::{ErrorKind, Read, Write};
7use std::net::{TcpStream, ToSocketAddrs};
8use std::os::unix::net::UnixStream;
9use std::path::Path;
10use std::time::{Duration, Instant};
11
12use serde_json::{Value, json};
13
14use crate::error::{Error, Result};
15
16pub const PROTOCOL_VERSIONS: [&str; 4] = ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26"];
18pub const MODERN_VERSIONS: [&str; 1] = ["2026-07-28"];
21pub const LEGACY_VERSIONS: [&str; 3] = ["2025-11-25", "2025-06-18", "2025-03-26"];
24
25const MAX_RESPONSE_BYTES: usize = 64 * 1024 * 1024;
26
27pub fn call_tool(socket: &Path, name: &str, args: Value, timeout: Duration) -> Result<Value> {
32 let r = rpc(
33 socket,
34 "tools/call",
35 json!({"name": name, "arguments": args}),
36 timeout,
37 )?;
38 unwrap_result(r)
39}
40
41fn unwrap_result(r: Value) -> Result<Value> {
43 let structured = r.get("structuredContent").cloned();
44 if r.get("isError").and_then(Value::as_bool) == Some(true) {
45 let s = structured.unwrap_or(Value::Null);
46 let text = r
47 .pointer("/content/0/text")
48 .and_then(Value::as_str)
49 .unwrap_or("tool failed");
50 return Err(Error::Remote {
51 code: s
52 .get("code")
53 .and_then(Value::as_str)
54 .unwrap_or("tool_error")
55 .to_string(),
56 message: s
57 .get("message")
58 .and_then(Value::as_str)
59 .unwrap_or(text)
60 .to_string(),
61 data: s.get("data").cloned().unwrap_or(Value::Null),
62 });
63 }
64 Ok(match structured {
66 Some(Value::Object(mut o)) if o.len() == 1 && o.contains_key("result") => {
67 o.remove("result").unwrap_or(Value::Null)
68 }
69 Some(v) => v,
70 None => r
71 .pointer("/content/0/text")
72 .and_then(Value::as_str)
73 .map(|t| serde_json::from_str(t).unwrap_or_else(|_| json!(t)))
74 .unwrap_or(Value::Null),
75 })
76}
77
78pub fn list_tools(socket: &Path, timeout: Duration) -> Result<Vec<Value>> {
80 let r = rpc(socket, "tools/list", json!({}), timeout)?;
81 match r.get("tools") {
82 Some(Value::Array(a)) => Ok(a.clone()),
83 _ => Err(Error::Protocol("tools/list: no tools array".into())),
84 }
85}
86
87fn rpc(socket: &Path, method: &str, mut params: Value, timeout: Duration) -> Result<Value> {
88 let version = MODERN_VERSIONS[0];
89 params["_meta"] = json!({
90 "io.modelcontextprotocol/protocolVersion": version,
91 "io.modelcontextprotocol/clientCapabilities": {},
92 "io.modelcontextprotocol/clientInfo": {"name": "isb", "version": env!("CARGO_PKG_VERSION")},
93 });
94 let body = serde_json::to_vec(
95 &json!({"jsonrpc": "2.0", "id": 1, "method": method, "params": params}),
96 )?;
97 let name = params.get("name").and_then(Value::as_str);
99 let mut headers = vec![("MCP-Protocol-Version", version), ("Mcp-Method", method)];
100 headers.extend(name.map(|n| ("Mcp-Name", n)));
101 let stream = UnixStream::connect(socket).map_err(|e| {
102 std::io::Error::new(
103 e.kind(),
104 format!("cannot connect to isb serve at {}: {e}", socket.display()),
105 )
106 })?;
107 let what = format!("{method} on {}", socket.display());
108 let (status, bytes) = exchange_with(
109 Stream::Unix(stream),
110 "POST",
111 "/mcp",
112 &body,
113 timeout,
114 &what,
115 &headers,
116 )?;
117 let v: Value = serde_json::from_slice(&bytes).map_err(|e| {
118 Error::Protocol(format!(
119 "{what}: HTTP {status}, undecodable body ({e}): {}",
120 String::from_utf8_lossy(&bytes[..bytes.len().min(200)])
121 ))
122 })?;
123 if let Some(err) = v.get("error") {
124 let code = err.get("code").and_then(Value::as_i64).unwrap_or(0);
125 let msg = err
126 .get("message")
127 .and_then(Value::as_str)
128 .unwrap_or("error")
129 .to_string();
130 return Err(if code == -32602 {
131 Error::Invalid(msg)
132 } else {
133 Error::Protocol(format!("{method}: {msg} ({code})"))
134 });
135 }
136 if status != 200 {
137 return Err(Error::Protocol(format!("{what}: HTTP {status}")));
138 }
139 v.get("result")
140 .cloned()
141 .ok_or_else(|| Error::Protocol(format!("{what}: response has no result")))
142}
143
144pub fn healthz(addr: &str, timeout: Duration) -> Result<(u16, Value)> {
146 let target = addr
147 .to_socket_addrs()?
148 .next()
149 .ok_or_else(|| Error::invalid(format!("{addr} does not resolve")))?;
150 let s = TcpStream::connect_timeout(&target, timeout)?;
151 let (status, body) = exchange(
152 Stream::Tcp(s),
153 "GET",
154 "/healthz",
155 b"",
156 timeout,
157 &format!("GET http://{addr}/healthz"),
158 )?;
159 Ok((status, serde_json::from_slice(&body).unwrap_or(Value::Null)))
160}
161
162enum Stream {
163 Tcp(TcpStream),
164 Unix(UnixStream),
165}
166
167impl Stream {
168 fn set_timeouts(&self, d: Duration) -> std::io::Result<()> {
169 match self {
170 Stream::Tcp(s) => {
171 s.set_read_timeout(Some(d))?;
172 s.set_write_timeout(Some(d))
173 }
174 Stream::Unix(s) => {
175 s.set_read_timeout(Some(d))?;
176 s.set_write_timeout(Some(d))
177 }
178 }
179 }
180
181 fn io(&mut self) -> &mut dyn ReadWrite {
182 match self {
183 Stream::Tcp(s) => s,
184 Stream::Unix(s) => s,
185 }
186 }
187}
188
189trait ReadWrite: Read + Write {}
190impl<T: Read + Write> ReadWrite for T {}
191
192fn exchange(
195 s: Stream,
196 method: &str,
197 path: &str,
198 body: &[u8],
199 timeout: Duration,
200 what: &str,
201) -> Result<(u16, Vec<u8>)> {
202 exchange_with(s, method, path, body, timeout, what, &[])
203}
204
205fn exchange_with(
207 mut s: Stream,
208 method: &str,
209 path: &str,
210 body: &[u8],
211 timeout: Duration,
212 what: &str,
213 extra: &[(&str, &str)],
214) -> Result<(u16, Vec<u8>)> {
215 let started = Instant::now();
216 let timed_out = || {
217 Error::Io(std::io::Error::new(
218 ErrorKind::TimedOut,
219 format!("isb serve did not answer {what} within {timeout:?}"),
220 ))
221 };
222 let to_err = |e: std::io::Error| {
223 if matches!(e.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) {
224 timed_out()
225 } else {
226 Error::Io(e)
227 }
228 };
229 s.set_timeouts(timeout)?;
230 let mut more = String::new();
231 for (k, v) in extra {
232 more.push_str(&format!("{k}: {v}\r\n"));
233 }
234 let head = format!(
235 "{method} {path} HTTP/1.1\r\nHost: localhost\r\nUser-Agent: isb/{}\r\n\
236 Content-Type: application/json\r\nAccept: application/json, text/event-stream\r\n\
237 Content-Length: {}\r\n{more}Connection: close\r\n\r\n",
238 env!("CARGO_PKG_VERSION"),
239 body.len()
240 );
241 s.io().write_all(head.as_bytes()).map_err(to_err)?;
242 s.io().write_all(body).map_err(to_err)?;
243 s.io().flush().map_err(to_err)?;
244
245 let mut buf = Vec::with_capacity(8192);
246 let mut chunk = [0u8; 16384];
247 loop {
248 let remaining = timeout.saturating_sub(started.elapsed());
249 if remaining.is_zero() {
250 return Err(timed_out());
251 }
252 let _ = s.set_timeouts(remaining);
255 match s.io().read(&mut chunk) {
256 Ok(0) => break,
257 Ok(n) => buf.extend_from_slice(&chunk[..n]),
258 Err(e) if e.kind() == ErrorKind::Interrupted => continue,
259 Err(e) => return Err(to_err(e)),
260 }
261 if buf.len() > MAX_RESPONSE_BYTES {
262 return Err(Error::Protocol(format!("{what}: response too large")));
263 }
264 }
265 let mut hs = [httparse::EMPTY_HEADER; 64];
266 let mut r = httparse::Response::new(&mut hs);
267 let n = match r.parse(&buf) {
268 Ok(httparse::Status::Complete(n)) => n,
269 Ok(httparse::Status::Partial) => {
270 return Err(Error::Protocol(format!("{what}: truncated response")));
271 }
272 Err(e) => return Err(Error::Protocol(format!("{what}: bad response: {e}"))),
273 };
274 let status = r.code.unwrap_or(0);
275 let length = r
276 .headers
277 .iter()
278 .find(|h| h.name.eq_ignore_ascii_case("content-length"))
279 .and_then(|h| {
280 std::str::from_utf8(h.value)
281 .ok()?
282 .trim()
283 .parse::<usize>()
284 .ok()
285 });
286 let mut body = buf.split_off(n);
287 if let Some(len) = length {
288 if body.len() < len {
289 return Err(Error::Protocol(format!("{what}: truncated response")));
290 }
291 body.truncate(len);
292 }
293 Ok((status, body))
294}