use serde_json::{Map, Value, json};
pub struct EndpointDoc {
pub method: &'static str,
pub path: &'static str,
pub auth: &'static str,
pub summary: &'static str,
}
pub fn endpoints() -> Vec<EndpointDoc> {
vec![
EndpointDoc {
method: "GET",
path: "/health",
auth: "none",
summary: "Liveness probe",
},
EndpointDoc {
method: "GET",
path: "/v1/manifest",
auth: "bearer",
summary: "Full MCP manifest",
},
EndpointDoc {
method: "GET",
path: "/v1/capabilities",
auth: "bearer",
summary: "Instance capabilities discovery",
},
EndpointDoc {
method: "GET",
path: "/v1/openapi.json",
auth: "bearer",
summary: "OpenAPI 3.0 spec for this surface",
},
EndpointDoc {
method: "GET",
path: "/v1/cache/stats",
auth: "bearer",
summary: "Cross-agent cache and delivery statistics",
},
EndpointDoc {
method: "GET",
path: "/v1/tools",
auth: "bearer",
summary: "Paginated tool list",
},
EndpointDoc {
method: "POST",
path: "/v1/tools/call",
auth: "bearer",
summary: "Execute a single tool",
},
EndpointDoc {
method: "GET",
path: "/v1/events",
auth: "bearer",
summary: "SSE stream with replay",
},
EndpointDoc {
method: "GET",
path: "/v1/context/summary",
auth: "bearer",
summary: "Materialized workspace/channel summary",
},
EndpointDoc {
method: "GET",
path: "/v1/events/search",
auth: "bearer",
summary: "Full-text search over event payloads",
},
EndpointDoc {
method: "GET",
path: "/v1/events/lineage",
auth: "bearer",
summary: "Causal lineage chain for an event",
},
EndpointDoc {
method: "GET",
path: "/v1/metrics",
auth: "bearer",
summary: "JSON metrics snapshot (slo block; ?format=prometheus for text exposition)",
},
]
}
pub fn openapi_value() -> Value {
let mut paths: Map<String, Value> = Map::new();
for e in endpoints() {
let security = if e.auth.contains("bearer") {
json!([{ "bearerAuth": [] }])
} else {
json!([])
};
let operation = json!({
"summary": e.summary,
"security": security,
"responses": { "200": { "description": "OK" } },
});
let entry = paths
.entry(e.path.to_string())
.or_insert_with(|| Value::Object(Map::new()));
if let Some(obj) = entry.as_object_mut() {
obj.insert(e.method.to_lowercase(), operation);
}
}
json!({
"openapi": "3.0.3",
"info": {
"title": "lean-ctx HTTP/MCP API",
"version": env!("CARGO_PKG_VERSION"),
"description": "Public /v1 surface of the lean-ctx Context OS. \
Full contract: docs/contracts/http-mcp-contract-v1.md. \
Discover instance features at GET /v1/capabilities.",
},
"components": {
"securitySchemes": {
"bearerAuth": { "type": "http", "scheme": "bearer" }
}
},
"paths": Value::Object(paths),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn document_is_well_formed() {
let v = openapi_value();
assert_eq!(v["openapi"], json!("3.0.3"));
assert!(v["paths"].as_object().is_some_and(|p| !p.is_empty()));
assert!(v["components"]["securitySchemes"]["bearerAuth"].is_object());
}
#[test]
fn every_endpoint_is_present() {
let v = openapi_value();
let paths = v["paths"].as_object().expect("paths object");
for e in endpoints() {
let op = &paths[e.path][e.method.to_lowercase()];
assert!(
op.is_object(),
"missing {} {} in OpenAPI paths",
e.method,
e.path
);
}
}
#[test]
fn bearer_endpoints_require_security() {
let v = openapi_value();
let paths = v["paths"].as_object().unwrap();
let manifest = &paths["/v1/manifest"]["get"];
assert_eq!(manifest["security"], json!([{ "bearerAuth": [] }]));
let health = &paths["/health"]["get"];
assert_eq!(health["security"], json!([]));
}
}