//! `spectra mcp --auto` (spec ยง15.1): one MCP entry for every folder. The //! shim speaks for the agent whose project folder the client started in, //! from any depth inside it; an agent home made before labels is labelled //! on first use; a folder with no agent gets a quiet server, not a failure. mod common; use common::{hermetic, run, setup, text, Serving}; use serde_json::{json, Value}; use std::io::{BufRead, BufReader, Write}; use std::path::Path; use std::process::Stdio; /// Run the shim in `cwd` and ask it two things: the handshake and `brief`. fn ask(d: &Path, cwd: &Path) -> (Value, Value) { let mut c = hermetic(d) .args(["mcp", "--auto"]) .current_dir(cwd) .env("SPECTRA_AGENTS", d.join("agents")) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .unwrap(); let mut stdin = c.stdin.take().unwrap(); for v in [ json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "t", "version": "1" } } }), json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "brief", "arguments": {} } }), ] { writeln!(stdin, "{v}").unwrap(); } stdin.flush().unwrap(); let mut lines = BufReader::new(c.stdout.take().unwrap()).lines(); let mut got = std::collections::HashMap::new(); while got.len() < 2 { let v: Value = serde_json::from_str(&lines.next().unwrap().unwrap()).unwrap(); got.insert(v["id"].as_u64().unwrap(), v); } drop(stdin); let _ = c.kill(); let _ = c.wait(); (got.remove(&1).unwrap(), got.remove(&2).unwrap()) } #[test] fn one_entry_serves_every_folder() { let dir = setup(); let d = dir.path(); for p in ["book", "web"] { std::fs::create_dir_all(d.join(p).join("chapters")).unwrap(); assert!(run(d, &["project", "add", d.join(p).to_str().unwrap()]).status.success()); } for (n, p) in [("claude-book", "book"), ("claude-web", "web")] { let home = d.join("agents").join(n); let o = run(d, &["agent", "add", n, "--kind", "claude-code", "--project", p, "--home", home.to_str().unwrap(), "--yes"]); assert!(o.status.success(), "{}", text(&o)); assert!(home.join("agent.toml").exists(), "agent add labels the home"); } // An agent home from before labels: labelled on first use. std::fs::remove_file(d.join("agents/claude-web/agent.toml")).unwrap(); let srv = Serving::start(d); let (_, brief) = ask(d, &d.join("book/chapters")); assert!(brief["result"]["content"][0]["text"].as_str().unwrap().contains("You are claude-book"), "{brief}"); let (_, brief) = ask(d, &d.join("web")); assert!(brief["result"]["content"][0]["text"].as_str().unwrap().contains("You are claude-web"), "{brief}"); assert!(d.join("agents/claude-web/agent.toml").exists(), "labelled on first use"); // Elsewhere: a quiet server that says how to join, not a failure. std::fs::create_dir_all(d.join("elsewhere")).unwrap(); let (init, call) = ask(d, &d.join("elsewhere")); assert!(init["result"]["instructions"].as_str().unwrap().contains("has no Spectra agent"), "{init}"); assert!(call["error"]["message"].as_str().unwrap().contains("Spectra: Add Agent"), "{call}"); srv.stop(); }