6.0 KiB
6.0 KiB
Task 10 — Wire 4 tutor tools into QuantumBridgeServer
Goal
Register get_lesson, check_exercise, explain_result, get_progress as MCP tools. The wrapper:
- Loads
UserProgressviaProgressStore(sandboxed by env var). - Routes through
self.backend(set up in Task 0). - Maps
protocol_errortoMcpError::invalid_params; all other errors stay in the structured payload. - Persists
mark_solvedonpassed: 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 (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)
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/listexposes 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"