use supercode_harness::mcp::{handle_request, McpClient, McpTool};
use supercode_harness::tools::{Tool, ToolContext, ToolRegistry};
fn tmp() -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let d = std::env::temp_dir().join(format!(
"sc-mcp-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&d).unwrap();
d
}
#[tokio::test]
async fn mcp_server_lists_and_calls_tools() {
let dir = tmp();
std::fs::write(dir.join("hello.txt"), "WORLD").unwrap();
let reg = ToolRegistry::with_builtins();
let ctx = ToolContext::new(dir.clone());
let init = handle_request(
®,
&ctx,
&serde_json::json!({
"jsonrpc":"2.0","id":1,"method":"initialize","params":{}
}),
)
.await
.unwrap();
assert_eq!(init["result"]["serverInfo"]["name"], "supercode");
let list = handle_request(
®,
&ctx,
&serde_json::json!({
"jsonrpc":"2.0","id":2,"method":"tools/list"
}),
)
.await
.unwrap();
let names: Vec<&str> = list["result"]["tools"]
.as_array()
.unwrap()
.iter()
.map(|t| t["name"].as_str().unwrap())
.collect();
assert!(names.contains(&"read_file") && names.contains(&"apply_patch"));
let call = handle_request(
®,
&ctx,
&serde_json::json!({
"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"read_file","arguments":{"path":"hello.txt"}}
}),
)
.await
.unwrap();
assert_eq!(call["result"]["content"][0]["text"], "WORLD");
assert_eq!(call["result"]["isError"], false);
let err = handle_request(
®,
&ctx,
&serde_json::json!({
"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"nope","arguments":{}}
}),
)
.await
.unwrap();
assert_eq!(err["error"]["code"], -32601);
assert!(handle_request(
®,
&ctx,
&serde_json::json!({
"jsonrpc":"2.0","method":"notifications/initialized"
})
)
.await
.is_none());
std::fs::remove_dir_all(&dir).ok();
}
const FAKE_SERVER: &str = r#"
import sys, json
def send(o): sys.stdout.write(json.dumps(o)+"\n"); sys.stdout.flush()
for line in sys.stdin:
line=line.strip()
if not line: continue
msg=json.loads(line)
m=msg.get("method"); i=msg.get("id")
if m=="initialize":
send({"jsonrpc":"2.0","id":i,"result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"fake"}}})
elif m=="tools/list":
send({"jsonrpc":"2.0","id":i,"result":{"tools":[
{"name":"echo","description":"echo back","inputSchema":{"type":"object","properties":{"text":{"type":"string"}}}}
]}})
elif m=="tools/call":
args=msg.get("params",{}).get("arguments",{})
send({"jsonrpc":"2.0","id":i,"result":{"content":[{"type":"text","text":"echo: "+args.get("text","")}],"isError":False}})
else:
if i is not None:
send({"jsonrpc":"2.0","id":i,"error":{"code":-32601,"message":"unknown"}})
"#;
#[tokio::test]
async fn mcp_client_connects_lists_and_calls() {
if std::process::Command::new("python3")
.arg("--version")
.output()
.is_err()
{
eprintln!("skipping: no python3");
return;
}
let dir = tmp();
let script = dir.join("server.py");
std::fs::write(&script, FAKE_SERVER).unwrap();
let client = McpClient::connect(
"python3",
&[script.to_str().unwrap()],
&std::collections::BTreeMap::new(),
)
.await
.unwrap();
let tools = McpTool::from_client("fake", client).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].name(), "mcp__fake__echo");
assert_eq!(tools[0].description(), "echo back");
let ctx = ToolContext::new(dir.clone());
let out = tools[0]
.execute(serde_json::json!({"text": "hi there"}), &ctx)
.await
.unwrap();
assert_eq!(out, "echo: hi there");
std::fs::remove_dir_all(&dir).ok();
}
const ENV_ECHO_SERVER: &str = r#"
import sys, json, os
def send(o): sys.stdout.write(json.dumps(o)+"\n"); sys.stdout.flush()
for line in sys.stdin:
line=line.strip()
if not line: continue
msg=json.loads(line)
m=msg.get("method"); i=msg.get("id")
if m=="initialize":
send({"jsonrpc":"2.0","id":i,"result":{"protocolVersion":"2025-06-18","serverInfo":{"name":"fake"}}})
elif m=="tools/list":
send({"jsonrpc":"2.0","id":i,"result":{"tools":[
{"name":"whoami","description":"report an env var","inputSchema":{"type":"object","properties":{}}}
]}})
elif m=="tools/call":
val = os.environ.get("SUPERCODE_MCP_TEST_TOKEN", "<unset>")
home = os.environ.get("SUPERCODE_MCP_TEST_INHERITED", "<unset>")
send({"jsonrpc":"2.0","id":i,"result":{"content":[{"type":"text","text":val+"|"+home}],"isError":False}})
else:
if i is not None:
send({"jsonrpc":"2.0","id":i,"error":{"code":-32601,"message":"unknown"}})
"#;
#[tokio::test]
async fn mcp_client_env_reaches_the_spawned_server_on_top_of_inherited_env() {
if std::process::Command::new("python3")
.arg("--version")
.output()
.is_err()
{
eprintln!("skipping: no python3");
return;
}
let dir = tmp();
let script = dir.join("env_server.py");
std::fs::write(&script, ENV_ECHO_SERVER).unwrap();
unsafe { std::env::set_var("SUPERCODE_MCP_TEST_INHERITED", "from-parent") };
let mut env = std::collections::BTreeMap::new();
env.insert(
"SUPERCODE_MCP_TEST_TOKEN".to_string(),
"secret-from-config".to_string(),
);
let client = McpClient::connect("python3", &[script.to_str().unwrap()], &env)
.await
.unwrap();
let tools = McpTool::from_client("envfake", client).await.unwrap();
assert_eq!(tools.len(), 1);
let ctx = ToolContext::new(dir.clone());
let out = tools[0].execute(serde_json::json!({}), &ctx).await.unwrap();
assert_eq!(
out, "secret-from-config|from-parent",
"the configured env var must reach the spawned server, ON TOP OF \
(not instead of) the inherited parent environment"
);
let client2 = McpClient::connect(
"python3",
&[script.to_str().unwrap()],
&std::collections::BTreeMap::new(),
)
.await
.unwrap();
let tools2 = McpTool::from_client("envfake2", client2).await.unwrap();
let out2 = tools2[0]
.execute(serde_json::json!({}), &ctx)
.await
.unwrap();
assert_eq!(
out2, "<unset>|from-parent",
"no configured env must not fabricate a var, but must still inherit \
the parent's existing environment"
);
unsafe { std::env::remove_var("SUPERCODE_MCP_TEST_INHERITED") };
std::fs::remove_dir_all(&dir).ok();
}