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 /// Listen address (`--bind` value).
20 pub bind: String,
21 /// Backend type: `"git2"` | `"in_memory"`.
22 pub blueprint_backend: String,
23 /// Git backend root (Git2 only). `None` for InMemory.
24 pub blueprint_store_root: Option<String>,
25 /// `--blueprint-ref-base` (= base dir for `$agent_md` / `$file` expansion).
26 pub blueprint_ref_base: Option<String>,
27 /// `--enable-enhance-flow` on/off.
28 pub enhance_flow_enabled: bool,
29 /// Seed blueprint id (= combined mode default).
30 pub seed_blueprint_id: String,
31}
32
33#[derive(Clone)]
34struct DoctorState {
35 info: Arc<DoctorInfo>,
36 store: Arc<dyn BlueprintStore>,
37}
38
39/// Builds the `/v1/doctor` router. `info` is the (immutable) startup snapshot
40/// to serve; `store` is used to peek the registered Blueprint id count/list.
41pub fn build_doctor_router(info: DoctorInfo, store: Arc<dyn BlueprintStore>) -> Router {
42 let state = DoctorState {
43 info: Arc::new(info),
44 store,
45 };
46 Router::new()
47 .route("/v1/doctor", get(doctor_get))
48 .with_state(state)
49}
50
51#[derive(Serialize)]
52struct DoctorResponse {
53 #[serde(flatten)]
54 info: DoctorInfo,
55 /// Registered BP id list (best-effort; currently returns empty for the InMemory backend).
56 registered_blueprint_ids: Vec<String>,
57 registered_blueprint_count: usize,
58}
59
60async fn doctor_get(State(state): State<DoctorState>) -> Json<DoctorResponse> {
61 // `store.list_ids()` applies the archive filter (archived ids are excluded by default).
62 // The InMemory backend is expected to return Ok(vec![]).
63 let mut ids: Vec<String> = state
64 .store
65 .list_ids()
66 .await
67 .map(|v| v.into_iter().map(|id| id.to_string()).collect())
68 .unwrap_or_default();
69 ids.sort();
70 let count = ids.len();
71 Json(DoctorResponse {
72 info: (*state.info).clone(),
73 registered_blueprint_ids: ids,
74 registered_blueprint_count: count,
75 })
76}