Files
quantum-bridge-mcp/docs/superpowers/plans/sub4-tools-server.md
T
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

609 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sub-plan 4 — Tools + MCP Server (Tasks 69)
> **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 13 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<MyParams>` where `MyParams` derives `Deserialize + JsonSchema`
- Return: `Result<CallToolResult, McpError>` — success via `CallToolResult::success(vec![Content::text("...")])`, error via `McpError::invalid_params("msg", None)`
- Server field: `tool_router: ToolRouter<Self>` 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 69.
---
## 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<Value> = 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::<Vec<_>>().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<QuantumBridgeServer>,
}
#[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<CallToolResult, McpError> {
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<ValidateCircuitParams>,
) -> Result<CallToolResult, McpError> {
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<RunCircuitParams>,
) -> Result<CallToolResult, McpError> {
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.