supercode-harness 0.4.10

The optional native Supercode agent and tool harness
Documentation
//! MCP client + server tests (P5 extensibility).

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
}

// ---- server side: handle_request dispatches to the registry ----------------

#[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());

    // initialize
    let init = handle_request(
        &reg,
        &ctx,
        &serde_json::json!({
            "jsonrpc":"2.0","id":1,"method":"initialize","params":{}
        }),
    )
    .await
    .unwrap();
    assert_eq!(init["result"]["serverInfo"]["name"], "supercode");

    // tools/list exposes the built-ins (read_file etc.)
    let list = handle_request(
        &reg,
        &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"));

    // tools/call actually runs the tool
    let call = handle_request(
        &reg,
        &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);

    // unknown tool → error
    let err = handle_request(
        &reg,
        &ctx,
        &serde_json::json!({
            "jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"nope","arguments":{}}
        }),
    )
    .await
    .unwrap();
    assert_eq!(err["error"]["code"], -32601);

    // a notification (no id) gets no reply
    assert!(handle_request(
        &reg,
        &ctx,
        &serde_json::json!({
            "jsonrpc":"2.0","method":"notifications/initialized"
        })
    )
    .await
    .is_none());

    std::fs::remove_dir_all(&dir).ok();
}

// ---- client side: connect to a tiny fake MCP server ------------------------

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() {
    // Skip gracefully if python3 isn't available.
    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();
}

// ---- MCP env support: the configured `env` map reaches the spawned server -

/// A fake server whose one tool reports back what it sees in its own
/// `os.environ` — the only way to prove `env` genuinely reached the spawned
/// process (not just that `Command` was built with it) without inspecting
/// tokio internals.
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"}})
"#;

// One sequential test (not two parallel `#[tokio::test]`s) deliberately: both
// scenarios mutate the SAME process-global env var
// (`SUPERCODE_MCP_TEST_INHERITED`), which would race if run as separate
// tests under cargo's default parallel test execution.
#[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();

    // Simulate a var already inherited from the parent process (e.g. a shell
    // export) that `connect` must NOT clobber.
    // Safety: no other thread in this test binary reads/writes this var.
    unsafe { std::env::set_var("SUPERCODE_MCP_TEST_INHERITED", "from-parent") };

    // Scenario 1: a configured `env` var reaches the server ON TOP OF the
    // inherited environment (both present).
    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"
    );

    // Scenario 2: no configured `env` at all — the server still inherits the
    // parent environment (unaffected), and gets no fabricated token.
    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();
}