Skip to main content

headless_lms_server/domain/
mcp_endpoint.rs

1//! Builds the MCP endpoint at `/api/v0/mcp` from the server's configuration.
2
3use std::sync::Arc;
4
5use headless_lms_base::config::{ApplicationConfiguration, McpConfiguration};
6use headless_lms_mcp::{
7    http::McpEndpoint,
8    state::{ExerciseServiceClients, McpState},
9    validator::{BlockValidator, CmsValidatorClient, UnconfiguredValidator},
10};
11use headless_lms_utils::file_store::FileStore;
12use sqlx::PgPool;
13use uuid::Uuid;
14
15use crate::domain::models_requests::{self, JwtKey};
16
17/// The endpoint with its tools. Without a configured block validator, agent writes fail closed and
18/// reads keep working.
19pub fn build_mcp_endpoint(
20    db_pool: PgPool,
21    app_conf: &ApplicationConfiguration,
22    jwt_key: Arc<JwtKey>,
23    file_store: Arc<dyn FileStore + Send + Sync>,
24) -> McpEndpoint {
25    let validator = block_validator(app_conf.mcp_configuration.as_ref());
26    let base_url = app_conf.base_url.clone();
27    let spec_jwt_key = jwt_key.clone();
28    let exercise_services = ExerciseServiceClients {
29        spec_fetcher: Arc::new(move || {
30            Box::new(models_requests::make_spec_fetcher(
31                base_url.clone(),
32                Uuid::new_v4(),
33                spec_jwt_key.clone(),
34            ))
35        }),
36        fetch_service_info: Arc::new(models_requests::fetch_service_info),
37    };
38    let state = McpState::new(
39        db_pool,
40        app_conf.clone(),
41        jwt_key,
42        file_store,
43        validator,
44        exercise_services,
45    );
46    McpEndpoint::new(state)
47}
48
49fn block_validator(mcp_configuration: Option<&McpConfiguration>) -> Arc<dyn BlockValidator> {
50    let Some(mcp_configuration) = mcp_configuration else {
51        return unconfigured("CMS_INTERNAL_URL and AGENT_VALIDATOR_SECRET must both be set".into());
52    };
53    match CmsValidatorClient::new(
54        mcp_configuration.cms_internal_url.clone(),
55        mcp_configuration.agent_validator_secret.clone(),
56    ) {
57        Ok(client) => Arc::new(client),
58        Err(err) => unconfigured(format!("couldn't build the validator's HTTP client: {err}")),
59    }
60}
61
62fn unconfigured(reason: String) -> Arc<dyn BlockValidator> {
63    warn!("Agent writes through MCP are disabled: {reason}");
64    Arc::new(UnconfiguredValidator { reason })
65}