171 lines
5.3 KiB
Rust
171 lines
5.3 KiB
Rust
use std::io::{BufRead, BufReader, Write};
|
|
use std::process::{Child, Command, Stdio};
|
|
|
|
use serde_json::Value;
|
|
|
|
struct McpProcess {
|
|
child: Child,
|
|
reader: BufReader<std::process::ChildStdout>,
|
|
}
|
|
|
|
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:?}"
|
|
);
|
|
}
|