1use serde_json::{Map, Value, json};
13
14pub struct EndpointDoc {
16 pub method: &'static str,
17 pub path: &'static str,
18 pub auth: &'static str,
20 pub summary: &'static str,
21}
22
23pub fn endpoints() -> Vec<EndpointDoc> {
26 vec![
27 EndpointDoc {
28 method: "GET",
29 path: "/health",
30 auth: "none",
31 summary: "Liveness probe",
32 },
33 EndpointDoc {
34 method: "GET",
35 path: "/v1/manifest",
36 auth: "bearer",
37 summary: "Full MCP manifest",
38 },
39 EndpointDoc {
40 method: "GET",
41 path: "/v1/capabilities",
42 auth: "bearer",
43 summary: "Instance capabilities discovery",
44 },
45 EndpointDoc {
46 method: "GET",
47 path: "/v1/openapi.json",
48 auth: "bearer",
49 summary: "OpenAPI 3.0 spec for this surface",
50 },
51 EndpointDoc {
52 method: "GET",
53 path: "/v1/cache/stats",
54 auth: "bearer",
55 summary: "Cross-agent cache and delivery statistics",
56 },
57 EndpointDoc {
58 method: "GET",
59 path: "/v1/tools",
60 auth: "bearer",
61 summary: "Paginated tool list",
62 },
63 EndpointDoc {
64 method: "POST",
65 path: "/v1/tools/call",
66 auth: "bearer",
67 summary: "Execute a single tool",
68 },
69 EndpointDoc {
70 method: "GET",
71 path: "/v1/events",
72 auth: "bearer",
73 summary: "SSE stream with replay",
74 },
75 EndpointDoc {
76 method: "GET",
77 path: "/v1/context/summary",
78 auth: "bearer",
79 summary: "Materialized workspace/channel summary",
80 },
81 EndpointDoc {
82 method: "GET",
83 path: "/v1/events/search",
84 auth: "bearer",
85 summary: "Full-text search over event payloads",
86 },
87 EndpointDoc {
88 method: "GET",
89 path: "/v1/events/lineage",
90 auth: "bearer",
91 summary: "Causal lineage chain for an event",
92 },
93 EndpointDoc {
94 method: "GET",
95 path: "/v1/metrics",
96 auth: "bearer",
97 summary: "JSON metrics snapshot (slo block; ?format=prometheus for text exposition)",
98 },
99 ]
100}
101
102pub fn openapi_value() -> Value {
104 let mut paths: Map<String, Value> = Map::new();
105
106 for e in endpoints() {
107 let security = if e.auth.contains("bearer") {
108 json!([{ "bearerAuth": [] }])
109 } else {
110 json!([])
111 };
112 let operation = json!({
113 "summary": e.summary,
114 "security": security,
115 "responses": { "200": { "description": "OK" } },
116 });
117
118 let entry = paths
119 .entry(e.path.to_string())
120 .or_insert_with(|| Value::Object(Map::new()));
121 if let Some(obj) = entry.as_object_mut() {
122 obj.insert(e.method.to_lowercase(), operation);
123 }
124 }
125
126 json!({
127 "openapi": "3.0.3",
128 "info": {
129 "title": "lean-ctx HTTP/MCP API",
130 "version": env!("CARGO_PKG_VERSION"),
131 "description": "Public /v1 surface of the lean-ctx Context OS. \
132 Full contract: docs/contracts/http-mcp-contract-v1.md. \
133 Discover instance features at GET /v1/capabilities.",
134 },
135 "components": {
136 "securitySchemes": {
137 "bearerAuth": { "type": "http", "scheme": "bearer" }
138 }
139 },
140 "paths": Value::Object(paths),
141 })
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn document_is_well_formed() {
150 let v = openapi_value();
151 assert_eq!(v["openapi"], json!("3.0.3"));
152 assert!(v["paths"].as_object().is_some_and(|p| !p.is_empty()));
153 assert!(v["components"]["securitySchemes"]["bearerAuth"].is_object());
154 }
155
156 #[test]
157 fn every_endpoint_is_present() {
158 let v = openapi_value();
159 let paths = v["paths"].as_object().expect("paths object");
160 for e in endpoints() {
161 let op = &paths[e.path][e.method.to_lowercase()];
162 assert!(
163 op.is_object(),
164 "missing {} {} in OpenAPI paths",
165 e.method,
166 e.path
167 );
168 }
169 }
170
171 #[test]
172 fn bearer_endpoints_require_security() {
173 let v = openapi_value();
174 let paths = v["paths"].as_object().unwrap();
175 let manifest = &paths["/v1/manifest"]["get"];
176 assert_eq!(manifest["security"], json!([{ "bearerAuth": [] }]));
177 let health = &paths["/health"]["get"];
178 assert_eq!(health["security"], json!([]));
179 }
180}