158 lines
6.0 KiB
Markdown
158 lines
6.0 KiB
Markdown
# 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 (1–7).
|
||
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"
|
||
```
|