Files
Vincent Bourdon 9af114e391 Initial import
2026-06-09 16:14:55 +02:00

158 lines
6.0 KiB
Markdown
Raw Permalink 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.
# Task 10 — Wire 4 tutor tools into `QuantumBridgeServer`
> **Index:** [README](README.md). **Spec:** [design](../../specs/2026-04-29-quantum-tutor-design.md).
## Goal
Register `get_lesson`, `check_exercise`, `explain_result`, `get_progress` as MCP tools. The wrapper:
1. Loads `UserProgress` via `ProgressStore` (sandboxed by env var).
2. Routes through `self.backend` (set up in Task 0).
3. Maps `protocol_error` to `McpError::invalid_params`; all other errors stay in the structured payload.
4. Persists `mark_solved` on `passed: true`.
## Prerequisites
- Task 9 merged (all 4 response functions exist in `tutor_tools.rs`).
## Files
- Modify: `src/tools/mod.rs`
## Steps
- [ ] **Step 1: Add imports + parameter structs at the top of `src/tools/mod.rs`**
```rust
use crate::progress::ProgressStore;
use crate::tutor::CurriculumLoader;
use crate::tools::tutor_tools;
#[derive(Debug, Deserialize, JsonSchema)]
pub struct GetLessonParams {
/// Module number (17).
pub module_id: u32,
/// Lesson number within the module (optional, defaults to first lesson with pending exercises).
pub lesson_id: Option<u32>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct CheckExerciseParams {
/// Exercise identifier, e.g. "1-1-a".
pub exercise_id: String,
/// OpenQASM 3.0 source of the circuit to verify.
pub circuit: String,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct ExplainResultParams {
/// OpenQASM 3.0 source of the circuit that was executed.
pub circuit: String,
/// Measurement counts from run_circuit (bitstring → count).
pub counts: serde_json::Value,
/// Optional statevector from run_circuit with return_statevector=true.
pub statevector: Option<serde_json::Value>,
}
```
- [ ] **Step 2: Add helpers on `QuantumBridgeServer` (outside the `#[tool_router]` block)**
```rust
impl QuantumBridgeServer {
fn loader(&self) -> CurriculumLoader { CurriculumLoader::default() }
fn store(&self) -> Result<ProgressStore, McpError> {
let path = ProgressStore::default_path()
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
Ok(ProgressStore::new(path))
}
}
```
- [ ] **Step 3: Add the 4 tool methods inside `#[tool_router(server_handler)] impl QuantumBridgeServer`**
```rust
#[tool(description = "Get a quantum-computing lesson with concept, example circuit, and the next pending exercise.")]
async fn get_lesson(
&self,
Parameters(GetLessonParams { module_id, lesson_id }): Parameters<GetLessonParams>,
) -> Result<CallToolResult, McpError> {
let store = self.store()?;
let progress = store.load();
let json = tutor_tools::get_lesson_response(&self.loader(), &progress, module_id, lesson_id);
if let Some(err) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) {
return Err(McpError::invalid_params(err, None));
}
Ok(CallToolResult::success(vec![Content::text(json.to_string())]))
}
#[tool(description = "Verify a submitted OpenQASM 3.0 circuit against a curriculum exercise. Returns pass/fail with feedback.")]
async fn check_exercise(
&self,
Parameters(CheckExerciseParams { exercise_id, circuit }): Parameters<CheckExerciseParams>,
) -> Result<CallToolResult, McpError> {
let store = self.store()?;
let progress = store.load();
let mut json = tutor_tools::check_exercise_response(
&self.loader(),
self.backend.as_ref(),
&progress,
&exercise_id,
&circuit,
);
if let Some(err) = json.get("protocol_error").and_then(|v| v.as_str()).map(str::to_owned) {
return Err(McpError::invalid_params(err, None));
}
if json["passed"].as_bool().unwrap_or(false) {
store
.mark_solved(&exercise_id)
.map_err(|e| McpError::internal_error(e.to_string(), None))?;
json["progress_updated"] = serde_json::json!(true);
}
Ok(CallToolResult::success(vec![Content::text(json.to_string())]))
}
#[tool(description = "Analyze a circuit and its measurement results to produce structured pedagogical data (gate breakdown, key concept, outcome stats).")]
async fn explain_result(
&self,
Parameters(ExplainResultParams { circuit, counts, statevector }): Parameters<ExplainResultParams>,
) -> Result<CallToolResult, McpError> {
let json = tutor_tools::explain_result_response(&self.loader(), &circuit, &counts, statevector.as_ref());
if let Some(err) = json.get("error").and_then(|v| v.as_str()).map(str::to_owned) {
return Err(McpError::invalid_params(err, None));
}
Ok(CallToolResult::success(vec![Content::text(json.to_string())]))
}
#[tool(description = "Get the learner's progress through the curriculum (modules status, current lesson, percent complete).")]
async fn get_progress(&self) -> Result<CallToolResult, McpError> {
let store = self.store()?;
let progress = store.load();
let json = tutor_tools::get_progress_response(&self.loader(), &progress);
Ok(CallToolResult::success(vec![Content::text(json.to_string())]))
}
```
- [ ] **Step 4: Build and run all unit tests**
```bash
cargo build && cargo test --lib 2>&1 | grep -E "test result|FAILED"
```
Expected: every existing suite green; the 4 new methods compile and the existing tests are untouched.
- [ ] **Step 5: Verify the MCP `tools/list` exposes 7 tools**
```bash
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.0.1"}}}
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' | timeout 3 cargo run 2>/dev/null | tail -1 | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d['result']['tools']), 'tools')"
```
Expected: `7 tools`.
- [ ] **Step 6: Commit**
```bash
git add src/tools/mod.rs
git commit -m "feat: register 4 tutor tools on QuantumBridgeServer"
```