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/tools",
54 auth: "bearer",
55 summary: "Paginated tool list",
56 },
57 EndpointDoc {
58 method: "POST",
59 path: "/v1/tools/call",
60 auth: "bearer",
61 summary: "Execute a single tool",
62 },
63 EndpointDoc {
64 method: "GET",
65 path: "/v1/events",
66 auth: "bearer",
67 summary: "SSE stream with replay",
68 },
69 EndpointDoc {
70 method: "GET",
71 path: "/v1/context/summary",
72 auth: "bearer",
73 summary: "Materialized workspace/channel summary",
74 },
75 EndpointDoc {
76 method: "GET",
77 path: "/v1/events/search",
78 auth: "bearer",
79 summary: "Full-text search over event payloads",
80 },
81 EndpointDoc {
82 method: "GET",
83 path: "/v1/events/lineage",
84 auth: "bearer",
85 summary: "Causal lineage chain for an event",
86 },
87 EndpointDoc {
88 method: "GET",
89 path: "/v1/metrics",
90 auth: "bearer",
91 summary: "JSON metrics snapshot (slo block; ?format=prometheus for text exposition)",
92 },
93 ]
94}
95
96pub fn openapi_value() -> Value {
98 let mut paths: Map<String, Value> = Map::new();
99
100 for e in endpoints() {
101 let security = if e.auth.contains("bearer") {
102 json!([{ "bearerAuth": [] }])
103 } else {
104 json!([])
105 };
106 let operation = json!({
107 "summary": e.summary,
108 "security": security,
109 "responses": { "200": { "description": "OK" } },
110 });
111
112 let entry = paths
113 .entry(e.path.to_string())
114 .or_insert_with(|| Value::Object(Map::new()));
115 if let Some(obj) = entry.as_object_mut() {
116 obj.insert(e.method.to_lowercase(), operation);
117 }
118 }
119
120 json!({
121 "openapi": "3.0.3",
122 "info": {
123 "title": "lean-ctx HTTP/MCP API",
124 "version": env!("CARGO_PKG_VERSION"),
125 "description": "Public /v1 surface of the lean-ctx Context OS. \
126 Full contract: docs/contracts/http-mcp-contract-v1.md. \
127 Discover instance features at GET /v1/capabilities.",
128 },
129 "components": {
130 "securitySchemes": {
131 "bearerAuth": { "type": "http", "scheme": "bearer" }
132 }
133 },
134 "paths": Value::Object(paths),
135 })
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn document_is_well_formed() {
144 let v = openapi_value();
145 assert_eq!(v["openapi"], json!("3.0.3"));
146 assert!(v["paths"].as_object().is_some_and(|p| !p.is_empty()));
147 assert!(v["components"]["securitySchemes"]["bearerAuth"].is_object());
148 }
149
150 #[test]
151 fn every_endpoint_is_present() {
152 let v = openapi_value();
153 let paths = v["paths"].as_object().expect("paths object");
154 for e in endpoints() {
155 let op = &paths[e.path][e.method.to_lowercase()];
156 assert!(
157 op.is_object(),
158 "missing {} {} in OpenAPI paths",
159 e.method,
160 e.path
161 );
162 }
163 }
164
165 #[test]
166 fn bearer_endpoints_require_security() {
167 let v = openapi_value();
168 let paths = v["paths"].as_object().unwrap();
169 let manifest = &paths["/v1/manifest"]["get"];
170 assert_eq!(manifest["security"], json!([{ "bearerAuth": [] }]));
171 let health = &paths["/health"]["get"];
172 assert_eq!(health["security"], json!([]));
173 }
174}