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 .map_err(|e| anyhow::anyhow!("Auto-detection failed: {}", e))?
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) -> HashMap<String, String> {
418 let mut merged = normalize_headers(headers).unwrap_or_default();
419 if let Some(token) = auth_token {
420 merged.insert(
421 "Authorization".to_string(),
422 crate::client::support::normalize_authorization(token),
423 );
424 }
425 merged
426}
427
428fn normalize_headers(headers: &Option<HashMap<String, String>>) -> Option<HashMap<String, String>> {
433 headers.as_ref().map(|h| {
434 h.iter()
435 .map(|(k, v)| {
436 let value = if k.eq_ignore_ascii_case("Authorization") {
437 crate::client::support::normalize_authorization(v)
438 } else {
439 v.clone()
440 };
441 (k.clone(), value)
442 })
443 .collect()
444 })
445}
446
447fn log_command_details(mcp_config: &McpServerCommandConfig) {
449 let args_str = mcp_config
450 .args
451 .as_ref()
452 .map_or(String::new(), |args| args.join(" "));
453
454 info!("Executing command: {} {}", mcp_config.command, args_str);
455
456 if let Some(env_vars) = &mcp_config.env {
458 let keys: Vec<&String> = env_vars.keys().collect();
459 if !keys.is_empty() {
460 debug!("Config env keys: {:?}", keys);
461 }
462 }
463
464 debug!(
466 "Process PATH: {}",
467 mcp_common::diagnostic::format_path_summary(3)
468 );
469 for (key, val) in mcp_common::diagnostic::collect_mirror_env_vars() {
470 debug!("Process env: {}={}", key, val);
471 }
472}
473
474#[axum::debug_handler]
476async fn base_path_fallback_handler(
477 method: axum::http::Method,
478 uri: axum::http::Uri,
479 headers: axum::http::HeaderMap,
480) -> impl axum::response::IntoResponse {
481 let path = uri.path();
482 info!("Base path handler: {} {}", method, path);
483
484 if path.contains("/sse/proxy/") {
486 match method {
488 axum::http::Method::GET => {
489 let mcp_id = path.split("/sse/proxy/").nth(1);
491
492 if let Some(mcp_id) = mcp_id {
493 let proxy_manager = get_proxy_manager();
495 if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
496 (
498 axum::http::StatusCode::NOT_FOUND,
499 [("Content-Type", "text/plain".to_string())],
500 format!("MCP service '{}' not found", mcp_id).to_string(),
501 )
502 } else {
503 let accept_header = headers.get("accept");
505 if let Some(accept) = accept_header {
506 let accept_str = accept.to_str().unwrap_or("");
507 if accept_str.contains("text/event-stream") {
508 let redirect_uri = format!("{}/sse", path);
510 info!("SSE redirect to: {}", redirect_uri);
511 (
512 axum::http::StatusCode::FOUND,
513 [("Location", redirect_uri.to_string())],
514 "Redirecting to SSE endpoint".to_string(),
515 )
516 } else {
517 (
519 axum::http::StatusCode::BAD_REQUEST,
520 [("Content-Type", "text/plain".to_string())],
521 "SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(),
522 )
523 }
524 } else {
525 (
527 axum::http::StatusCode::BAD_REQUEST,
528 [("Content-Type", "text/plain".to_string())],
529 "SSE error: Missing Accept header, expected 'text/event-stream'"
530 .to_string(),
531 )
532 }
533 }
534 } else {
535 (
537 axum::http::StatusCode::BAD_REQUEST,
538 [("Content-Type", "text/plain".to_string())],
539 "SSE error: Invalid SSE path".to_string(),
540 )
541 }
542 }
543 axum::http::Method::POST => {
544 let redirect_uri = format!("{}/message", path);
546 info!("SSE redirect to: {}", redirect_uri);
547 (
548 axum::http::StatusCode::FOUND,
549 [("Location", redirect_uri.to_string())],
550 "Redirecting to message endpoint".to_string(),
551 )
552 }
553 _ => {
554 (
556 axum::http::StatusCode::METHOD_NOT_ALLOWED,
557 [("Allow", "GET, POST".to_string())],
558 "Only GET and POST methods are allowed".to_string(),
559 )
560 }
561 }
562 } else if path.contains("/stream/proxy/") {
563 match method {
565 axum::http::Method::GET => {
566 (
568 axum::http::StatusCode::OK,
569 [("Content-Type", "application/json".to_string())],
570 r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(),
571 )
572 }
573 axum::http::Method::POST => {
574 (
576 axum::http::StatusCode::OK,
577 [("Content-Type", "application/json".to_string())],
578 r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(),
579 )
580 }
581 _ => {
582 (
584 axum::http::StatusCode::METHOD_NOT_ALLOWED,
585 [("Allow", "GET, POST".to_string())],
586 "Only GET and POST methods are allowed".to_string(),
587 )
588 }
589 }
590 } else {
591 (
593 axum::http::StatusCode::BAD_REQUEST,
594 [("Content-Type", "text/plain".to_string())],
595 "Unknown protocol or path".to_string(),
596 )
597 }
598}