1use std::collections::{BTreeMap, BTreeSet};
18use std::io::{BufRead, BufReader, Read, Write};
19use std::net::TcpStream;
20use std::process::{Child, Command, Stdio};
21
22use serde_json::{json, Value as Json};
23
24use crate::cap::ToolPolicy;
25use crate::error::{Error, Result};
26use crate::kernel::Provider;
27use crate::value::Response;
28use crate::value::{Arg, Call, Value};
29
30pub fn json_to_value(value: &Json) -> Value {
32 match value {
33 Json::Null => Value::Null,
34 Json::Bool(flag) => Value::Bool(*flag),
35 Json::Number(number) => {
36 if let Some(int) = number.as_i64() {
37 Value::Int(int)
38 } else {
39 Value::Float(number.as_f64().unwrap_or(0.0))
40 }
41 }
42 Json::String(text) => Value::Text(text.clone()),
43 Json::Array(items) => Value::List(items.iter().map(json_to_value).collect()),
44 Json::Object(entries) => Value::Map(
45 entries
46 .iter()
47 .map(|(key, value)| (key.clone(), json_to_value(value)))
48 .collect(),
49 ),
50 }
51}
52
53pub fn value_to_json(value: &Value) -> Json {
55 match value {
56 Value::Null => Json::Null,
57 Value::Bool(flag) => Json::Bool(*flag),
58 Value::Int(number) => json!(number),
59 Value::Float(number) => json!(number),
60 Value::Text(text) => Json::String(text.clone()),
61 Value::List(items) => Json::Array(items.iter().map(value_to_json).collect()),
62 Value::Map(entries) => Json::Object(
63 entries
64 .iter()
65 .map(|(key, value)| (key.clone(), value_to_json(value)))
66 .collect(),
67 ),
68 Value::Secret => Json::String("***SECRET***".to_string()),
69 Value::Module(bytes) => json!({ "$module_bytes": bytes.len() }),
70 Value::Path(path) => Json::String(path.clone()),
71 }
72}
73
74pub trait McpTransport {
80 fn send_line(&mut self, line: &str) -> Result<()>;
82 fn recv_line(&mut self) -> Result<String>;
84}
85
86pub struct MemoryTransport {
88 responses: BTreeMap<String, String>,
89 queue: Vec<String>,
90 sent: Vec<String>,
91}
92
93impl MemoryTransport {
94 pub fn new(responses: BTreeMap<String, String>) -> Self {
96 Self {
97 responses,
98 queue: Vec::new(),
99 sent: Vec::new(),
100 }
101 }
102
103 pub fn sent(&self) -> &[String] {
105 &self.sent
106 }
107}
108
109impl McpTransport for MemoryTransport {
110 fn send_line(&mut self, line: &str) -> Result<()> {
111 self.sent.push(line.to_string());
112 let request: Json = serde_json::from_str(line).map_err(|_| Error::Provider {
113 code: "mcp/bad-json".to_string(),
114 })?;
115 let method = request.get("method").and_then(Json::as_str).unwrap_or("");
116 let id = request.get("id").cloned().unwrap_or(Json::Null);
117 let response = self.responses.get(method).cloned().unwrap_or_else(|| {
118 json!({
119 "jsonrpc": "2.0",
120 "id": id,
121 "error": { "code": -32601, "message": "method not found" }
122 })
123 .to_string()
124 });
125 self.queue.push(response);
126 Ok(())
127 }
128
129 fn recv_line(&mut self) -> Result<String> {
130 if self.queue.is_empty() {
131 return Err(Error::Provider {
132 code: "mcp/no-response".to_string(),
133 });
134 }
135 Ok(self.queue.remove(0))
136 }
137}
138
139pub struct StdioTransport {
141 child: Child,
142 stdin: std::process::ChildStdin,
143 stdout: BufReader<std::process::ChildStdout>,
144}
145
146impl StdioTransport {
147 pub fn spawn(program: &str, args: &[&str], env: &BTreeMap<String, String>) -> Result<Self> {
150 let mut command = Command::new(program);
151 command
152 .args(args)
153 .stdin(Stdio::piped())
154 .stdout(Stdio::piped())
155 .stderr(Stdio::null())
156 .env_clear();
157 for (key, value) in env {
158 command.env(key, value);
159 }
160 let mut child = command.spawn().map_err(|error| Error::Provider {
161 code: format!("mcp/spawn:{error}"),
162 })?;
163 let stdin = child.stdin.take().ok_or(Error::Provider {
164 code: "mcp/stdin".to_string(),
165 })?;
166 let stdout = child.stdout.take().ok_or(Error::Provider {
167 code: "mcp/stdout".to_string(),
168 })?;
169 Ok(Self {
170 child,
171 stdin,
172 stdout: BufReader::new(stdout),
173 })
174 }
175}
176
177impl McpTransport for StdioTransport {
178 fn send_line(&mut self, line: &str) -> Result<()> {
179 self.stdin
180 .write_all(line.as_bytes())
181 .and_then(|_| self.stdin.write_all(b"\n"))
182 .and_then(|_| self.stdin.flush())
183 .map_err(|error| Error::Provider {
184 code: format!("mcp/write:{error}"),
185 })
186 }
187
188 fn recv_line(&mut self) -> Result<String> {
189 let mut line = String::new();
190 let read = self
191 .stdout
192 .read_line(&mut line)
193 .map_err(|error| Error::Provider {
194 code: format!("mcp/read:{error}"),
195 })?;
196 if read == 0 {
197 return Err(Error::Provider {
198 code: "mcp/eof".to_string(),
199 });
200 }
201 Ok(line.trim_end().to_string())
202 }
203}
204
205impl Drop for StdioTransport {
206 fn drop(&mut self) {
207 let _ = self.child.kill();
208 let _ = self.child.wait();
209 }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct McpToolDef {
215 pub name: String,
217 pub description: String,
219}
220
221pub struct McpSession<T: McpTransport> {
227 transport: T,
228 tools: Vec<McpToolDef>,
229 next_id: u64,
230 pub calls: u64,
233}
234
235impl<T: McpTransport> McpSession<T> {
236 pub fn handshake(transport: T) -> Result<Self> {
238 let mut session = McpSession {
239 transport,
240 tools: Vec::new(),
241 next_id: 0,
242 calls: 0,
243 };
244 let initialize = session.request(
245 "initialize",
246 json!({
247 "protocolVersion": "2025-06-18",
248 "capabilities": {},
249 "clientInfo": { "name": "active-rpc", "version": env!("CARGO_PKG_VERSION") }
250 }),
251 )?;
252 if initialize.get("error").is_some() {
253 return Err(Error::Provider {
254 code: "mcp/initialize".to_string(),
255 });
256 }
257 let list = session.request("tools/list", json!({}))?;
258 if let Some(items) = list
259 .get("result")
260 .and_then(|result| result.get("tools"))
261 .and_then(Json::as_array)
262 {
263 for item in items {
264 let name = item.get("name").and_then(Json::as_str).unwrap_or("");
265 if name.is_empty() {
266 continue;
267 }
268 session.tools.push(McpToolDef {
269 name: name.to_string(),
270 description: item
271 .get("description")
272 .and_then(Json::as_str)
273 .unwrap_or("")
274 .to_string(),
275 });
276 }
277 }
278 Ok(session)
279 }
280
281 fn request(&mut self, method: &str, params: Json) -> Result<Json> {
282 self.next_id += 1;
283 let payload = json!({
284 "jsonrpc": "2.0",
285 "id": self.next_id,
286 "method": method,
287 "params": params,
288 });
289 self.transport.send_line(&payload.to_string())?;
290 let line = self.transport.recv_line()?;
291 serde_json::from_str(&line).map_err(|_| Error::Provider {
292 code: "mcp/bad-response".to_string(),
293 })
294 }
295
296 pub fn tools(&self) -> &[McpToolDef] {
298 &self.tools
299 }
300
301 pub fn policy(&self) -> ToolPolicy {
303 let mut policy = ToolPolicy::default();
304 for tool in &self.tools {
305 policy.allowed.insert(tool.name.clone());
306 }
307 policy.max_arg_bytes = 1 << 20;
308 policy
309 }
310}
311
312impl<T: McpTransport> Provider for McpSession<T> {
313 fn name(&self) -> &'static str {
314 "mcp"
315 }
316
317 fn methods(&self) -> Vec<String> {
318 self.tools.iter().map(|tool| tool.name.clone()).collect()
319 }
320
321 fn mutating(&self, _method: &str) -> bool {
322 true
323 }
324
325 fn invoke(&mut self, object: u64, call: &Call) -> Result<Response> {
326 self.calls += 1;
327 let tool = self
328 .tools
329 .get(object as usize)
330 .ok_or(Error::InvalidArgument("unknown MCP tool"))?;
331 if tool.name != call.method {
332 return Err(Error::MethodNotGranted {
333 method: call.method.clone(),
334 });
335 }
336 let mut arguments = serde_json::Map::new();
337 for (index, arg) in call.args.iter().enumerate() {
338 if let Arg::Value(value) = arg {
339 arguments.insert(format!("arg{index}"), value_to_json(value));
340 }
341 }
342 let response = self.request(
343 "tools/call",
344 json!({ "name": tool.name, "arguments": Json::Object(arguments) }),
345 )?;
346 if let Some(error) = response.get("error") {
347 return Err(Error::Provider {
348 code: format!(
349 "mcp/{}",
350 error.get("code").and_then(Json::as_i64).unwrap_or(0)
351 ),
352 });
353 }
354 let result = response.get("result").cloned().unwrap_or(Json::Null);
355 Ok(Response::Value(json_to_value(&result)))
356 }
357}
358
359#[derive(Debug)]
369pub struct HttpTool {
370 pub base: String,
372 pub allow: BTreeSet<(String, String)>,
374 pub max_body: usize,
376 pub max_request: usize,
378 pub calls: u64,
380}
381
382impl HttpTool {
383 pub fn new(base: impl Into<String>, allow: &[(&str, &str)]) -> Self {
385 Self {
386 base: base.into(),
387 allow: allow
388 .iter()
389 .map(|(method, path)| (method.to_string(), path.to_string()))
390 .collect(),
391 max_body: 1 << 20,
392 max_request: 1 << 20,
393 calls: 0,
394 }
395 }
396
397 pub fn policy(&self) -> ToolPolicy {
399 ToolPolicy {
400 allowed: ["get", "post", "request"]
401 .iter()
402 .map(|method| method.to_string())
403 .collect(),
404 max_arg_bytes: self.max_request as u64,
405 allow_process: false,
406 }
407 }
408
409 fn perform(&self, method: &str, path: &str, body: &[u8]) -> Result<(u16, Vec<u8>)> {
410 let base = self
411 .base
412 .strip_prefix("http://")
413 .ok_or(Error::ToolPolicy("only http:// endpoints are supported"))?;
414 let mut stream = TcpStream::connect(base).map_err(|error| Error::Provider {
415 code: format!("http/connect:{error}"),
416 })?;
417 stream
418 .set_read_timeout(Some(std::time::Duration::from_secs(10)))
419 .ok();
420 let head = format!(
421 "{method} {path} HTTP/1.1\r\nhost: {base}\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
422 body.len()
423 );
424 stream
425 .write_all(head.as_bytes())
426 .and_then(|_| stream.write_all(body))
427 .and_then(|_| stream.flush())
428 .map_err(|error| Error::Provider {
429 code: format!("http/write:{error}"),
430 })?;
431 let mut reader = BufReader::new(stream);
432 let mut status_line = String::new();
433 reader
434 .read_line(&mut status_line)
435 .map_err(|error| Error::Provider {
436 code: format!("http/read:{error}"),
437 })?;
438 let status: u16 = status_line
439 .split_whitespace()
440 .nth(1)
441 .and_then(|code| code.parse().ok())
442 .ok_or(Error::Provider {
443 code: "http/status".to_string(),
444 })?;
445 let mut content_length: Option<usize> = None;
446 loop {
447 let mut line = String::new();
448 let read = reader
449 .read_line(&mut line)
450 .map_err(|error| Error::Provider {
451 code: format!("http/read:{error}"),
452 })?;
453 if read == 0 || line.trim().is_empty() {
454 break;
455 }
456 if let Some((name, value)) = line.split_once(':') {
457 if name.eq_ignore_ascii_case("content-length") {
458 content_length = value.trim().parse().ok();
459 }
460 }
461 }
462 let mut body = Vec::new();
463 match content_length {
464 Some(length) => {
465 if length > self.max_body {
466 return Err(Error::ToolPolicy("response too large"));
467 }
468 body.resize(length, 0);
469 reader
470 .read_exact(&mut body)
471 .map_err(|error| Error::Provider {
472 code: format!("http/body:{error}"),
473 })?;
474 }
475 None => {
476 let mut rest = Vec::new();
477 reader
478 .take(self.max_body as u64 + 1)
479 .read_to_end(&mut rest)
480 .map_err(|error| Error::Provider {
481 code: format!("http/body:{error}"),
482 })?;
483 if rest.len() > self.max_body {
484 return Err(Error::ToolPolicy("response too large"));
485 }
486 body = rest;
487 }
488 }
489 Ok((status, body))
490 }
491}
492
493impl Provider for HttpTool {
494 fn name(&self) -> &'static str {
495 "http"
496 }
497
498 fn methods(&self) -> Vec<String> {
499 ["get", "post", "request"]
500 .iter()
501 .map(|method| method.to_string())
502 .collect()
503 }
504
505 fn mutating(&self, method: &str) -> bool {
506 method != "get"
507 }
508
509 fn invoke(&mut self, _object: u64, call: &Call) -> Result<Response> {
510 self.calls += 1;
511 let method: String = match call.method.as_str() {
512 "get" => "GET".to_string(),
513 "post" => "POST".to_string(),
514 "request" => call
515 .args
516 .iter()
517 .find_map(|arg| match arg {
518 Arg::Value(Value::Text(text)) => Some(text.to_uppercase()),
519 _ => None,
520 })
521 .unwrap_or_else(|| "GET".to_string()),
522 other => {
523 return Err(Error::MethodNotGranted {
524 method: other.to_string(),
525 })
526 }
527 };
528 let path = call
529 .args
530 .iter()
531 .find_map(|arg| match arg {
532 Arg::Value(Value::Text(text)) if text.starts_with('/') => Some(text.clone()),
533 Arg::Value(Value::Path(path)) => Some(path.clone()),
534 _ => None,
535 })
536 .ok_or(Error::InvalidArgument("an HTTP path argument is required"))?;
537 if !self.allow.contains(&(method.to_string(), path.clone())) {
538 return Err(Error::ToolPolicy("path is not on the allowlist"));
539 }
540 let body = serde_json::to_vec(
541 &call
542 .args
543 .iter()
544 .filter_map(|arg| match arg {
545 Arg::Value(Value::Map(entries)) => {
546 Some(value_to_json(&Value::Map(entries.clone())))
547 }
548 _ => None,
549 })
550 .next()
551 .unwrap_or(Json::Null),
552 )
553 .unwrap_or_default();
554 if body.len() > self.max_request {
555 return Err(Error::ToolPolicy("request too large"));
556 }
557 let (status, response) = self.perform(&method, &path, &body)?;
558 if status >= 400 {
559 return Err(Error::Provider {
560 code: format!("http/{status}"),
561 });
562 }
563 let text = String::from_utf8_lossy(&response).to_string();
564 let value = serde_json::from_str::<Json>(&text)
565 .map(|json| json_to_value(&json))
566 .unwrap_or(Value::Text(text));
567 Ok(Response::Value(value))
568 }
569}
570
571#[derive(Debug)]
581pub struct LocalProcess {
582 pub program: String,
584 pub allow_args: Vec<String>,
587 pub env: BTreeMap<String, String>,
589 pub max_output: usize,
591 pub calls: u64,
593}
594
595impl LocalProcess {
596 pub fn new(program: impl Into<String>, allow_args: &[&str]) -> Self {
598 Self {
599 program: program.into(),
600 allow_args: allow_args.iter().map(|arg| arg.to_string()).collect(),
601 env: BTreeMap::new(),
602 max_output: 1 << 20,
603 calls: 0,
604 }
605 }
606
607 pub fn policy(&self) -> ToolPolicy {
609 ToolPolicy {
610 allowed: ["run"].iter().map(|method| method.to_string()).collect(),
611 max_arg_bytes: 64 << 10,
612 allow_process: true,
613 }
614 }
615
616 fn permitted(&self, arg: &str) -> bool {
617 self.allow_args.iter().any(|allowed| {
618 if let Some(prefix) = allowed.strip_suffix('*') {
619 arg.starts_with(prefix)
620 } else {
621 allowed == arg
622 }
623 })
624 }
625}
626
627impl Provider for LocalProcess {
628 fn name(&self) -> &'static str {
629 "local-process"
630 }
631
632 fn methods(&self) -> Vec<String> {
633 vec!["run".to_string()]
634 }
635
636 fn mutating(&self, _method: &str) -> bool {
637 true
638 }
639
640 fn invoke(&mut self, _object: u64, call: &Call) -> Result<Response> {
641 self.calls += 1;
642 if call.method != "run" {
643 return Err(Error::MethodNotGranted {
644 method: call.method.clone(),
645 });
646 }
647 let mut args = Vec::new();
648 for arg in &call.args {
649 if let Arg::Value(Value::Text(text)) = arg {
650 if !self.permitted(text) {
651 return Err(Error::ToolPolicy("argument is not allowed"));
652 }
653 args.push(text.clone());
654 }
655 }
656 if !call.tool_allows_process {
658 return Err(Error::ToolPolicy("process execution not granted"));
659 }
660 let mut command = Command::new(&self.program);
661 command
662 .args(&args)
663 .env_clear()
664 .stdin(Stdio::null())
665 .stderr(Stdio::piped())
666 .stdout(Stdio::piped());
667 for (key, value) in &self.env {
668 command.env(key, value);
669 }
670 let output = command.output().map_err(|error| Error::Provider {
671 code: format!("process/spawn:{error}"),
672 })?;
673 if output.stdout.len() > self.max_output {
674 return Err(Error::ToolPolicy("output too large"));
675 }
676 let mut map = BTreeMap::new();
677 map.insert(
678 "status".to_string(),
679 Value::Int(output.status.code().unwrap_or(-1) as i64),
680 );
681 map.insert(
682 "stdout".to_string(),
683 Value::Text(String::from_utf8_lossy(&output.stdout).to_string()),
684 );
685 map.insert(
686 "stderr".to_string(),
687 Value::Text(String::from_utf8_lossy(&output.stderr).to_string()),
688 );
689 Ok(Response::Value(Value::Map(map)))
690 }
691}