Skip to main content

systemprompt_api/services/server/
discovery.rs

1//! Hypermedia discovery and health-routing endpoints.
2//!
3//! Serves the root, core, agents, and MCP discovery documents that advertise
4//! the gateway's endpoint topology (URLs derived from the configured external
5//! base), and assembles the public [`discovery_router`] and the
6//! [`authenticated_discovery_router`] for the detailed health surface.
7
8use 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}