supercode-harness 0.4.8

The optional native Supercode agent and tool harness
Documentation
//! P5-2 (COMPOSABLE-HARNESS-DESIGN.md §2 module 15 D7 row 3 "resources +
//! templates"): `resources/list`, `resources/templates/list`,
//! `resources/read`, `resources/subscribe` exposed as model-callable tools —
//! driven against a fake stdio MCP server (`live-agent-test-safety`).

use supercode_harness::mcp::{McpClient, McpServerHandle};
use supercode_harness::tools::ToolContext;

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-resources-{}-{}",
        std::process::id(),
        N.fetch_add(1, Ordering::SeqCst)
    ));
    std::fs::create_dir_all(&d).unwrap();
    d
}

fn have_python3() -> bool {
    std::process::Command::new("python3")
        .arg("--version")
        .output()
        .is_ok()
}

const RESOURCE_SERVER: &str = r#"
import sys, json
def send(o): sys.stdout.write(json.dumps(o)+"\n"); sys.stdout.flush()
subscribed = []
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":[]}})
    elif m=="resources/list":
        send({"jsonrpc":"2.0","id":i,"result":{"resources":[
            {"uri":"file:///a.txt","name":"a","description":"file a","mimeType":"text/plain"}
        ]}})
    elif m=="resources/templates/list":
        send({"jsonrpc":"2.0","id":i,"result":{"resourceTemplates":[
            {"uriTemplate":"file:///{name}.txt","name":"any file","description":"template"}
        ]}})
    elif m=="resources/read":
        uri = msg.get("params",{}).get("uri")
        send({"jsonrpc":"2.0","id":i,"result":{"contents":[{"uri":uri,"text":"CONTENT OF "+uri}]}})
    elif m=="resources/subscribe":
        uri = msg.get("params",{}).get("uri")
        subscribed.append(uri)
        send({"jsonrpc":"2.0","id":i,"result":{}})
    else:
        if i is not None:
            send({"jsonrpc":"2.0","id":i,"error":{"code":-32601,"message":"unknown"}})
"#;

async fn connect_fake() -> Option<McpServerHandle> {
    if !have_python3() {
        eprintln!("skipping: no python3");
        return None;
    }
    let dir = tmp();
    let script = dir.join("server.py");
    std::fs::write(&script, RESOURCE_SERVER).unwrap();
    let client = McpClient::connect("python3", &[script.to_str().unwrap()], &Default::default())
        .await
        .unwrap();
    Some(McpServerHandle::new("fake", client))
}

fn ctx() -> ToolContext {
    ToolContext::new(std::env::temp_dir())
}

#[tokio::test]
async fn resource_tools_are_namespaced_per_server() {
    let Some(handle) = connect_fake().await else {
        return;
    };
    let tools = handle.resource_tools();
    let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
    assert_eq!(
        names,
        vec![
            "mcp__fake__resources_list",
            "mcp__fake__resources_read",
            "mcp__fake__resources_subscribe",
        ]
    );
}

#[tokio::test]
async fn resources_list_tool_reports_resources_and_templates() {
    let Some(handle) = connect_fake().await else {
        return;
    };
    let tools = handle.resource_tools();
    let list_tool = &tools[0];
    let out = list_tool
        .execute(serde_json::json!({}), &ctx())
        .await
        .unwrap();
    assert!(out.contains("file:///a.txt"));
    assert!(out.contains("template"));
}

#[tokio::test]
async fn resources_read_tool_returns_the_resource_content() {
    let Some(handle) = connect_fake().await else {
        return;
    };
    let tools = handle.resource_tools();
    let read_tool = &tools[1];
    let out = read_tool
        .execute(serde_json::json!({"uri": "file:///a.txt"}), &ctx())
        .await
        .unwrap();
    assert_eq!(out, "CONTENT OF file:///a.txt");
}

#[tokio::test]
async fn resources_subscribe_tool_confirms_and_client_logs_the_update_notification() {
    let Some(handle) = connect_fake().await else {
        return;
    };
    let tools = handle.resource_tools();
    let subscribe_tool = &tools[2];
    let out = subscribe_tool
        .execute(serde_json::json!({"uri": "file:///a.txt"}), &ctx())
        .await
        .unwrap();
    assert!(out.contains("file:///a.txt"));
}

const OVERSIZED_RESOURCE_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=="resources/read":
        uri = msg.get("params",{}).get("uri")
        # 17 MiB of text -- over the 16 MiB MCP_MAX_RESOURCE_BYTES cap
        # (crates/harness/src/mcp.rs) this client enforces.
        big = "x" * (17 * 1024 * 1024)
        send({"jsonrpc":"2.0","id":i,"result":{"contents":[{"uri":uri,"text":big}]}})
    else:
        if i is not None:
            send({"jsonrpc":"2.0","id":i,"error":{"code":-32601,"message":"unknown"}})
"#;

async fn connect_oversized_fake() -> Option<McpClient> {
    if !have_python3() {
        eprintln!("skipping: no python3");
        return None;
    }
    let dir = tmp();
    let script = dir.join("server.py");
    std::fs::write(&script, OVERSIZED_RESOURCE_SERVER).unwrap();
    let client = McpClient::connect("python3", &[script.to_str().unwrap()], &Default::default())
        .await
        .unwrap();
    Some(client)
}

/// Hardening (Fable-5 review, memory-DoS finding): a resource whose joined
/// `resources/read` contents exceed `MCP_MAX_RESOURCE_BYTES` must error
/// (fail-closed, named), not buffer/return an unbounded string.
#[tokio::test]
async fn read_resource_over_the_cap_errors_named_not_unbounded() {
    let Some(mut client) = connect_oversized_fake().await else {
        return;
    };
    let result = tokio::time::timeout(
        std::time::Duration::from_secs(20),
        client.read_resource("file:///huge.txt"),
    )
    .await
    .expect("must not hang");
    let err = result.expect_err("an oversized resource must error, not silently succeed");
    assert!(
        err.to_string().contains("exceeded max"),
        "error should name the cap: {err}"
    );
}

#[tokio::test]
async fn resources_read_tool_rejects_missing_uri_argument() {
    let Some(handle) = connect_fake().await else {
        return;
    };
    let tools = handle.resource_tools();
    let read_tool = &tools[1];
    let err = read_tool
        .execute(serde_json::json!({}), &ctx())
        .await
        .unwrap_err();
    assert!(err.to_string().contains("uri") || err.to_string().contains("invalid"));
}