返回 CodeWhale
test_support.rs
根目录 / crates / mcp / src / test_support.rs
1 //! Test-only helpers. Compiled under `#[cfg(all(test, unix))]` only, so
2 //! nothing here can be reached by a real `codewhale mcp-server` run, and
3 //! Windows — where the POSIX-sh fixture cannot run and its consumers are
4 //! `#[cfg(unix)]` — does not compile it as dead code.
5
6 use std::path::{Path, PathBuf};
7
8 /// A minimal POSIX-sh MCP server used to prove that responses come from a
9 /// spawned process rather than from an in-process stub.
10 ///
11 /// It is written in `sh` rather than Rust or Python so the test depends on
12 /// nothing beyond the shell every unix CI runner already has.
13 pub const FAKE_MCP_SERVER_SH: &str = r#"#!/bin/sh
14 while IFS= read -r line; do
15 id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p')
16 method=$(printf '%s' "$line" | sed -n 's/.*"method":"\([^"]*\)".*/\1/p')
17 if [ -z "$id" ]; then
18 continue
19 fi
20 case "$method" in
21 initialize)
22 printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{},"resources":{}},"serverInfo":{"name":"fake-mcp","version":"0"}}}\n' "$id"
23 ;;
24 tools/list)
25 printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[{"name":"add","description":"add two numbers","inputSchema":{"type":"object","properties":{"a":{"type":"number"},"b":{"type":"number"}},"required":["a","b"]}}]}}\n' "$id"
26 ;;
27 tools/call)
28 name=$(printf '%s' "$line" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p')
29 if [ "$name" = "add" ]; then
30 printf '{"jsonrpc":"2.0","id":%s,"result":{"content":[{"type":"text","text":"5"}]}}\n' "$id"
31 else
32 printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32602,"message":"unknown tool: '"$name"'"}}\n' "$id"
33 fi
34 ;;
35 resources/list)
36 printf '{"jsonrpc":"2.0","id":%s,"result":{"resources":[{"uri":"file:///fake/readme.txt","name":"Fake readme","description":"resource from the spawned process","mimeType":"text/plain","size":16,"annotations":{"audience":["assistant"],"priority":0.75}}]}}\n' "$id"
37 ;;
38 resources/read)
39 printf '{"jsonrpc":"2.0","id":%s,"result":{"contents":[{"uri":"file:///fake/readme.txt","mimeType":"text/plain","text":"spawned-resource"}]}}\n' "$id"
40 ;;
41 *)
42 printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"unsupported method"}}\n' "$id"
43 ;;
44 esac
45 done
46 "#;
47
48 /// Owns a temporary directory holding [`FAKE_MCP_SERVER_SH`], removing it on
49 /// drop.
50 pub struct FakeServerScript {
51 dir: PathBuf,
52 script: PathBuf,
53 }
54
55 impl FakeServerScript {
56 pub fn path(&self) -> &Path {
57 &self.script
58 }
59 }
60
61 impl Drop for FakeServerScript {
62 fn drop(&mut self) {
63 let _ = std::fs::remove_dir_all(&self.dir);
64 }
65 }
66
67 /// Write the fake server to a unique temp directory and return a guard.
68 pub fn write_fake_mcp_server(label: &str) -> FakeServerScript {
69 let dir = std::env::temp_dir().join(format!(
70 "codewhale-mcp-{label}-{}-{:?}",
71 std::process::id(),
72 std::thread::current().id()
73 ));
74 std::fs::create_dir_all(&dir).expect("fake MCP server dir");
75 let script = dir.join("server.sh");
76 std::fs::write(&script, FAKE_MCP_SERVER_SH).expect("write fake MCP server");
77 FakeServerScript { dir, script }
78 }
79
79 lines RUST