Skip to main content

mlua_swarm_server/
doctor.rs

1//! `GET /v1/doctor` — server-side infra info snapshot.
2//!
3//! Surfaces startup config read-only: the Blueprint store real (backend /
4//! root path), ref_base, bind, enhance flow on/off, etc. An entry point for
5//! callers (the MCP adapter's doctor tool / operator) to answer "where is the Store?"
6//! and "how many BPs are registered?" in one shot.
7//!
8//! Store contents (BP list / head / history) are peeked via the existing
9//! `/v1/blueprints/...` routes; doctor covers only the infra layer.
10
11use axum::{extract::State, routing::get, Json, Router};
12use mlua_swarm::blueprint::store::BlueprintStore;
13use serde::Serialize;
14use std::sync::Arc;
15
16/// Startup config snapshot. Populated from `Args` in `main.rs` and mounted on the router.
17#[derive(Clone, Serialize)]
18pub struct DoctorInfo {
19    /// Version of the `mse` binary this **running** server process was
20    /// built from. Distinct from whatever `mse --version` reports on
21    /// disk: a long-lived launchd `mse serve` keeps serving its original
22    /// vintage across a `cargo install`, so this is the only way to tell
23    /// what is actually answering requests. Paired with `mse mcp`'s own
24    /// version in the `mse_doctor` tool's drift check.
25    pub server_version: String,
26    /// Listen address (`--bind` value).
27    pub bind: String,
28    /// Backend type: `"git2"` | `"in_memory"`.
29    pub blueprint_backend: String,
30    /// Git backend root (Git2 only). `None` for InMemory.
31    pub blueprint_store_root: Option<String>,
32    /// `--blueprint-ref-base` (= base dir for `$agent_md` / `$file` expansion).
33    pub blueprint_ref_base: Option<String>,
34    /// `--enable-enhance-flow` on/off.
35    pub enhance_flow_enabled: bool,
36    /// Fresh-launch migration policy for deprecated `profile.worker_binding`.
37    pub legacy_worker_binding_policy: mlua_swarm::LegacyWorkerBindingPolicy,
38    /// Seed blueprint id (= combined mode default).
39    pub seed_blueprint_id: String,
40    /// Server-wide [`mlua_swarm::core::config::CheckPolicy`] resolved
41    /// from CLI flag > config file > built-in default (`Warn`). See
42    /// `mlua_swarm_server::config::ResolvedConfig.check_policy` for the
43    /// full cascade. Serialised as snake_case (`"silent"` / `"warn"` /
44    /// `"strict"`).
45    pub check_policy: mlua_swarm::core::config::CheckPolicy,
46}
47
48#[derive(Clone)]
49struct DoctorState {
50    info: Arc<DoctorInfo>,
51    store: Arc<dyn BlueprintStore>,
52}
53
54/// Builds the `/v1/doctor` router. `info` is the (immutable) startup snapshot
55/// to serve; `store` is used to peek the registered Blueprint id count/list.
56pub fn build_doctor_router(info: DoctorInfo, store: Arc<dyn BlueprintStore>) -> Router {
57    let state = DoctorState {
58        info: Arc::new(info),
59        store,
60    };
61    Router::new()
62        .route("/v1/doctor", get(doctor_get))
63        .with_state(state)
64}
65
66#[derive(Serialize)]
67struct DoctorResponse {
68    #[serde(flatten)]
69    info: DoctorInfo,
70    /// Registered BP id list (best-effort; currently returns empty for the InMemory backend).
71    registered_blueprint_ids: Vec<String>,
72    registered_blueprint_count: usize,
73}
74
75async fn doctor_get(State(state): State<DoctorState>) -> Json<DoctorResponse> {
76    // `store.list_ids()` applies the archive filter (archived ids are excluded by default).
77    // The InMemory backend is expected to return Ok(vec![]).
78    let mut ids: Vec<String> = state
79        .store
80        .list_ids()
81        .await
82        .map(|v| v.into_iter().map(|id| id.to_string()).collect())
83        .unwrap_or_default();
84    ids.sort();
85    let count = ids.len();
86    Json(DoctorResponse {
87        info: (*state.info).clone(),
88        registered_blueprint_ids: ids,
89        registered_blueprint_count: count,
90    })
91}