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