Skip to main content

headless_lms_mcp/handlers/exercise/
mod.rs

1//! The exercise tools: `get_exercise` and `patch_exercise` read and edit an exercise's name, own
2//! deadline, task instructions and review instructions, and `list_exercises` lists a course's
3//! exercises. Everything else of an exercise is read-only for agents.
4
5mod get;
6mod list;
7mod patch;
8mod paths;
9mod review_config;
10mod specs;
11
12use chrono::{DateTime, Utc};
13use headless_lms_models::library::agent_changesets::course_state::{CourseState, CourseView};
14use serde::Serialize;
15use uuid::Uuid;
16
17use crate::access::{connection, open_course};
18use crate::state::McpState;
19use crate::tools::{RequestContext, ToolError};
20
21pub use get::GetExercise;
22pub use list::ListExercises;
23pub use patch::PatchExercise;
24
25const REVIEW_INSTRUCTIONS: &str = "review_instructions";
26
27/// The course with the records of the exercises on the pages holding `exercise_id` loaded.
28async fn open_course_with_exercise(
29    state: &McpState,
30    ctx: &RequestContext,
31    course_id: Uuid,
32    exercise_id: Uuid,
33) -> Result<CourseView, ToolError> {
34    let mut view = open_course(state, ctx, course_id).await?;
35    let page_ids: Vec<Uuid> = [
36        view.staged.exercise_page_id(exercise_id),
37        view.live.exercise_page_id(exercise_id),
38    ]
39    .into_iter()
40    .flatten()
41    .collect();
42    view.load_exercises_on_pages(&mut *connection(state).await?, &page_ids)
43        .await?;
44    Ok(view)
45}
46
47fn missing_exercise(view: &CourseView, exercise_id: Uuid) -> ToolError {
48    ToolError::NotFound {
49        message: format!(
50            "course {} has no exercise {exercise_id} on its pages; exercise blocks in get_page_content show exercise ids, and exam exercises can't be edited here",
51            view.course.id
52        ),
53    }
54}
55
56/// An exercise's own deadline and the one that applies to it, its chapter's without an own one.
57#[derive(Serialize)]
58struct ExerciseDeadline {
59    own: Option<DateTime<Utc>>,
60    effective: Option<DateTime<Utc>>,
61}
62
63impl ExerciseDeadline {
64    fn of(state: &CourseState, exercise_id: Uuid) -> Self {
65        Self {
66            own: state.exercise(exercise_id).and_then(|e| e.deadline),
67            effective: state.effective_exercise_deadline(exercise_id),
68        }
69    }
70}