Skip to main content

mcp_stdio_proxy/server/task/
mcp_start_task.rs

1//! MCP Service Start Task
2//!
3//! This module handles starting MCP services using the Builder APIs from
4//! mcp-sse-proxy and mcp-streamable-proxy libraries.
5//!
6//! The refactored implementation removes direct rmcp dependency by delegating
7//! protocol-specific logic to the proxy libraries.
8
9use 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
25/// Start an MCP service based on configuration
26///
27/// This function creates and configures an MCP proxy service based on the
28/// provided configuration. It supports both SSE and Streamable HTTP client
29/// protocols, with automatic backend protocol detection for URL-based services.
30pub 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    // Create router path based on client protocol (determines exposed API interface)
37    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    // Use the integrated method to create the server
48    integrate_server_with_axum(
49        mcp_server_config.clone(),
50        mcp_router_path.clone(),
51        mcp_config.clone(),
52    )
53    .await
54}
55
56/// Integrate MCP server with axum router
57///
58/// This function:
59/// 1. Determines backend protocol (stdio, SSE, or Streamable HTTP)
60/// 2. Creates the appropriate server using Builder APIs
61/// 3. Registers the handler with ProxyManager
62/// 4. Sets up dynamic routing
63pub 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    // Determine backend protocol from configuration
73    let backend_protocol = match &mcp_config {
74        // Command-line config: use stdio protocol
75        McpServerConfig::Command(_) => McpProtocol::Stdio,
76        // URL config: parse type field or auto-detect
77        McpServerConfig::Url(url_config) => {
78            // Merge headers + auth_token for protocol detection (same headers as backend connection)
79            let detection_headers =
80                merge_headers_with_auth(&url_config.headers, url_config.auth_token.as_deref());
81            // 空 map 等价无 header,探测函数内部按 is_empty 判定,无需 if-empty 桥接
82            // Check type field first
83            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                        // If parsing fails, auto-detect
94                        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                // No type field, auto-detect
116                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    // Create server based on client protocol using Builder APIs
134    let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
135        // ================ Client uses SSE protocol ================
136        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            // Build backend config for SSE
143            let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
144                // Streamable HTTP backend: connect via mcp-streamable-proxy (rmcp 1.4.0),
145                // then pass as BackendBridge to decouple mcp-sse-proxy from mcp-streamable-proxy
146                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            // 对于 OneShot 服务,使用更短的 keep_alive 间隔(5秒)来保持后端活跃
158            // 防止后端进程因空闲超时而退出
159            let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
160                5
161            } else {
162                15
163            };
164
165            // 对于 OneShot 服务,禁用 stateful 模式以加快响应速度
166            // stateful=false 会跳过 MCP 初始化步骤,直接处理请求
167            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        // ================ Client uses Streamable HTTP protocol ================
193        McpProtocol::Stream => {
194            // Build backend config for Stream
195            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        // Client stdio protocol is not supported in server mode
218        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    // Clone cancellation token for monitoring
226    let ct_clone = ct.clone();
227    let mcp_id_clone = mcp_id.clone();
228
229    // Store MCP service status with full mcp_config for auto-restart
230    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    // Add MCP service status and proxy handler to global manager
240    let proxy_manager = get_proxy_manager();
241    proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
242
243    // ===== 新增:注册配置到缓存 =====
244    proxy_manager
245        .register_mcp_config(&mcp_id, full_mcp_config.clone())
246        .await;
247
248    // Add base path fallback handler for SSE protocol
249    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    // Register route to global route table
258    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    // 记录重启时间戳(仅在服务成功启动后)
277    GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
278
279    Ok((router, ct))
280}
281
282/// Connect to a Streamable HTTP backend and return a BackendBridge
283///
284/// This lives in mcp-proxy (not mcp-sse-proxy) because it uses mcp-streamable-proxy types.
285/// The returned `Arc<dyn BackendBridge>` is protocol-agnostic, allowing mcp-sse-proxy
286/// to use it without depending on mcp-streamable-proxy.
287async 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
318/// Build SSE backend configuration from MCP server config
319fn 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                // 合并 auth_token → Authorization,与探测/Stream 连接路径共用 merge_headers_with_auth
339                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
358/// Build Stream backend configuration from MCP server config
359fn 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                    // Note: StreamServerBuilder currently only supports Streamable HTTP URL backend
379                    // SSE backend with Stream frontend would require protocol conversion
380                    // For now, we return an error for this combination
381                    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                    // 合并 auth_token → Authorization,与 SSE 后端 / 探测路径一致
392                    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
410/// 合并配置 headers 与 auth_token,并规范化 Authorization(补 "Bearer " 前缀)。
411///
412/// 协议探测与后端连接(SSE / Stream)共用此函数,确保两者使用完全一致的 headers,
413/// 避免出现「探测带鉴权但连接不带」(或反之)导致协议误判或连接失败。
414fn 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
428/// 规范化 headers:确保 Authorization header 有 "Bearer " 前缀
429///
430/// 与 client 模式 (`convert.rs:build_mcp_config`) 行为一致,复用
431/// [`normalize_authorization`](crate::client::support::normalize_authorization) 统一处理。
432fn 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
447/// Log command execution details for debugging
448fn 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    // 只输出 env 变量的 key 列表,避免泄露敏感 value
457    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    // 输出进程级关键环境变量(PATH 摘要 + 镜像变量)
465    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/// Base path fallback handler - supports direct access to base path with automatic redirection
475#[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    // Determine if SSE or Stream protocol
485    if path.contains("/sse/proxy/") {
486        // SSE protocol handling
487        match method {
488            axum::http::Method::GET => {
489                // Extract MCP ID from path
490                let mcp_id = path.split("/sse/proxy/").nth(1);
491
492                if let Some(mcp_id) = mcp_id {
493                    // Check if MCP service exists
494                    let proxy_manager = get_proxy_manager();
495                    if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
496                        // MCP service not found
497                        (
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                        // MCP service exists, check Accept header
504                        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                                // Correct Accept header, redirect to /sse
509                                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                                // Incorrect Accept header
518                                (
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                            // No Accept header
526                            (
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                    // Cannot extract MCP ID from path
536                    (
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                // POST request redirect to /message
545                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                // Other methods return 405 Method Not Allowed
555                (
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        // Stream protocol handling - return success directly without redirect
564        match method {
565            axum::http::Method::GET => {
566                // GET request returns server info
567                (
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                // POST request returns success, let StreamableHttpService handle
575                (
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                // Other methods return 405 Method Not Allowed
583                (
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        // Unknown protocol
592        (
593            axum::http::StatusCode::BAD_REQUEST,
594            [("Content-Type", "text/plain".to_string())],
595            "Unknown protocol or path".to_string(),
596        )
597    }
598}