Skip to main content

headless_lms_mcp/
state.rs

1//! What the tool handlers share: the database, configuration and the services writes depend on.
2
3use std::sync::Arc;
4use std::time::{Duration, Instant};
5
6use futures::future::BoxFuture;
7use headless_lms_base::{config::ApplicationConfiguration, jwt::JwtKey};
8use headless_lms_models::{
9    ModelResult, SpecFetcher, exercise_service_info::ExerciseServiceInfoApi,
10};
11use headless_lms_utils::file_store::FileStore;
12use sqlx::PgPool;
13use tokio::sync::Mutex;
14use url::Url;
15use uuid::Uuid;
16
17use crate::validator::{BlockSchemas, BlockValidator, ValidatorError};
18
19const SCHEMA_CACHE_TTL: Duration = Duration::from_secs(300);
20
21/// Fetches an exercise service's info while publishing saves a page.
22pub type ServiceInfoFetcher =
23    Arc<dyn Fn(Url) -> BoxFuture<'static, ModelResult<ExerciseServiceInfoApi>> + Send + Sync>;
24
25/// The exercise-service calls publishing makes. The server owns their HTTP clients.
26#[derive(Clone)]
27pub struct ExerciseServiceClients {
28    /// A spec fetcher for one publish, under a request id of its own.
29    pub spec_fetcher: Arc<dyn Fn() -> Box<dyn SpecFetcher + Send + Sync> + Send + Sync>,
30    pub fetch_service_info: ServiceInfoFetcher,
31}
32
33/// Everything a tool handler needs besides its request.
34pub struct McpState {
35    pub pool: PgPool,
36    pub app_conf: ApplicationConfiguration,
37    pub jwt_key: Arc<JwtKey>,
38    pub file_store: Arc<dyn FileStore + Send + Sync>,
39    pub validator: Arc<dyn BlockValidator>,
40    pub exercise_services: ExerciseServiceClients,
41    /// Held while fetching, so calls that find the cache stale meanwhile wait for that one fetch.
42    block_schemas: Mutex<Option<(Instant, Arc<BlockSchemas>)>>,
43}
44
45impl McpState {
46    /// The state with an empty block schema cache.
47    pub fn new(
48        pool: PgPool,
49        app_conf: ApplicationConfiguration,
50        jwt_key: Arc<JwtKey>,
51        file_store: Arc<dyn FileStore + Send + Sync>,
52        validator: Arc<dyn BlockValidator>,
53        exercise_services: ExerciseServiceClients,
54    ) -> Self {
55        Self {
56            pool,
57            app_conf,
58            jwt_key,
59            file_store,
60            validator,
61            exercise_services,
62            block_schemas: Mutex::new(None),
63        }
64    }
65
66    /// The page where the teacher reviews, publishes or discards a changeset.
67    pub fn review_url(&self, changeset_id: Uuid) -> String {
68        self.app_conf
69            .public_url(&format!("/manage/agent-changesets/{changeset_id}"))
70    }
71
72    /// The CMS block schemas, cached briefly.
73    pub async fn block_schemas(&self) -> Result<Arc<BlockSchemas>, ValidatorError> {
74        let mut cache = self.block_schemas.lock().await;
75        if let Some((fetched_at, schemas)) = &*cache
76            && fetched_at.elapsed() < SCHEMA_CACHE_TTL
77        {
78            return Ok(schemas.clone());
79        }
80        let schemas = Arc::new(self.validator.block_schemas().await?);
81        *cache = Some((Instant::now(), schemas.clone()));
82        Ok(schemas)
83    }
84}