# Sub-plan 4 — Tools + MCP Server (Tasks 6–9) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans. **Goal:** Implement the three tool handlers (`list_backends`, `validate_circuit`, `run_circuit`) and wire everything into a working rmcp stdio MCP server. Smoke-test with a live JSON-RPC call. **Starting state (after sub-plan 3):** - All of sub-plans 1–3 files present - `src/executor.rs` — `LocalSimulator` fully implemented, all 7 tests pass - `src/validator.rs` — `CircuitValidator` fully implemented, all 5 tests pass - `cargo test` passes **Deliverable:** `cargo build` produces a working MCP binary. `cargo test` passes with 16+ unit tests. Smoke test shows a valid JSON-RPC `initialize` response. Four commits added. **Key rmcp API:** - Macros: `#[tool_router]` on `impl`, `#[tool_handler]` on `impl ServerHandler`, `#[tool(description="...")]` on each method - Parameters: `Parameters(MyParams { field }): Parameters` where `MyParams` derives `Deserialize + JsonSchema` - Return: `Result` — success via `CallToolResult::success(vec![Content::text("...")])`, error via `McpError::invalid_params("msg", None)` - Server field: `tool_router: ToolRouter` initialised with `Self::tool_router()` - stdio: `QuantumBridgeServer::new().serve(stdio()).await?.waiting().await?` - Logs go to **stderr** — stdout is the MCP wire **Main plan reference:** `docs/superpowers/plans/2026-04-28-quantum-bridge-mcp-v1.md` Tasks 6–9. --- ## Task 6: Tool — list_backends **Files:** - Create: `src/tools/list_backends.rs` - Create: `src/tools/mod.rs` (stub, extended in Task 9) - [ ] **Step 1: Create src/tools/list_backends.rs with failing test** ```rust use serde_json::json; use crate::executor::{CanIntrospect, LocalSimulator, MAX_LOCAL_QUBITS}; pub fn list_backends_response() -> serde_json::Value { todo!("implement list_backends_response") } #[cfg(test)] mod tests { use super::*; #[test] fn response_contains_local_simulator() { let resp = list_backends_response(); let backends = resp["backends"].as_array().unwrap(); assert_eq!(backends.len(), 1); assert_eq!(backends[0]["name"], "local_simulator"); } #[test] fn response_contains_max_qubits() { let resp = list_backends_response(); assert_eq!( resp["backends"][0]["max_qubits"].as_u64().unwrap(), MAX_LOCAL_QUBITS as u64 ); } #[test] fn response_lists_supported_gates() { let resp = list_backends_response(); let gates = resp["backends"][0]["supported_gates"].as_array().unwrap(); assert!(!gates.is_empty()); assert!(gates.iter().any(|g| g == "h")); assert!(gates.iter().any(|g| g == "cx")); } } ``` - [ ] **Step 2: Create src/tools/mod.rs (stub)** ```rust pub mod list_backends; pub mod run_circuit; pub mod validate_circuit; ``` - [ ] **Step 3: Declare tools module in src/main.rs** Add `mod tools;` to `src/main.rs`. - [ ] **Step 4: Run tests to confirm todo!() panics** ```bash cargo test tools::list_backends 2>&1 | tail -5 ``` - [ ] **Step 5: Implement list_backends_response** Replace `todo!()`: ```rust pub fn list_backends_response() -> serde_json::Value { let sim = LocalSimulator::new(); json!({ "backends": [{ "name": sim.name(), "description": "Local statevector simulator (Spinoza engine). No network, no account required.", "max_qubits": sim.max_qubits(), "supported_gates": sim.supported_gates(), "simulation_type": "statevector", "notes": "Statevector memory: 2^n × 16 bytes. Max 28 qubits." }] }) } ``` - [ ] **Step 6: Run tests** ```bash cargo test tools::list_backends 2>&1 ``` Expected: 3 tests pass. - [ ] **Step 7: Commit** ```bash git add src/tools/mod.rs src/tools/list_backends.rs src/main.rs git commit -m "feat: implement list_backends tool handler" ``` --- ## Task 7: Tool — validate_circuit **Files:** - Create: `src/tools/validate_circuit.rs` - [ ] **Step 1: Create file with failing test** ```rust use serde_json::{json, Value}; use crate::executor::MAX_LOCAL_QUBITS; use crate::types::CircuitSource; pub fn validate_circuit_response(circuit: &str) -> Value { todo!("implement validate_circuit_response") } #[cfg(test)] mod tests { use super::*; const BELL: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;"#; #[test] fn valid_circuit_returns_is_valid_true() { let resp = validate_circuit_response(BELL); assert_eq!(resp["is_valid"], true); assert_eq!(resp["diagnostics"].as_array().unwrap().len(), 0); } #[test] fn valid_circuit_includes_qubit_count() { let resp = validate_circuit_response(BELL); assert_eq!(resp["num_qubits"].as_u64().unwrap(), 2); } #[test] fn invalid_circuit_returns_is_valid_false_with_message() { let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;"); assert_eq!(resp["is_valid"], false); assert!(!resp["diagnostics"].as_array().unwrap().is_empty()); } #[test] fn unsupported_gate_returns_diagnostic_with_gate_name() { let circuit = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nu3(0.1,0.2,0.3) q[0];"; let resp = validate_circuit_response(circuit); assert_eq!(resp["is_valid"], false); let msg = resp["diagnostics"][0]["message"].as_str().unwrap(); assert!(msg.to_lowercase().contains("u3"), "got: {msg}"); } #[test] fn diagnostic_includes_line_and_column() { let resp = validate_circuit_response("OPENQASM 3.0;\nnot_valid_qasm;\n"); assert!(resp["diagnostics"][0]["line"].as_u64().unwrap() >= 1); assert!(resp["diagnostics"][0]["column"].as_u64().unwrap() >= 1); } } ``` - [ ] **Step 2: Run to confirm todo!() failure** ```bash cargo test tools::validate_circuit 2>&1 | tail -5 ``` - [ ] **Step 3: Implement validate_circuit_response** ```rust pub fn validate_circuit_response(circuit: &str) -> Value { use crate::types::DiagnosticSeverity; use crate::validator::CircuitValidator; let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); match validator.validate(&CircuitSource(circuit.to_string())) { Err(e) => json!({ "is_valid": false, "diagnostics": [{"line": 1, "column": 1, "message": e.to_string(), "severity": "error"}], "num_qubits": null, "num_gates": null, }), Ok(result) => { let diagnostics: Vec = result.diagnostics.iter().map(|d| { json!({ "line": d.line, "column": d.column, "message": d.message, "severity": match d.severity { DiagnosticSeverity::Error => "error", DiagnosticSeverity::Warning => "warning", } }) }).collect(); json!({ "is_valid": result.is_valid, "diagnostics": diagnostics, "num_qubits": result.num_qubits, "num_gates": result.num_gates, }) } } } ``` - [ ] **Step 4: Run tests** ```bash cargo test tools::validate_circuit 2>&1 ``` Expected: 5 tests pass. - [ ] **Step 5: Commit** ```bash git add src/tools/validate_circuit.rs git commit -m "feat: implement validate_circuit tool handler" ``` --- ## Task 8: Tool — run_circuit **Files:** - Create: `src/tools/run_circuit.rs` - [ ] **Step 1: Create file with failing test** ```rust use serde_json::{json, Value}; use crate::types::{CircuitSource, ShotCount}; pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { todo!("implement run_circuit_response") } #[cfg(test)] mod tests { use super::*; const BELL: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[2] q; bit[2] c; h q[0]; cx q[0], q[1]; c = measure q;"#; const X: &str = r#"OPENQASM 3.0; include "stdgates.inc"; qubit[1] q; bit[1] c; x q[0]; c = measure q;"#; #[test] fn bell_counts_total_matches_shots() { let resp = run_circuit_response(BELL, 1_000, false); let total: u64 = resp["counts"].as_object().unwrap().values() .map(|v| v.as_u64().unwrap()).sum(); assert_eq!(total, 1_000); } #[test] fn bell_only_produces_00_and_11_outcomes() { let resp = run_circuit_response(BELL, 1_000, false); for key in resp["counts"].as_object().unwrap().keys() { assert!(key == "00" || key == "11", "unexpected: {key}"); } } #[test] fn x_gate_produces_only_one_outcome() { let resp = run_circuit_response(X, 100, false); assert_eq!(resp["counts"]["1"].as_u64().unwrap(), 100); } #[test] fn response_includes_shots_and_execution_time() { let resp = run_circuit_response(BELL, 512, false); assert_eq!(resp["shots"].as_u64().unwrap(), 512); assert!(resp["execution_time_ms"].as_f64().unwrap() >= 0.0); } #[test] fn statevector_absent_when_not_requested() { assert!(run_circuit_response(BELL, 100, false)["statevector"].is_null()); } #[test] fn statevector_has_correct_length_when_requested() { let sv = run_circuit_response(BELL, 100, true)["statevector"].as_array().unwrap().len(); assert_eq!(sv, 4); } #[test] fn invalid_circuit_returns_error_field() { let resp = run_circuit_response("OPENQASM 3.0;\nnot_valid;", 100, false); assert!(resp.get("error").is_some(), "got: {resp}"); } #[test] fn shots_clamped_to_max() { let resp = run_circuit_response(BELL, ShotCount::MAX.0 + 1, false); let has_error = resp.get("error").is_some(); let shots = resp["shots"].as_u64().unwrap_or(0); assert!(has_error || shots == ShotCount::MAX.0 as u64); } } ``` - [ ] **Step 2: Run to confirm todo!() failure** ```bash cargo test tools::run_circuit 2>&1 | tail -5 ``` - [ ] **Step 3: Implement run_circuit_response** ```rust pub fn run_circuit_response(circuit: &str, shots: u32, return_statevector: bool) -> Value { use crate::executor::{CanExecute, LocalSimulator, MAX_LOCAL_QUBITS}; use crate::validator::CircuitValidator; let shot_count = ShotCount(shots.min(ShotCount::MAX.0)); let validator = CircuitValidator::new(MAX_LOCAL_QUBITS); let validation = match validator.validate(&CircuitSource(circuit.to_string())) { Err(e) => return json!({"error": e.to_string()}), Ok(v) => v, }; if !validation.is_valid { let msgs: Vec<&str> = validation.diagnostics.iter().map(|d| d.message.as_str()).collect(); return json!({"error": format!("validation failed: {}", msgs.join("; "))}); } let sim = LocalSimulator::new(); match sim.run(&CircuitSource(circuit.to_string()), shot_count, return_statevector) { Err(e) => json!({"error": e.to_string()}), Ok(result) => { let statevector: Value = match result.statevector { None => Value::Null, Some(sv) => sv.iter().map(|(r, i)| json!([r, i])).collect::>().into(), }; json!({ "counts": result.counts, "shots": result.shots, "execution_time_ms": result.execution_time_ms, "statevector": statevector, "backend": "local_simulator", }) } } } ``` - [ ] **Step 4: Run tests** ```bash cargo test tools::run_circuit 2>&1 ``` Expected: 8 tests pass. - [ ] **Step 5: Commit** ```bash git add src/tools/run_circuit.rs git commit -m "feat: implement run_circuit tool handler" ``` --- ## Task 9: MCP Server — main.rs + rmcp wiring **Files:** - Modify: `src/tools/mod.rs` (replace stub with full QuantumBridgeServer) - Modify: `src/main.rs` (replace stub with async tokio main) - [ ] **Step 1: Replace src/tools/mod.rs with full server implementation** ```rust pub mod list_backends; pub mod run_circuit; pub mod validate_circuit; use rmcp::{ ErrorData as McpError, ServerHandler, handler::server::{router::tool::ToolRouter, wrapper::Parameters}, model::*, tool, tool_handler, tool_router, }; use schemars::JsonSchema; use serde::Deserialize; use crate::tools::list_backends::list_backends_response; use crate::tools::run_circuit::run_circuit_response; use crate::tools::validate_circuit::validate_circuit_response; #[derive(Debug, Deserialize, JsonSchema)] pub struct ValidateCircuitParams { /// OpenQASM 3.0 source string to validate. pub circuit: String, } #[derive(Debug, Deserialize, JsonSchema)] pub struct RunCircuitParams { /// OpenQASM 3.0 source string to execute. pub circuit: String, /// Number of measurement shots (default 1024, max 100 000). #[serde(default = "default_shots")] pub shots: u32, /// Return the full statevector in the response. #[serde(default)] pub return_statevector: bool, } fn default_shots() -> u32 { 1024 } #[derive(Clone)] pub struct QuantumBridgeServer { tool_router: ToolRouter, } #[tool_router] impl QuantumBridgeServer { pub fn new() -> Self { Self { tool_router: Self::tool_router() } } #[tool(description = "List available quantum simulation backends and their capabilities.")] async fn list_backends(&self) -> Result { let json = list_backends_response(); Ok(CallToolResult::success(vec![Content::text(json.to_string())])) } #[tool(description = "Validate an OpenQASM 3.0 circuit. Returns structured diagnostics with line/column.")] async fn validate_circuit( &self, Parameters(ValidateCircuitParams { circuit }): Parameters, ) -> Result { let json = validate_circuit_response(&circuit); Ok(CallToolResult::success(vec![Content::text(json.to_string())])) } #[tool(description = "Execute an OpenQASM 3.0 circuit on the local statevector simulator. Returns counts, execution time, and optionally the full statevector.")] async fn run_circuit( &self, Parameters(RunCircuitParams { circuit, shots, return_statevector }): Parameters, ) -> Result { let json = run_circuit_response(&circuit, shots, return_statevector); if json.get("error").is_some() { return Err(McpError::invalid_params( json["error"].as_str().unwrap_or("unknown error"), None, )); } Ok(CallToolResult::success(vec![Content::text(json.to_string())])) } } #[tool_handler] impl ServerHandler for QuantumBridgeServer { fn get_info(&self) -> ServerInfo { ServerInfo::new( ServerCapabilities::builder().enable_tools().build(), ) .with_server_info(Implementation::from_build_env()) .with_protocol_version(ProtocolVersion::V_2024_11_05) .with_instructions( "Quantum circuit simulator. Accepts OpenQASM 3.0. \ Run validate_circuit first to get actionable errors.".to_string(), ) } } #[cfg(test)] mod tests { use super::*; #[test] fn quantum_bridge_server_can_be_constructed() { let _server = QuantumBridgeServer::new(); } } ``` - [ ] **Step 2: Run compile test** ```bash cargo test tools::tests 2>&1 ``` Expected: passes. If macro names are wrong (`#[tool_router]` / `#[tool_handler]`), check `~/.cargo/git/checkouts/rust-sdk-*/examples/servers/src/common/` for the canonical pattern. - [ ] **Step 3: Replace src/main.rs with async server entry point** ```rust mod error; mod executor; mod tools; mod types; mod validator; use anyhow::Result; use rmcp::{ServiceExt, transport::stdio}; use tools::QuantumBridgeServer; #[tokio::main] async fn main() -> Result<()> { // Logs go to stderr — stdout is the MCP JSON-RPC channel. tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_ansi(false) .with_env_filter( tracing_subscriber::EnvFilter::from_default_env() .add_directive(tracing::Level::INFO.into()), ) .init(); tracing::info!("quantum-bridge-mcp starting"); let service = QuantumBridgeServer::new().serve(stdio()).await?; service.waiting().await?; Ok(()) } ``` - [ ] **Step 4: Build the binary** ```bash cargo build 2>&1 ``` Expected: compiles without errors. - [ ] **Step 5: Smoke test** ```bash echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}' \ | cargo run 2>/dev/null ``` Expected: JSON response with `"result"."serverInfo"."name":"quantum-bridge-mcp"`. Press Ctrl+C to stop. - [ ] **Step 6: Run full test suite** ```bash cargo test 2>&1 ``` Expected: all tests pass (3 list_backends + 5 validate_circuit + 8 run_circuit + 1 server construction + 7 executor + 5 validator). - [ ] **Step 7: Commit** ```bash git add src/main.rs src/tools/mod.rs git commit -m "feat: wire rmcp stdio server with list_backends/validate_circuit/run_circuit tools" ``` --- ## Final verification ```bash cargo fmt --check && cargo clippy -- -D warnings && cargo test ``` Expected: all green. Sub-plan 4 complete — hand off to sub-plan 5.