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(
63 proxy: &McpProxy,
64 admin_state: AdminState,
65 session_handle: SessionHandle,
66 config: &ProxyConfig,
67 discovery_tools: Option<Vec<tower_mcp::Tool>>,
68) -> Result<(), AddBackendError> {
69 let config_toml =
70 toml::to_string_pretty(config).unwrap_or_else(|e| format!("error serializing: {e}"));
71
72 let search_mode = config.proxy.tool_exposure == crate::config::ToolExposure::Search;
73
74 let state = AdminToolState {
75 admin_state,
76 session_handle,
77 config_snapshot: Arc::new(config_toml),
78 proxy: proxy.clone(),
79 };
80
81 let router = build_admin_router(state, discovery_tools, search_mode);
82 let transport = ChannelTransport::new(router);
83
84 proxy.add_backend("proxy", transport).await
85}
86
87fn build_admin_router(
88 state: AdminToolState,
89 discovery_tools: Option<Vec<tower_mcp::Tool>>,
90 search_mode: bool,
91) -> McpRouter {
92 let state_for_backends = state.clone();
93 let list_backends = ToolBuilder::new("list_backends")
94 .description("List all proxy backends with health status")
95 .handler(move |_: NoParams| {
96 let s = state_for_backends.clone();
97 async move {
98 let health = s.admin_state.health().await;
99 let backends: Vec<BackendInfo> = health
100 .iter()
101 .map(|b| BackendInfo {
102 namespace: b.namespace.clone(),
103 healthy: b.healthy,
104 last_checked_at: b.last_checked_at.map(|t| t.to_rfc3339()),
105 consecutive_failures: b.consecutive_failures,
106 error: b.error.clone(),
107 transport: b.transport.clone(),
108 })
109 .collect();
110
111 let result = BackendsResult {
112 proxy_name: s.admin_state.proxy_name().to_string(),
113 proxy_version: s.admin_state.proxy_version().to_string(),
114 backend_count: s.admin_state.backend_count(),
115 backends,
116 };
117
118 Ok(CallToolResult::text(
119 serde_json::to_string_pretty(&result).unwrap(),
120 ))
121 }
122 })
123 .build();
124
125 let state_for_sessions = state.clone();
126 let session_count = ToolBuilder::new("session_count")
127 .description("Get the number of active MCP sessions")
128 .handler(move |_: NoParams| {
129 let s = state_for_sessions.clone();
130 async move {
131 let count = s.session_handle.session_count().await;
132 let result = SessionResult {
133 active_sessions: count,
134 };
135 Ok(CallToolResult::text(
136 serde_json::to_string_pretty(&result).unwrap(),
137 ))
138 }
139 })
140 .build();
141
142 let config_snapshot = Arc::clone(&state.config_snapshot);
143 let config_tool = ToolBuilder::new("config")
144 .description("Dump the current proxy configuration")
145 .handler(move |_: NoParams| {
146 let config = Arc::clone(&config_snapshot);
147 async move { Ok(CallToolResult::text((*config).clone())) }
148 })
149 .build();
150
151 let state_for_health = state.clone();
152 let health_check = ToolBuilder::new("health_check")
153 .description("Get cached health check results for all backends")
154 .handler(move |_: NoParams| {
155 let s = state_for_health.clone();
156 async move {
157 let health = s.admin_state.health().await;
158 let backends: Vec<BackendInfo> = health
159 .iter()
160 .map(|b| BackendInfo {
161 namespace: b.namespace.clone(),
162 healthy: b.healthy,
163 last_checked_at: b.last_checked_at.map(|t| t.to_rfc3339()),
164 consecutive_failures: b.consecutive_failures,
165 error: b.error.clone(),
166 transport: b.transport.clone(),
167 })
168 .collect();
169 let healthy_count = backends.iter().filter(|b| b.healthy).count();
170 let total = backends.len();
171 let result = HealthCheckResult {
172 status: if healthy_count == total {
173 "healthy"
174 } else {
175 "degraded"
176 }
177 .to_string(),
178 healthy_count,
179 total_count: total,
180 backends,
181 };
182 Ok(CallToolResult::text(
183 serde_json::to_string_pretty(&result).unwrap(),
184 ))
185 }
186 })
187 .build();
188
189 let state_for_add = state.clone();
190 let add_backend = ToolBuilder::new("add_backend")
191 .description("Dynamically add an HTTP backend to the proxy")
192 .handler(move |input: AddBackendInput| {
193 let s = state_for_add.clone();
194 async move {
195 let transport = tower_mcp::client::HttpClientTransport::new(&input.url);
196 match s.proxy.add_backend(&input.name, transport).await {
197 Ok(()) => Ok(CallToolResult::text(format!(
198 "Backend '{}' added successfully at {}",
199 input.name, input.url
200 ))),
201 Err(e) => Ok(CallToolResult::text(format!(
202 "Failed to add backend '{}': {e}",
203 input.name
204 ))),
205 }
206 }
207 })
208 .build();
209
210 let mut router = McpRouter::new()
211 .server_info("mcp-proxy-admin", "0.1.0")
212 .tool(list_backends)
213 .tool(health_check)
214 .tool(session_count)
215 .tool(add_backend)
216 .tool(config_tool);
217
218 if search_mode {
219 let state_for_call = state.clone();
220 let call_tool = ToolBuilder::new("call_tool")
221 .description(
222 "Invoke any backend tool by its fully-qualified name. Use proxy/search_tools \
223 to discover available tools, then call them through this tool.",
224 )
225 .handler(move |input: CallToolInput| {
226 let s = state_for_call.clone();
227 async move {
228 use tower::Service;
229 use tower_mcp::protocol::{CallToolParams, McpRequest, McpResponse, RequestId};
230 use tower_mcp::router::{Extensions, RouterRequest};
231
232 let req = RouterRequest {
233 id: RequestId::Number(0),
234 inner: McpRequest::CallTool(CallToolParams {
235 name: input.name.clone(),
236 arguments: input.arguments.unwrap_or_default().into(),
237 meta: None,
238 task: None,
239 }),
240 extensions: Extensions::new(),
241 };
242
243 let mut proxy = s.proxy.clone();
244 match proxy.call(req).await {
245 Ok(resp) => match resp.inner {
246 Ok(McpResponse::CallTool(result)) => Ok(result),
247 Ok(_) => Ok(CallToolResult::text(format!(
248 "Unexpected response type for tool '{}'",
249 input.name
250 ))),
251 Err(e) => Ok(CallToolResult::text(format!(
252 "Error calling '{}': {}",
253 input.name, e.message
254 ))),
255 },
256 Err(_) => Ok(CallToolResult::text(format!(
257 "Internal error calling '{}'",
258 input.name
259 ))),
260 }
261 }
262 })
263 .build();
264 router = router.tool(call_tool);
265 }
266
267 if let Some(tools) = discovery_tools {
268 for tool in tools {
269 router = router.tool(tool);
270 }
271 }
272
273 router
274}
275
276#[derive(Serialize)]
277struct HealthCheckResult {
278 status: String,
279 healthy_count: usize,
280 total_count: usize,
281 backends: Vec<BackendInfo>,
282}
283
284#[derive(Debug, Deserialize, JsonSchema)]
285struct AddBackendInput {
286 name: String,
288 url: String,
290}
291
292#[derive(Debug, Deserialize, JsonSchema)]
294struct CallToolInput {
295 name: String,
297 arguments: Option<serde_json::Map<String, serde_json::Value>>,
299}