1use crate::{
10 AppError, DynamicRouterService, get_proxy_manager,
11 model::GLOBAL_RESTART_TRACKER,
12 model::{
13 CheckMcpStatusResponseStatus, McpConfig, McpProtocol, McpProtocolPath, McpRouterPath,
14 McpServerCommandConfig, McpServerConfig, McpServiceStatus, McpType,
15 },
16 proxy::{
17 McpHandler, SseBackendConfig, SseServerBuilder, StreamBackendConfig, StreamServerBuilder,
18 },
19};
20
21use anyhow::{Context, Result};
22use log::{debug, info};
23use std::collections::HashMap;
24
25pub async fn mcp_start_task(
31 mcp_config: McpConfig,
32) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
33 let mcp_id = mcp_config.mcp_id.clone();
34 let client_protocol = mcp_config.client_protocol.clone();
35
36 let mcp_router_path: McpRouterPath = McpRouterPath::new(mcp_id, client_protocol)
38 .map_err(|e| AppError::mcp_server_error(e.to_string()))?;
39
40 let mcp_json_config = mcp_config
41 .mcp_json_config
42 .clone()
43 .expect("mcp_json_config is required");
44
45 let mcp_server_config = McpServerConfig::try_from(mcp_json_config)?;
46
47 integrate_server_with_axum(
49 mcp_server_config.clone(),
50 mcp_router_path.clone(),
51 mcp_config.clone(),
52 )
53 .await
54}
55
56pub async fn integrate_server_with_axum(
64 mcp_config: McpServerConfig,
65 mcp_router_path: McpRouterPath,
66 full_mcp_config: McpConfig,
67) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
68 let mcp_type = full_mcp_config.mcp_type.clone();
69 let base_path = mcp_router_path.base_path.clone();
70 let mcp_id = mcp_router_path.mcp_id.clone();
71
72 let backend_protocol = match &mcp_config {
74 McpServerConfig::Command(_) => McpProtocol::Stdio,
76 McpServerConfig::Url(url_config) => {
78 let detection_headers =
80 merge_headers_with_auth(&url_config.headers, url_config.auth_token.as_deref())?;
81 if let Some(type_str) = &url_config.r#type {
84 match type_str.parse::<McpProtocol>() {
85 Ok(protocol) => {
86 debug!(
87 "Using configured protocol type: {} -> {:?}",
88 type_str, protocol
89 );
90 protocol
91 }
92 Err(_) => {
93 debug!("Protocol type '{}' unrecognized, auto-detecting", type_str);
95 let detected_protocol = crate::server::detect_mcp_protocol_with_headers(
96 url_config.get_url(),
97 Some(&detection_headers),
98 )
99 .await
100 .map_err(|e| {
101 anyhow::anyhow!(
102 "Protocol type '{}' unrecognized and auto-detection failed: {}",
103 type_str,
104 e
105 )
106 })?;
107 debug!(
108 "Auto-detected protocol: {:?} (original config: '{}')",
109 detected_protocol, type_str
110 );
111 detected_protocol
112 }
113 }
114 } else {
115 debug!("No type field specified, auto-detecting protocol");
117
118 crate::server::detect_mcp_protocol_with_headers(
119 url_config.get_url(),
120 Some(&detection_headers),
121 )
122 .await
123 .context("Auto-detection failed")?
124 }
125 }
126 };
127
128 debug!(
129 "MCP ID: {}, client protocol: {:?}, backend protocol: {:?}",
130 mcp_id, mcp_router_path.mcp_protocol, backend_protocol
131 );
132
133 let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
135 McpProtocol::Sse => {
137 let sse_path = match &mcp_router_path.mcp_protocol_path {
138 McpProtocolPath::SsePath(sse_path) => sse_path,
139 _ => unreachable!(),
140 };
141
142 let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
144 let bridge = connect_stream_backend(&mcp_config, &mcp_id).await?;
147 SseBackendConfig::BackendBridge(bridge)
148 } else {
149 build_sse_backend_config(&mcp_config, backend_protocol)?
150 };
151
152 debug!(
153 "Creating SSE server, sse_path={}, post_path={}",
154 sse_path.sse_path, sse_path.message_path
155 );
156
157 let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
160 5
161 } else {
162 15
163 };
164
165 let stateful = !matches!(mcp_type, McpType::OneShot);
168
169 let (router, ct, handler) = SseServerBuilder::new(backend_config)
170 .mcp_id(mcp_id.clone())
171 .sse_path(sse_path.sse_path.clone())
172 .post_path(sse_path.message_path.clone())
173 .keep_alive(keep_alive_secs)
174 .stateful(stateful)
175 .build()
176 .await
177 .with_context(|| {
178 format!(
179 "SSE server build failed - MCP ID: {}, type: {:?}",
180 mcp_id, mcp_type
181 )
182 })?;
183
184 info!(
185 "SSE server started - MCP ID: {}, type: {:?}",
186 mcp_router_path.mcp_id, mcp_type
187 );
188
189 (router, ct, McpHandler::Sse(Box::new(handler)))
190 }
191
192 McpProtocol::Stream => {
194 let backend_config = build_stream_backend_config(&mcp_config, backend_protocol)?;
196
197 let (router, ct, handler) = StreamServerBuilder::new(backend_config)
198 .mcp_id(mcp_id.clone())
199 .stateful(false)
200 .build()
201 .await
202 .with_context(|| {
203 format!(
204 "Stream server build failed - MCP ID: {}, type: {:?}",
205 mcp_id, mcp_type
206 )
207 })?;
208
209 info!(
210 "Streamable HTTP server started - MCP ID: {}, type: {:?}",
211 mcp_router_path.mcp_id, mcp_type
212 );
213
214 (router, ct, McpHandler::Stream(Box::new(handler)))
215 }
216
217 McpProtocol::Stdio => {
219 return Err(anyhow::anyhow!(
220 "Client protocol cannot be Stdio. McpRouterPath::new does not support creating Stdio protocol router paths"
221 ));
222 }
223 };
224
225 let ct_clone = ct.clone();
227 let mcp_id_clone = mcp_id.clone();
228
229 let mcp_service_status = McpServiceStatus::new(
231 mcp_id_clone.clone(),
232 mcp_type.clone(),
233 mcp_router_path.clone(),
234 ct_clone.clone(),
235 CheckMcpStatusResponseStatus::Ready,
236 )
237 .with_mcp_config(full_mcp_config.clone());
238
239 let proxy_manager = get_proxy_manager();
241 proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
242
243 proxy_manager
245 .register_mcp_config(&mcp_id, full_mcp_config.clone())
246 .await;
247
248 let router = if matches!(mcp_router_path.mcp_protocol, McpProtocol::Sse) {
250 let modified_router = router.fallback(base_path_fallback_handler);
251 info!("SSE base path handler added, base_path: {}", base_path);
252 modified_router
253 } else {
254 router
255 };
256
257 info!(
259 "Registering route: base_path={}, mcp_id={}",
260 base_path, mcp_id
261 );
262 info!(
263 "SSE path config: sse_path={}, post_path={}",
264 match &mcp_router_path.mcp_protocol_path {
265 McpProtocolPath::SsePath(sse_path) => &sse_path.sse_path,
266 _ => "N/A",
267 },
268 match &mcp_router_path.mcp_protocol_path {
269 McpProtocolPath::SsePath(sse_path) => &sse_path.message_path,
270 _ => "N/A",
271 }
272 );
273 DynamicRouterService::register_route(&base_path, router.clone());
274 info!("Route registration complete: base_path={}", base_path);
275
276 GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
278
279 Ok((router, ct))
280}
281
282async fn connect_stream_backend(
288 mcp_config: &McpServerConfig,
289 mcp_id: &str,
290) -> Result<std::sync::Arc<dyn mcp_common::BackendBridge>> {
291 use crate::proxy::{StreamClientConnection, StreamProxyHandler};
292
293 let url_config = match mcp_config {
294 McpServerConfig::Url(url_config) => url_config,
295 _ => return Err(anyhow::anyhow!("Stream backend requires URL-based config")),
296 };
297
298 let url = url_config.get_url();
299 info!(
300 "Connecting to Streamable HTTP backend (SSE frontend) \
301 - MCP ID: {}, URL: {}",
302 mcp_id, url
303 );
304
305 let mut config = mcp_common::McpClientConfig::new(url.to_string());
306 let headers = merge_headers_with_auth(&url_config.headers, url_config.auth_token.as_deref())?;
307 for (k, v) in &headers {
308 config = config.with_header(k, v);
309 }
310
311 let conn = StreamClientConnection::connect(config).await?;
312 let proxy_handler =
313 StreamProxyHandler::with_mcp_id(conn.into_running_service(), mcp_id.to_string());
314
315 Ok(std::sync::Arc::new(proxy_handler))
316}
317
318fn build_sse_backend_config(
320 mcp_config: &McpServerConfig,
321 backend_protocol: McpProtocol,
322) -> Result<SseBackendConfig> {
323 match mcp_config {
324 McpServerConfig::Command(cmd_config) => {
325 log_command_details(cmd_config);
326 Ok(SseBackendConfig::Stdio {
327 command: cmd_config.command.clone(),
328 args: cmd_config.args.clone(),
329 env: cmd_config.env.clone(),
330 })
331 }
332 McpServerConfig::Url(url_config) => match backend_protocol {
333 McpProtocol::Stdio => Err(anyhow::anyhow!(
334 "URL-based MCP service cannot use Stdio protocol"
335 )),
336 McpProtocol::Sse => {
337 info!("Connecting to SSE backend: {}", url_config.get_url());
338 let headers =
340 merge_headers_with_auth(&url_config.headers, url_config.auth_token.as_deref())?;
341 Ok(SseBackendConfig::SseUrl {
342 url: url_config.get_url().to_string(),
343 headers: if headers.is_empty() {
344 None
345 } else {
346 Some(headers)
347 },
348 })
349 }
350 McpProtocol::Stream => Err(anyhow::anyhow!(
351 "Stream backend should be handled via connect_stream_backend(), \
352 not build_sse_backend_config()"
353 )),
354 },
355 }
356}
357
358fn build_stream_backend_config(
360 mcp_config: &McpServerConfig,
361 backend_protocol: McpProtocol,
362) -> Result<StreamBackendConfig> {
363 match mcp_config {
364 McpServerConfig::Command(cmd_config) => {
365 log_command_details(cmd_config);
366 Ok(StreamBackendConfig::Stdio {
367 command: cmd_config.command.clone(),
368 args: cmd_config.args.clone(),
369 env: cmd_config.env.clone(),
370 })
371 }
372 McpServerConfig::Url(url_config) => {
373 match backend_protocol {
374 McpProtocol::Stdio => Err(anyhow::anyhow!(
375 "URL-based MCP service cannot use Stdio protocol"
376 )),
377 McpProtocol::Sse => {
378 Err(anyhow::anyhow!(
382 "SSE backend with Streamable HTTP frontend is not yet supported. \
383 Please use SSE frontend or configure a Streamable HTTP backend."
384 ))
385 }
386 McpProtocol::Stream => {
387 info!(
388 "Connecting to Streamable HTTP backend: {}",
389 url_config.get_url()
390 );
391 let headers = merge_headers_with_auth(
393 &url_config.headers,
394 url_config.auth_token.as_deref(),
395 )?;
396 Ok(StreamBackendConfig::Url {
397 url: url_config.get_url().to_string(),
398 headers: if headers.is_empty() {
399 None
400 } else {
401 Some(headers)
402 },
403 })
404 }
405 }
406 }
407 }
408}
409
410fn merge_headers_with_auth(
415 headers: &Option<HashMap<String, String>>,
416 auth_token: Option<&str>,
417) -> Result<HashMap<String, String>> {
418 crate::client::support::merge_config_headers_checked(
419 headers.clone().unwrap_or_default(),
420 auth_token,
421 )
422}
423
424fn log_command_details(mcp_config: &McpServerCommandConfig) {
426 let args_str = mcp_config
427 .args
428 .as_ref()
429 .map_or(String::new(), |args| args.join(" "));
430
431 info!("Executing command: {} {}", mcp_config.command, args_str);
432
433 if let Some(env_vars) = &mcp_config.env {
435 let keys: Vec<&String> = env_vars.keys().collect();
436 if !keys.is_empty() {
437 debug!("Config env keys: {:?}", keys);
438 }
439 }
440
441 debug!(
443 "Process PATH: {}",
444 mcp_common::diagnostic::format_path_summary(3)
445 );
446 for (key, val) in mcp_common::diagnostic::collect_mirror_env_vars() {
447 debug!("Process env: {}={}", key, val);
448 }
449}
450
451#[axum::debug_handler]
453async fn base_path_fallback_handler(
454 method: axum::http::Method,
455 uri: axum::http::Uri,
456 headers: axum::http::HeaderMap,
457) -> impl axum::response::IntoResponse {
458 let path = uri.path();
459 info!("Base path handler: {} {}", method, path);
460
461 if path.contains("/sse/proxy/") {
463 match method {
465 axum::http::Method::GET => {
466 let mcp_id = path.split("/sse/proxy/").nth(1);
468
469 if let Some(mcp_id) = mcp_id {
470 let proxy_manager = get_proxy_manager();
472 if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
473 (
475 axum::http::StatusCode::NOT_FOUND,
476 [("Content-Type", "text/plain".to_string())],
477 format!("MCP service '{}' not found", mcp_id).to_string(),
478 )
479 } else {
480 let accept_header = headers.get("accept");
482 if let Some(accept) = accept_header {
483 let accept_str = accept.to_str().unwrap_or("");
484 if accept_str.contains("text/event-stream") {
485 let redirect_uri = format!("{}/sse", path);
487 info!("SSE redirect to: {}", redirect_uri);
488 (
489 axum::http::StatusCode::FOUND,
490 [("Location", redirect_uri.to_string())],
491 "Redirecting to SSE endpoint".to_string(),
492 )
493 } else {
494 (
496 axum::http::StatusCode::BAD_REQUEST,
497 [("Content-Type", "text/plain".to_string())],
498 "SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(),
499 )
500 }
501 } else {
502 (
504 axum::http::StatusCode::BAD_REQUEST,
505 [("Content-Type", "text/plain".to_string())],
506 "SSE error: Missing Accept header, expected 'text/event-stream'"
507 .to_string(),
508 )
509 }
510 }
511 } else {
512 (
514 axum::http::StatusCode::BAD_REQUEST,
515 [("Content-Type", "text/plain".to_string())],
516 "SSE error: Invalid SSE path".to_string(),
517 )
518 }
519 }
520 axum::http::Method::POST => {
521 let redirect_uri = format!("{}/message", path);
523 info!("SSE redirect to: {}", redirect_uri);
524 (
525 axum::http::StatusCode::FOUND,
526 [("Location", redirect_uri.to_string())],
527 "Redirecting to message endpoint".to_string(),
528 )
529 }
530 _ => {
531 (
533 axum::http::StatusCode::METHOD_NOT_ALLOWED,
534 [("Allow", "GET, POST".to_string())],
535 "Only GET and POST methods are allowed".to_string(),
536 )
537 }
538 }
539 } else if path.contains("/stream/proxy/") {
540 match method {
542 axum::http::Method::GET => {
543 (
545 axum::http::StatusCode::OK,
546 [("Content-Type", "application/json".to_string())],
547 r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(),
548 )
549 }
550 axum::http::Method::POST => {
551 (
553 axum::http::StatusCode::OK,
554 [("Content-Type", "application/json".to_string())],
555 r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(),
556 )
557 }
558 _ => {
559 (
561 axum::http::StatusCode::METHOD_NOT_ALLOWED,
562 [("Allow", "GET, POST".to_string())],
563 "Only GET and POST methods are allowed".to_string(),
564 )
565 }
566 }
567 } else {
568 (
570 axum::http::StatusCode::BAD_REQUEST,
571 [("Content-Type", "text/plain".to_string())],
572 "Unknown protocol or path".to_string(),
573 )
574 }
575}