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 mut detection_headers = normalize_headers(&url_config.headers).unwrap_or_default();
80 if let Some(auth_token) = &url_config.auth_token {
81 let value = if auth_token.starts_with("Bearer ") {
82 auth_token.clone()
83 } else {
84 format!("Bearer {}", auth_token)
85 };
86 detection_headers.insert("Authorization".to_string(), value);
87 }
88 let detection_headers_ref = if detection_headers.is_empty() {
89 None
90 } else {
91 Some(&detection_headers)
92 };
93
94 if let Some(type_str) = &url_config.r#type {
96 match type_str.parse::<McpProtocol>() {
97 Ok(protocol) => {
98 debug!(
99 "Using configured protocol type: {} -> {:?}",
100 type_str, protocol
101 );
102 protocol
103 }
104 Err(_) => {
105 debug!("Protocol type '{}' unrecognized, auto-detecting", type_str);
107 let detected_protocol = crate::server::detect_mcp_protocol_with_headers(
108 url_config.get_url(),
109 detection_headers_ref,
110 )
111 .await
112 .map_err(|e| {
113 anyhow::anyhow!(
114 "Protocol type '{}' unrecognized and auto-detection failed: {}",
115 type_str,
116 e
117 )
118 })?;
119 debug!(
120 "Auto-detected protocol: {:?} (original config: '{}')",
121 detected_protocol, type_str
122 );
123 detected_protocol
124 }
125 }
126 } else {
127 debug!("No type field specified, auto-detecting protocol");
129
130 crate::server::detect_mcp_protocol_with_headers(
131 url_config.get_url(),
132 detection_headers_ref,
133 )
134 .await
135 .map_err(|e| anyhow::anyhow!("Auto-detection failed: {}", e))?
136 }
137 }
138 };
139
140 debug!(
141 "MCP ID: {}, client protocol: {:?}, backend protocol: {:?}",
142 mcp_id, mcp_router_path.mcp_protocol, backend_protocol
143 );
144
145 let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
147 McpProtocol::Sse => {
149 let sse_path = match &mcp_router_path.mcp_protocol_path {
150 McpProtocolPath::SsePath(sse_path) => sse_path,
151 _ => unreachable!(),
152 };
153
154 let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
156 let bridge = connect_stream_backend(&mcp_config, &mcp_id).await?;
159 SseBackendConfig::BackendBridge(bridge)
160 } else {
161 build_sse_backend_config(&mcp_config, backend_protocol)?
162 };
163
164 debug!(
165 "Creating SSE server, sse_path={}, post_path={}",
166 sse_path.sse_path, sse_path.message_path
167 );
168
169 let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
172 5
173 } else {
174 15
175 };
176
177 let stateful = !matches!(mcp_type, McpType::OneShot);
180
181 let (router, ct, handler) = SseServerBuilder::new(backend_config)
182 .mcp_id(mcp_id.clone())
183 .sse_path(sse_path.sse_path.clone())
184 .post_path(sse_path.message_path.clone())
185 .keep_alive(keep_alive_secs)
186 .stateful(stateful)
187 .build()
188 .await
189 .with_context(|| {
190 format!(
191 "SSE server build failed - MCP ID: {}, type: {:?}",
192 mcp_id, mcp_type
193 )
194 })?;
195
196 info!(
197 "SSE server started - MCP ID: {}, type: {:?}",
198 mcp_router_path.mcp_id, mcp_type
199 );
200
201 (router, ct, McpHandler::Sse(Box::new(handler)))
202 }
203
204 McpProtocol::Stream => {
206 let backend_config = build_stream_backend_config(&mcp_config, backend_protocol)?;
208
209 let (router, ct, handler) = StreamServerBuilder::new(backend_config)
210 .mcp_id(mcp_id.clone())
211 .stateful(false)
212 .build()
213 .await
214 .with_context(|| {
215 format!(
216 "Stream server build failed - MCP ID: {}, type: {:?}",
217 mcp_id, mcp_type
218 )
219 })?;
220
221 info!(
222 "Streamable HTTP server started - MCP ID: {}, type: {:?}",
223 mcp_router_path.mcp_id, mcp_type
224 );
225
226 (router, ct, McpHandler::Stream(Box::new(handler)))
227 }
228
229 McpProtocol::Stdio => {
231 return Err(anyhow::anyhow!(
232 "Client protocol cannot be Stdio. McpRouterPath::new does not support creating Stdio protocol router paths"
233 ));
234 }
235 };
236
237 let ct_clone = ct.clone();
239 let mcp_id_clone = mcp_id.clone();
240
241 let mcp_service_status = McpServiceStatus::new(
243 mcp_id_clone.clone(),
244 mcp_type.clone(),
245 mcp_router_path.clone(),
246 ct_clone.clone(),
247 CheckMcpStatusResponseStatus::Ready,
248 )
249 .with_mcp_config(full_mcp_config.clone());
250
251 let proxy_manager = get_proxy_manager();
253 proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
254
255 proxy_manager
257 .register_mcp_config(&mcp_id, full_mcp_config.clone())
258 .await;
259
260 let router = if matches!(mcp_router_path.mcp_protocol, McpProtocol::Sse) {
262 let modified_router = router.fallback(base_path_fallback_handler);
263 info!("SSE base path handler added, base_path: {}", base_path);
264 modified_router
265 } else {
266 router
267 };
268
269 info!(
271 "Registering route: base_path={}, mcp_id={}",
272 base_path, mcp_id
273 );
274 info!(
275 "SSE path config: sse_path={}, post_path={}",
276 match &mcp_router_path.mcp_protocol_path {
277 McpProtocolPath::SsePath(sse_path) => &sse_path.sse_path,
278 _ => "N/A",
279 },
280 match &mcp_router_path.mcp_protocol_path {
281 McpProtocolPath::SsePath(sse_path) => &sse_path.message_path,
282 _ => "N/A",
283 }
284 );
285 DynamicRouterService::register_route(&base_path, router.clone());
286 info!("Route registration complete: base_path={}", base_path);
287
288 GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
290
291 Ok((router, ct))
292}
293
294async fn connect_stream_backend(
300 mcp_config: &McpServerConfig,
301 mcp_id: &str,
302) -> Result<std::sync::Arc<dyn mcp_common::BackendBridge>> {
303 use crate::proxy::{StreamClientConnection, StreamProxyHandler};
304
305 let url_config = match mcp_config {
306 McpServerConfig::Url(url_config) => url_config,
307 _ => return Err(anyhow::anyhow!("Stream backend requires URL-based config")),
308 };
309
310 let url = url_config.get_url();
311 info!(
312 "Connecting to Streamable HTTP backend (SSE frontend) \
313 - MCP ID: {}, URL: {}",
314 mcp_id, url
315 );
316
317 let mut config = mcp_common::McpClientConfig::new(url.to_string());
318 let normalized = normalize_headers(&url_config.headers);
319 if let Some(ref headers) = normalized {
320 for (k, v) in headers {
321 config = config.with_header(k, v);
322 }
323 }
324 if let Some(ref auth_token) = url_config.auth_token {
326 let value = if auth_token.starts_with("Bearer ") {
327 auth_token.clone()
328 } else {
329 format!("Bearer {}", auth_token)
330 };
331 config = config.with_header("Authorization", value);
332 }
333
334 let conn = StreamClientConnection::connect(config).await?;
335 let proxy_handler =
336 StreamProxyHandler::with_mcp_id(conn.into_running_service(), mcp_id.to_string());
337
338 Ok(std::sync::Arc::new(proxy_handler))
339}
340
341fn build_sse_backend_config(
343 mcp_config: &McpServerConfig,
344 backend_protocol: McpProtocol,
345) -> Result<SseBackendConfig> {
346 match mcp_config {
347 McpServerConfig::Command(cmd_config) => {
348 log_command_details(cmd_config);
349 Ok(SseBackendConfig::Stdio {
350 command: cmd_config.command.clone(),
351 args: cmd_config.args.clone(),
352 env: cmd_config.env.clone(),
353 })
354 }
355 McpServerConfig::Url(url_config) => match backend_protocol {
356 McpProtocol::Stdio => Err(anyhow::anyhow!(
357 "URL-based MCP service cannot use Stdio protocol"
358 )),
359 McpProtocol::Sse => {
360 info!("Connecting to SSE backend: {}", url_config.get_url());
361 Ok(SseBackendConfig::SseUrl {
362 url: url_config.get_url().to_string(),
363 headers: normalize_headers(&url_config.headers),
364 })
365 }
366 McpProtocol::Stream => Err(anyhow::anyhow!(
367 "Stream backend should be handled via connect_stream_backend(), \
368 not build_sse_backend_config()"
369 )),
370 },
371 }
372}
373
374fn build_stream_backend_config(
376 mcp_config: &McpServerConfig,
377 backend_protocol: McpProtocol,
378) -> Result<StreamBackendConfig> {
379 match mcp_config {
380 McpServerConfig::Command(cmd_config) => {
381 log_command_details(cmd_config);
382 Ok(StreamBackendConfig::Stdio {
383 command: cmd_config.command.clone(),
384 args: cmd_config.args.clone(),
385 env: cmd_config.env.clone(),
386 })
387 }
388 McpServerConfig::Url(url_config) => {
389 match backend_protocol {
390 McpProtocol::Stdio => Err(anyhow::anyhow!(
391 "URL-based MCP service cannot use Stdio protocol"
392 )),
393 McpProtocol::Sse => {
394 Err(anyhow::anyhow!(
398 "SSE backend with Streamable HTTP frontend is not yet supported. \
399 Please use SSE frontend or configure a Streamable HTTP backend."
400 ))
401 }
402 McpProtocol::Stream => {
403 info!(
404 "Connecting to Streamable HTTP backend: {}",
405 url_config.get_url()
406 );
407 Ok(StreamBackendConfig::Url {
408 url: url_config.get_url().to_string(),
409 headers: normalize_headers(&url_config.headers),
410 })
411 }
412 }
413 }
414 }
415}
416
417fn normalize_headers(headers: &Option<HashMap<String, String>>) -> Option<HashMap<String, String>> {
422 headers.as_ref().map(|h| {
423 h.iter()
424 .map(|(k, v)| {
425 if k.eq_ignore_ascii_case("Authorization") && !v.starts_with("Bearer ") {
426 (k.clone(), format!("Bearer {}", v))
427 } else {
428 (k.clone(), v.clone())
429 }
430 })
431 .collect()
432 })
433}
434
435fn log_command_details(mcp_config: &McpServerCommandConfig) {
437 let args_str = mcp_config
438 .args
439 .as_ref()
440 .map_or(String::new(), |args| args.join(" "));
441
442 info!("Executing command: {} {}", mcp_config.command, args_str);
443
444 if let Some(env_vars) = &mcp_config.env {
446 let keys: Vec<&String> = env_vars.keys().collect();
447 if !keys.is_empty() {
448 debug!("Config env keys: {:?}", keys);
449 }
450 }
451
452 debug!(
454 "Process PATH: {}",
455 mcp_common::diagnostic::format_path_summary(3)
456 );
457 for (key, val) in mcp_common::diagnostic::collect_mirror_env_vars() {
458 debug!("Process env: {}={}", key, val);
459 }
460}
461
462#[axum::debug_handler]
464async fn base_path_fallback_handler(
465 method: axum::http::Method,
466 uri: axum::http::Uri,
467 headers: axum::http::HeaderMap,
468) -> impl axum::response::IntoResponse {
469 let path = uri.path();
470 info!("Base path handler: {} {}", method, path);
471
472 if path.contains("/sse/proxy/") {
474 match method {
476 axum::http::Method::GET => {
477 let mcp_id = path.split("/sse/proxy/").nth(1);
479
480 if let Some(mcp_id) = mcp_id {
481 let proxy_manager = get_proxy_manager();
483 if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
484 (
486 axum::http::StatusCode::NOT_FOUND,
487 [("Content-Type", "text/plain".to_string())],
488 format!("MCP service '{}' not found", mcp_id).to_string(),
489 )
490 } else {
491 let accept_header = headers.get("accept");
493 if let Some(accept) = accept_header {
494 let accept_str = accept.to_str().unwrap_or("");
495 if accept_str.contains("text/event-stream") {
496 let redirect_uri = format!("{}/sse", path);
498 info!("SSE redirect to: {}", redirect_uri);
499 (
500 axum::http::StatusCode::FOUND,
501 [("Location", redirect_uri.to_string())],
502 "Redirecting to SSE endpoint".to_string(),
503 )
504 } else {
505 (
507 axum::http::StatusCode::BAD_REQUEST,
508 [("Content-Type", "text/plain".to_string())],
509 "SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(),
510 )
511 }
512 } else {
513 (
515 axum::http::StatusCode::BAD_REQUEST,
516 [("Content-Type", "text/plain".to_string())],
517 "SSE error: Missing Accept header, expected 'text/event-stream'"
518 .to_string(),
519 )
520 }
521 }
522 } else {
523 (
525 axum::http::StatusCode::BAD_REQUEST,
526 [("Content-Type", "text/plain".to_string())],
527 "SSE error: Invalid SSE path".to_string(),
528 )
529 }
530 }
531 axum::http::Method::POST => {
532 let redirect_uri = format!("{}/message", path);
534 info!("SSE redirect to: {}", redirect_uri);
535 (
536 axum::http::StatusCode::FOUND,
537 [("Location", redirect_uri.to_string())],
538 "Redirecting to message endpoint".to_string(),
539 )
540 }
541 _ => {
542 (
544 axum::http::StatusCode::METHOD_NOT_ALLOWED,
545 [("Allow", "GET, POST".to_string())],
546 "Only GET and POST methods are allowed".to_string(),
547 )
548 }
549 }
550 } else if path.contains("/stream/proxy/") {
551 match method {
553 axum::http::Method::GET => {
554 (
556 axum::http::StatusCode::OK,
557 [("Content-Type", "application/json".to_string())],
558 r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(),
559 )
560 }
561 axum::http::Method::POST => {
562 (
564 axum::http::StatusCode::OK,
565 [("Content-Type", "application/json".to_string())],
566 r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(),
567 )
568 }
569 _ => {
570 (
572 axum::http::StatusCode::METHOD_NOT_ALLOWED,
573 [("Allow", "GET, POST".to_string())],
574 "Only GET and POST methods are allowed".to_string(),
575 )
576 }
577 }
578 } else {
579 (
581 axum::http::StatusCode::BAD_REQUEST,
582 [("Content-Type", "text/plain".to_string())],
583 "Unknown protocol or path".to_string(),
584 )
585 }
586}