development
claude/code-feature-parity-q003hm
claude/elegant-carson-l1menh
claude/sigit-acp-local-chat-cx6380
claude/sigit-cloud-agent-expansion-reox0a
claude/tool-permission-system
claude/zen-feynman-0u78dk
development
feature/agent-tools-multiedit-glob-todos-remember
feature/background-commands
feature/commit-coauthor-attribution
feature/headless-mode
feature/init-command
feature/load-local-model-explicitly
feature/session-persistence-compaction
feature/sigit-code-cloud
feature/subagent-tool
feature/tool-permission-system
feature/tui-repo-tabs
feature/tui-tabs
main
release/v1.3.1
| 1 | //! Test-only MCP stdio server stub, used by `tests/mcp_stdio.rs`. |
| 2 | //! |
| 3 | //! Speaks the MCP stdio transport: newline-delimited JSON-RPC 2.0, one message |
| 4 | //! per line on stdin/stdout. It answers `initialize`, `tools/list` (a single |
| 5 | //! `echo` tool) and `tools/call` (echoes `text` back, prefixed with the |
| 6 | //! `STUB_PREFIX` env var so tests can verify env propagation). |
| 7 | //! |
| 8 | //! Flags that script failure modes: |
| 9 | //! - `--fail`: exit(1) immediately, before reading anything (a server whose |
| 10 | //! process dies at spawn). |
| 11 | //! - `--exit-after-list`: exit(0) right after answering `tools/list` (a server |
| 12 | //! that dies after discovery, exercising the dead-child call path). |
| 13 | //! |
| 14 | //! After the `initialize` response it also emits a server-initiated |
| 15 | //! notification the client must log and ignore. |
| 16 | //! |
| 17 | //! This binary is excluded from the published crate (see `exclude` in |
| 18 | //! `Cargo.toml`); it exists only for the integration tests. |
| 19 | |
| 20 | use std::io::{BufRead, Write}; |
| 21 | |
| 22 | use serde_json::{Value, json}; |
| 23 | |
| 24 | fn main() { |
| 25 | let args: Vec<String> = std::env::args().skip(1).collect(); |
| 26 | if args.iter().any(|a| a == "--fail") { |
| 27 | std::process::exit(1); |
| 28 | } |
| 29 | let exit_after_list = args.iter().any(|a| a == "--exit-after-list"); |
| 30 | |
| 31 | let stdin = std::io::stdin(); |
| 32 | let stdout = std::io::stdout(); |
| 33 | |
| 34 | for line in stdin.lock().lines() { |
| 35 | let Ok(line) = line else { break }; |
| 36 | if line.trim().is_empty() { |
| 37 | continue; |
| 38 | } |
| 39 | let Ok(message) = serde_json::from_str::<Value>(&line) else { |
| 40 | continue; |
| 41 | }; |
| 42 | let id = message.get("id").cloned(); |
| 43 | let method = message |
| 44 | .get("method") |
| 45 | .and_then(Value::as_str) |
| 46 | .unwrap_or_default() |
| 47 | .to_string(); |
| 48 | |
| 49 | let result = match method.as_str() { |
| 50 | "initialize" => Some(json!({ |
| 51 | "protocolVersion": "2025-06-18", |
| 52 | "capabilities": { "tools": {} }, |
| 53 | "serverInfo": { "name": "mcp-stdio-stub", "version": "0.0.0" } |
| 54 | })), |
| 55 | "tools/list" => Some(json!({ |
| 56 | "tools": [{ |
| 57 | "name": "echo", |
| 58 | "description": "Echo the text back.", |
| 59 | "inputSchema": { |
| 60 | "type": "object", |
| 61 | "properties": { "text": { "type": "string" } }, |
| 62 | "required": ["text"] |
| 63 | } |
| 64 | }] |
| 65 | })), |
| 66 | "tools/call" => { |
| 67 | let text = message |
| 68 | .pointer("/params/arguments/text") |
| 69 | .and_then(Value::as_str) |
| 70 | .unwrap_or_default(); |
| 71 | let prefix = std::env::var("STUB_PREFIX").unwrap_or_default(); |
| 72 | Some(json!({ |
| 73 | "content": [{ "type": "text", "text": format!("{prefix}{text}") }] |
| 74 | })) |
| 75 | } |
| 76 | // Notifications (`notifications/initialized`) and anything else |
| 77 | // without a scripted answer fall through. |
| 78 | _ => None, |
| 79 | }; |
| 80 | |
| 81 | let mut out = stdout.lock(); |
| 82 | match (id, result) { |
| 83 | (Some(id), Some(result)) => { |
| 84 | let response = json!({ "jsonrpc": "2.0", "id": id, "result": result }); |
| 85 | writeln!(out, "{response}").ok(); |
| 86 | } |
| 87 | (Some(id), None) => { |
| 88 | let response = json!({ |
| 89 | "jsonrpc": "2.0", |
| 90 | "id": id, |
| 91 | "error": { "code": -32601, "message": "method not found" } |
| 92 | }); |
| 93 | writeln!(out, "{response}").ok(); |
| 94 | } |
| 95 | // A notification: nothing to answer. |
| 96 | (None, _) => {} |
| 97 | } |
| 98 | if method == "initialize" { |
| 99 | // Server-initiated traffic the client must ignore. |
| 100 | let notification = json!({ "jsonrpc": "2.0", "method": "notifications/stub" }); |
| 101 | writeln!(out, "{notification}").ok(); |
| 102 | } |
| 103 | out.flush().ok(); |
| 104 | |
| 105 | if exit_after_list && method == "tools/list" { |
| 106 | std::process::exit(0); |
| 107 | } |
| 108 | } |
| 109 | } |