lean_ctx/gateway_server/mcp/
admin.rs1use std::sync::Arc;
7
8use axum::extract::{Query, State};
9use axum::http::StatusCode;
10use axum::response::{IntoResponse, Json, Response};
11use serde::Serialize;
12
13use crate::gateway_server::admin_api::{AdminState, UsageQuery, resolve_window};
14
15#[derive(Debug, Clone, Serialize)]
17pub struct McpServerRow {
18 pub id: String,
19 pub url: String,
20 pub credential: &'static str,
23 pub tools: i64,
24 pub changed_tools: i64,
27}
28
29#[derive(Debug, Clone, Serialize)]
30pub struct McpToolRow {
31 pub server_id: String,
32 pub tool: String,
33 pub calls: i64,
34 pub errors: i64,
35 pub persons: i64,
36 pub result_tokens: i64,
37 pub context_cost_usd: f64,
38 pub p50_duration_ms: f64,
39 pub max_duration_ms: i64,
40}
41
42#[derive(Debug, Clone, Serialize)]
43pub struct McpInventoryRow {
44 pub server_id: String,
45 pub tool: String,
46 pub schema_sha256: String,
47 pub previous_sha256: Option<String>,
48 pub change_count: i64,
49 pub first_seen: String,
50 pub last_seen: String,
51}
52
53#[derive(Debug, Clone, Default, Serialize)]
54pub struct McpTotals {
55 pub calls: i64,
56 pub errors: i64,
57 pub persons: i64,
58 pub result_tokens: i64,
59 pub context_cost_usd: f64,
60}
61
62#[derive(Debug, Clone, Serialize)]
63pub struct McpAdminResponse {
64 pub from: String,
65 pub to: String,
66 pub reference_model: Option<String>,
67 pub servers: Vec<McpServerRow>,
68 pub totals: McpTotals,
69 pub tools: Vec<McpToolRow>,
70 pub inventory: Vec<McpInventoryRow>,
71}
72
73pub async fn get_mcp(
77 State(state): State<Arc<AdminState>>,
78 Query(q): Query<UsageQuery>,
79) -> Response {
80 let (from, to) = match resolve_window(q.from.as_deref(), q.to.as_deref()) {
81 Ok(w) => w,
82 Err(msg) => {
83 return (
84 StatusCode::BAD_REQUEST,
85 Json(serde_json::json!({"error": msg})),
86 )
87 .into_response();
88 }
89 };
90
91 match assemble(&state, from, to).await {
92 Ok(resp) => Json(resp).into_response(),
93 Err(e) => {
94 tracing::warn!("admin mcp query failed: {e:#}");
95 (
96 StatusCode::SERVICE_UNAVAILABLE,
97 Json(serde_json::json!({"error": "mcp store unavailable"})),
98 )
99 .into_response()
100 }
101 }
102}
103
104async fn assemble(
105 state: &AdminState,
106 from: chrono::DateTime<chrono::Utc>,
107 to: chrono::DateTime<chrono::Utc>,
108) -> anyhow::Result<McpAdminResponse> {
109 let client = state.pool.get().await?;
110 let _ = super::store::init_schema(&state.pool).await;
113
114 let inventory: Vec<McpInventoryRow> = client
115 .query(super::store::INVENTORY_SQL, &[])
116 .await?
117 .iter()
118 .map(|r| McpInventoryRow {
119 server_id: r.get("server_id"),
120 tool: r.get("tool"),
121 schema_sha256: r.get("schema_sha256"),
122 previous_sha256: r.get("previous_sha256"),
123 change_count: r.get("change_count"),
124 first_seen: r.get("first_seen"),
125 last_seen: r.get("last_seen"),
126 })
127 .collect();
128
129 let servers = state
130 .mcp_servers
131 .iter()
132 .map(|s| {
133 let tools = inventory.iter().filter(|i| i.server_id == s.id).count() as i64;
134 let changed_tools = inventory
135 .iter()
136 .filter(|i| i.server_id == s.id && i.change_count > 0)
137 .count() as i64;
138 McpServerRow {
139 id: s.id.clone(),
140 url: s.url.clone(),
141 credential: if s.auth_env.is_some() {
142 "gateway"
143 } else {
144 "caller"
145 },
146 tools,
147 changed_tools,
148 }
149 })
150 .collect();
151
152 let t = client
153 .query_one(super::store::TOTALS_SQL, &[&from, &to])
154 .await?;
155 let totals = McpTotals {
156 calls: t.get("calls"),
157 errors: t.get("errors"),
158 persons: t.get("persons"),
159 result_tokens: t.get("result_tokens"),
160 context_cost_usd: t.get("context_cost_usd"),
161 };
162
163 let tools = client
164 .query(super::store::TOOL_BREAKDOWN_SQL, &[&from, &to])
165 .await?
166 .iter()
167 .map(|r| McpToolRow {
168 server_id: r.get("server_id"),
169 tool: r.get("tool"),
170 calls: r.get("calls"),
171 errors: r.get("errors"),
172 persons: r.get("persons"),
173 result_tokens: r.get("result_tokens"),
174 context_cost_usd: r.get("context_cost_usd"),
175 p50_duration_ms: r.get::<_, Option<f64>>("p50_duration_ms").unwrap_or(0.0),
176 max_duration_ms: r.get("max_duration_ms"),
177 })
178 .collect();
179
180 Ok(McpAdminResponse {
181 from: from.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
182 to: to.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
183 reference_model: state.reference_model.clone(),
184 servers,
185 totals,
186 tools,
187 inventory,
188 })
189}