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