use std::io::{BufRead, BufReader, Write}; use std::process::{Child, Command, Stdio}; use serde_json::Value; struct McpProcess { child: Child, reader: BufReader, } impl McpProcess { fn spawn() -> Self { let mut child = Command::new(env!("CARGO_BIN_EXE_quantum-bridge-mcp")) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() .expect("failed to spawn quantum-bridge-mcp binary"); let stdout = child.stdout.take().expect("failed to take stdout"); let reader = BufReader::new(stdout); Self { child, reader } } fn send(&mut self, msg: &str) { let stdin = self.child.stdin.as_mut().expect("failed to get stdin"); writeln!(stdin, "{}", msg).expect("failed to write to stdin"); stdin.flush().expect("failed to flush stdin"); } fn recv(&mut self) -> Value { let mut line = String::new(); self.reader .read_line(&mut line) .expect("failed to read line from stdout"); serde_json::from_str(line.trim()).expect("failed to parse JSON response") } /// Send initialize and consume the response, returning self for chaining. fn initialize(&mut self) -> &mut Self { self.send( r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#, ); let _ = self.recv(); self } /// Extract the text payload from a tools/call response content array. fn tool_text(response: &Value) -> Value { let text = response["result"]["content"][0]["text"] .as_str() .expect("expected text field in content[0]"); serde_json::from_str(text).expect("failed to parse tool response text as JSON") } } impl Drop for McpProcess { fn drop(&mut self) { let _ = self.child.kill(); } } #[test] fn initialize_returns_server_info() { let mut proc = McpProcess::spawn(); proc.send( r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}"#, ); let resp = proc.recv(); assert_eq!(resp["result"]["protocolVersion"], "2024-11-05"); assert!( resp["result"]["serverInfo"]["name"].is_string(), "serverInfo.name should be a string" ); } #[test] fn tools_list_contains_core_tools() { let mut proc = McpProcess::spawn(); proc.initialize(); proc.send(r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#); let resp = proc.recv(); let tools = resp["result"]["tools"] .as_array() .expect("tools should be an array"); let names: Vec<&str> = tools .iter() .map(|t| t["name"].as_str().expect("tool name should be a string")) .collect(); assert!(names.contains(&"list_backends")); assert!(names.contains(&"validate_circuit")); assert!(names.contains(&"run_circuit")); } #[test] fn call_list_backends_returns_local_simulator() { let mut proc = McpProcess::spawn(); proc.initialize(); proc.send( r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_backends","arguments":{}}}"#, ); let resp = proc.recv(); let payload = McpProcess::tool_text(&resp); let backends = payload["backends"] .as_array() .expect("backends should be an array"); assert!(!backends.is_empty(), "expected at least one backend"); assert_eq!( backends[0]["name"], "local_simulator", "first backend name should be local_simulator" ); } #[test] fn call_validate_circuit_with_valid_bell_returns_is_valid_true() { let bell_circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;"; let params = serde_json::json!({ "name": "validate_circuit", "arguments": { "circuit": bell_circuit } }); let request = serde_json::json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": params }); let mut proc = McpProcess::spawn(); proc.initialize(); proc.send(&request.to_string()); let resp = proc.recv(); let payload = McpProcess::tool_text(&resp); assert_eq!( payload["is_valid"], true, "Bell circuit should be valid, got: {payload}" ); } #[test] fn call_run_circuit_with_x_gate_returns_count_of_1_for_all_shots() { let x_circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;"; let params = serde_json::json!({ "name": "run_circuit", "arguments": { "circuit": x_circuit, "shots": 100 } }); let request = serde_json::json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": params }); let mut proc = McpProcess::spawn(); proc.initialize(); proc.send(&request.to_string()); let resp = proc.recv(); let payload = McpProcess::tool_text(&resp); let counts = payload["counts"] .as_object() .expect("counts should be an object"); assert_eq!( counts.get("1").and_then(|v| v.as_u64()), Some(100), "all 100 shots should produce outcome '1', got counts: {counts:?}" ); }