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

6.0 KiB
Raw Permalink Blame History

Task 10 — Wire 4 tutor tools into QuantumBridgeServer

Index: README. Spec: design.

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
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)
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
    #[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
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
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
git add src/tools/mod.rs
git commit -m "feat: register 4 tutor tools on QuantumBridgeServer"