systemprompt_api/services/server/
discovery.rs1use axum::routing::get;
9use axum::{Json, Router};
10use metrics_exporter_prometheus::PrometheusHandle;
11use serde_json::json;
12use systemprompt_models::api::SingleResponse;
13use systemprompt_models::modules::ApiPaths;
14use systemprompt_runtime::AppContext;
15
16use super::health::handle_health;
17use super::health_detail::handle_health_detail;
18use super::metrics::handle_metrics;
19
20pub async fn handle_root_discovery(
21 axum::extract::State(ctx): axum::extract::State<AppContext>,
22) -> impl axum::response::IntoResponse {
23 let base = &ctx.config().api_external_url;
24 let data = json!({
25 "name": format!("{} API", ctx.config().sitename),
26 "version": env!("CARGO_PKG_VERSION"),
27 "description": "systemprompt.io OS API Gateway",
28 "endpoints": {
29 "health": format!("{}{}", base, ApiPaths::HEALTH),
30 "oauth": {
31 "href": format!("{}{}", base, ApiPaths::OAUTH_BASE),
32 "description": "OAuth2/OIDC authentication and WebAuthn",
33 "endpoints": {
34 "authorize": format!("{}{}", base, ApiPaths::OAUTH_AUTHORIZE),
35 "token": format!("{}{}", base, ApiPaths::OAUTH_TOKEN),
36 "userinfo": format!("{}{}/userinfo", base, ApiPaths::OAUTH_BASE),
37 "introspect": format!("{}{}/introspect", base, ApiPaths::OAUTH_BASE),
38 "revoke": format!("{}{}/revoke", base, ApiPaths::OAUTH_BASE),
39 "webauthn": format!("{}{}/webauthn", base, ApiPaths::OAUTH_BASE)
40 }
41 },
42 "core": {
43 "href": format!("{}{}", base, ApiPaths::CORE_BASE),
44 "description": "Core conversation, task, and artifact management",
45 "endpoints": {
46 "contexts": format!("{}{}", base, ApiPaths::CORE_CONTEXTS),
47 "tasks": format!("{}{}", base, ApiPaths::CORE_TASKS),
48 "artifacts": format!("{}{}", base, ApiPaths::CORE_ARTIFACTS)
49 }
50 },
51 "agents": {
52 "href": format!("{}{}", base, ApiPaths::AGENTS_REGISTRY),
53 "description": "A2A protocol agent registry and proxy",
54 "endpoints": {
55 "registry": format!("{}{}", base, ApiPaths::AGENTS_REGISTRY),
56 "proxy": format!("{}{}{{agent_id}}", base, ApiPaths::AGENTS_BASE)
57 }
58 },
59 "mcp": {
60 "href": format!("{}{}", base, ApiPaths::MCP_REGISTRY),
61 "description": "MCP server registry and lifecycle management",
62 "endpoints": {
63 "registry": format!("{}{}", base, ApiPaths::MCP_REGISTRY),
64 "proxy": format!("{}{}{{server_name}}", base, ApiPaths::MCP_BASE)
65 }
66 },
67 "stream": {
68 "href": format!("{}{}", base, ApiPaths::STREAM_BASE),
69 "description": "Server-Sent Events (SSE) for real-time updates",
70 "endpoints": {
71 "contexts": format!("{}{}", base, ApiPaths::STREAM_CONTEXTS)
72 }
73 }
74 },
75 "wellknown": {
76 "oauth": format!("{}{}", base, ApiPaths::WELLKNOWN_OAUTH_SERVER),
77 "agent": format!("{}{}", base, ApiPaths::WELLKNOWN_AGENT_CARD)
78 }
79 });
80
81 Json(SingleResponse::new(data))
82}
83
84pub async fn handle_core_discovery(
85 axum::extract::State(ctx): axum::extract::State<AppContext>,
86) -> impl axum::response::IntoResponse {
87 let base = &ctx.config().api_external_url;
88 let data = json!({
89 "name": "Core Services",
90 "description": "Core conversation, task, and artifact management APIs",
91 "endpoints": {
92 "contexts": {
93 "href": format!("{}{}", base, ApiPaths::CORE_CONTEXTS),
94 "description": "Conversation context management",
95 "methods": ["GET", "POST", "DELETE"]
96 },
97 "tasks": {
98 "href": format!("{}{}", base, ApiPaths::CORE_TASKS),
99 "description": "Task management for agent operations",
100 "methods": ["GET", "POST", "PUT", "DELETE"]
101 },
102 "artifacts": {
103 "href": format!("{}{}", base, ApiPaths::CORE_ARTIFACTS),
104 "description": "Artifact storage and retrieval",
105 "methods": ["GET", "POST", "DELETE"]
106 },
107 "oauth": {
108 "href": format!("{}{}", base, ApiPaths::OAUTH_BASE),
109 "description": "OAuth2/OIDC authentication endpoints"
110 }
111 }
112 });
113 Json(SingleResponse::new(data))
114}
115
116pub async fn handle_agents_discovery(
117 axum::extract::State(ctx): axum::extract::State<AppContext>,
118) -> impl axum::response::IntoResponse {
119 let base = &ctx.config().api_external_url;
120 let data = json!({
121 "name": "Agent Services",
122 "description": "A2A protocol agent registry and proxy",
123 "endpoints": {
124 "registry": {
125 "href": format!("{}{}", base, ApiPaths::AGENTS_REGISTRY),
126 "description": "List and discover available agents",
127 "methods": ["GET"]
128 },
129 "proxy": {
130 "href": format!("{}{}/<agent_id>/", base, ApiPaths::AGENTS_BASE),
131 "description": "Proxy requests to specific agents",
132 "methods": ["GET", "POST"]
133 }
134 }
135 });
136 Json(SingleResponse::new(data))
137}
138
139pub async fn handle_mcp_discovery(
140 axum::extract::State(ctx): axum::extract::State<AppContext>,
141) -> impl axum::response::IntoResponse {
142 let base = &ctx.config().api_external_url;
143 let data = json!({
144 "name": "MCP Services",
145 "description": "Model Context Protocol server registry and proxy",
146 "endpoints": {
147 "registry": {
148 "href": format!("{}{}", base, ApiPaths::MCP_REGISTRY),
149 "description": "List and discover available MCP servers",
150 "methods": ["GET"]
151 },
152 "proxy": {
153 "href": format!("{}{}/<server_name>/mcp", base, ApiPaths::MCP_BASE),
154 "description": "Proxy requests to specific MCP servers",
155 "methods": ["GET", "POST"]
156 }
157 }
158 });
159 Json(SingleResponse::new(data))
160}
161
162pub fn discovery_router(ctx: &AppContext, metrics_handle: PrometheusHandle) -> Router {
163 let metrics_route = Router::new()
164 .route("/metrics", get(handle_metrics))
165 .with_state(metrics_handle);
166
167 Router::new()
168 .route(ApiPaths::DISCOVERY, get(handle_root_discovery))
169 .route(ApiPaths::HEALTH, get(handle_health))
170 .route("/health", get(handle_health))
171 .route(ApiPaths::CORE_BASE, get(handle_core_discovery))
172 .route(ApiPaths::AGENTS_BASE, get(handle_agents_discovery))
173 .route(ApiPaths::MCP_BASE, get(handle_mcp_discovery))
174 .with_state(ctx.clone())
175 .merge(metrics_route)
176}
177
178pub fn authenticated_discovery_router(ctx: &AppContext) -> Router {
179 Router::new()
180 .route("/api/v1/health/detail", get(handle_health_detail))
181 .with_state(ctx.clone())
182}