1use std::sync::Arc;
8
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use tower_mcp::client::ChannelTransport;
12use tower_mcp::proxy::{AddBackendError, McpProxy};
13use tower_mcp::{CallToolResult, McpRouter, NoParams, SessionHandle, ToolBuilder};
14
15use crate::admin::AdminState;
16use crate::config::ProxyConfig;
17
18#[derive(Clone)]
20struct AdminToolState {
21 admin_state: AdminState,
22 session_handle: SessionHandle,
23 config_snapshot: Arc<String>,
24 proxy: McpProxy,
25}
26
27#[derive(Serialize)]
28struct BackendInfo {
29 namespace: String,
30 healthy: bool,
31 #[serde(skip_serializing_if = "Option::is_none")]
32 last_checked_at: Option<String>,
33 consecutive_failures: u32,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 error: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 transport: Option<String>,
38}
39
40#[derive(Serialize)]
41struct BackendsResult {
42 proxy_name: String,
43 proxy_version: String,
44 backend_count: usize,
45 backends: Vec<BackendInfo>,
46}
47
48#[derive(Serialize)]
49struct SessionResult {
50 active_sessions: usize,
51}
52
53pub async fn register_admin_tools(
62 proxy: &McpProxy,
63 admin_state: AdminState,
64 session_handle: SessionHandle,
65 config: &ProxyConfig,
66) -> Result<(), AddBackendError> {
67 let config_toml =
68 toml::to_string_pretty(config).unwrap_or_else(|e| format!("error serializing: {e}"));
69
70 let state = AdminToolState {
71 admin_state,
72 session_handle,
73 config_snapshot: Arc::new(config_toml),
74 proxy: proxy.clone(),
75 };
76
77 let router = build_admin_router(state);
78 let transport = ChannelTransport::new(router);
79
80 proxy.add_backend("proxy", transport).await
81}
82
83fn build_admin_router(state: AdminToolState) -> McpRouter {
84 let state_for_backends = state.clone();
85 let list_backends = ToolBuilder::new("list_backends")
86 .description("List all proxy backends with health status")
87 .handler(move |_: NoParams| {
88 let s = state_for_backends.clone();
89 async move {
90 let health = s.admin_state.health().await;
91 let backends: Vec<BackendInfo> = health
92 .iter()
93 .map(|b| BackendInfo {
94 namespace: b.namespace.clone(),
95 healthy: b.healthy,
96 last_checked_at: b.last_checked_at.map(|t| t.to_rfc3339()),
97 consecutive_failures: b.consecutive_failures,
98 error: b.error.clone(),
99 transport: b.transport.clone(),
100 })
101 .collect();
102
103 let result = BackendsResult {
104 proxy_name: s.admin_state.proxy_name().to_string(),
105 proxy_version: s.admin_state.proxy_version().to_string(),
106 backend_count: s.admin_state.backend_count(),
107 backends,
108 };
109
110 Ok(CallToolResult::text(
111 serde_json::to_string_pretty(&result).unwrap(),
112 ))
113 }
114 })
115 .build();
116
117 let state_for_sessions = state.clone();
118 let session_count = ToolBuilder::new("session_count")
119 .description("Get the number of active MCP sessions")
120 .handler(move |_: NoParams| {
121 let s = state_for_sessions.clone();
122 async move {
123 let count = s.session_handle.session_count().await;
124 let result = SessionResult {
125 active_sessions: count,
126 };
127 Ok(CallToolResult::text(
128 serde_json::to_string_pretty(&result).unwrap(),
129 ))
130 }
131 })
132 .build();
133
134 let config_snapshot = Arc::clone(&state.config_snapshot);
135 let config_tool = ToolBuilder::new("config")
136 .description("Dump the current proxy configuration")
137 .handler(move |_: NoParams| {
138 let config = Arc::clone(&config_snapshot);
139 async move { Ok(CallToolResult::text((*config).clone())) }
140 })
141 .build();
142
143 let state_for_health = state.clone();
144 let health_check = ToolBuilder::new("health_check")
145 .description("Get cached health check results for all backends")
146 .handler(move |_: NoParams| {
147 let s = state_for_health.clone();
148 async move {
149 let health = s.admin_state.health().await;
150 let backends: Vec<BackendInfo> = health
151 .iter()
152 .map(|b| BackendInfo {
153 namespace: b.namespace.clone(),
154 healthy: b.healthy,
155 last_checked_at: b.last_checked_at.map(|t| t.to_rfc3339()),
156 consecutive_failures: b.consecutive_failures,
157 error: b.error.clone(),
158 transport: b.transport.clone(),
159 })
160 .collect();
161 let healthy_count = backends.iter().filter(|b| b.healthy).count();
162 let total = backends.len();
163 let result = HealthCheckResult {
164 status: if healthy_count == total {
165 "healthy"
166 } else {
167 "degraded"
168 }
169 .to_string(),
170 healthy_count,
171 total_count: total,
172 backends,
173 };
174 Ok(CallToolResult::text(
175 serde_json::to_string_pretty(&result).unwrap(),
176 ))
177 }
178 })
179 .build();
180
181 let state_for_add = state.clone();
182 let add_backend = ToolBuilder::new("add_backend")
183 .description("Dynamically add an HTTP backend to the proxy")
184 .handler(move |input: AddBackendInput| {
185 let s = state_for_add.clone();
186 async move {
187 let transport = tower_mcp::client::HttpClientTransport::new(&input.url);
188 match s.proxy.add_backend(&input.name, transport).await {
189 Ok(()) => Ok(CallToolResult::text(format!(
190 "Backend '{}' added successfully at {}",
191 input.name, input.url
192 ))),
193 Err(e) => Ok(CallToolResult::text(format!(
194 "Failed to add backend '{}': {e}",
195 input.name
196 ))),
197 }
198 }
199 })
200 .build();
201
202 McpRouter::new()
203 .server_info("mcp-proxy-admin", "0.1.0")
204 .tool(list_backends)
205 .tool(health_check)
206 .tool(session_count)
207 .tool(add_backend)
208 .tool(config_tool)
209}
210
211#[derive(Serialize)]
212struct HealthCheckResult {
213 status: String,
214 healthy_count: usize,
215 total_count: usize,
216 backends: Vec<BackendInfo>,
217}
218
219#[derive(Debug, Deserialize, JsonSchema)]
220struct AddBackendInput {
221 name: String,
223 url: String,
225}