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
79            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            // Check type field first
95            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                        // If parsing fails, auto-detect
106                        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                // No type field, auto-detect
128                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    // Create server based on client protocol using Builder APIs
146    let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
147        // ================ Client uses SSE protocol ================
148        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            // Build backend config for SSE
155            let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
156                // Streamable HTTP backend: connect via mcp-streamable-proxy (rmcp 1.4.0),
157                // then pass as BackendBridge to decouple mcp-sse-proxy from mcp-streamable-proxy
158                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            // 对于 OneShot 服务,使用更短的 keep_alive 间隔(5秒)来保持后端活跃
170            // 防止后端进程因空闲超时而退出
171            let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
172                5
173            } else {
174                15
175            };
176
177            // 对于 OneShot 服务,禁用 stateful 模式以加快响应速度
178            // stateful=false 会跳过 MCP 初始化步骤,直接处理请求
179            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        // ================ Client uses Streamable HTTP protocol ================
205        McpProtocol::Stream => {
206            // Build backend config for Stream
207            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        // Client stdio protocol is not supported in server mode
230        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    // Clone cancellation token for monitoring
238    let ct_clone = ct.clone();
239    let mcp_id_clone = mcp_id.clone();
240
241    // Store MCP service status with full mcp_config for auto-restart
242    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    // Add MCP service status and proxy handler to global manager
252    let proxy_manager = get_proxy_manager();
253    proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
254
255    // ===== 新增:注册配置到缓存 =====
256    proxy_manager
257        .register_mcp_config(&mcp_id, full_mcp_config.clone())
258        .await;
259
260    // Add base path fallback handler for SSE protocol
261    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    // Register route to global route table
270    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    // 记录重启时间戳(仅在服务成功启动后)
289    GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
290
291    Ok((router, ct))
292}
293
294/// Connect to a Streamable HTTP backend and return a BackendBridge
295///
296/// This lives in mcp-proxy (not mcp-sse-proxy) because it uses mcp-streamable-proxy types.
297/// The returned `Arc<dyn BackendBridge>` is protocol-agnostic, allowing mcp-sse-proxy
298/// to use it without depending on mcp-streamable-proxy.
299async 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    // auth_token 合并到 Authorization header(与 build_sse_backend_config 逻辑一致)
325    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
341/// Build SSE backend configuration from MCP server config
342fn 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
374/// Build Stream backend configuration from MCP server config
375fn 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                    // Note: StreamServerBuilder currently only supports Streamable HTTP URL backend
395                    // SSE backend with Stream frontend would require protocol conversion
396                    // For now, we return an error for this combination
397                    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
417/// 规范化 headers:确保 Authorization header 有 "Bearer " 前缀
418///
419/// 与 client 模式 (`convert.rs:build_mcp_config`) 行为一致,
420/// 对没有 "Bearer " 前缀的 Authorization header 自动添加前缀。
421fn 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
435/// Log command execution details for debugging
436fn 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    // 只输出 env 变量的 key 列表,避免泄露敏感 value
445    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    // 输出进程级关键环境变量(PATH 摘要 + 镜像变量)
453    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/// Base path fallback handler - supports direct access to base path with automatic redirection
463#[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    // Determine if SSE or Stream protocol
473    if path.contains("/sse/proxy/") {
474        // SSE protocol handling
475        match method {
476            axum::http::Method::GET => {
477                // Extract MCP ID from path
478                let mcp_id = path.split("/sse/proxy/").nth(1);
479
480                if let Some(mcp_id) = mcp_id {
481                    // Check if MCP service exists
482                    let proxy_manager = get_proxy_manager();
483                    if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
484                        // MCP service not found
485                        (
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                        // MCP service exists, check Accept header
492                        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                                // Correct Accept header, redirect to /sse
497                                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                                // Incorrect Accept header
506                                (
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                            // No Accept header
514                            (
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                    // Cannot extract MCP ID from path
524                    (
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                // POST request redirect to /message
533                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                // Other methods return 405 Method Not Allowed
543                (
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        // Stream protocol handling - return success directly without redirect
552        match method {
553            axum::http::Method::GET => {
554                // GET request returns server info
555                (
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                // POST request returns success, let StreamableHttpService handle
563                (
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                // Other methods return 405 Method Not Allowed
571                (
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        // Unknown protocol
580        (
581            axum::http::StatusCode::BAD_REQUEST,
582            [("Content-Type", "text/plain".to_string())],
583            "Unknown protocol or path".to_string(),
584        )
585    }
586}