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};
23
24/// Start an MCP service based on configuration
25///
26/// This function creates and configures an MCP proxy service based on the
27/// provided configuration. It supports both SSE and Streamable HTTP client
28/// protocols, with automatic backend protocol detection for URL-based services.
29pub async fn mcp_start_task(
30    mcp_config: McpConfig,
31) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
32    let mcp_id = mcp_config.mcp_id.clone();
33    let client_protocol = mcp_config.client_protocol.clone();
34
35    // Create router path based on client protocol (determines exposed API interface)
36    let mcp_router_path: McpRouterPath = McpRouterPath::new(mcp_id, client_protocol)
37        .map_err(|e| AppError::mcp_server_error(e.to_string()))?;
38
39    let mcp_json_config = mcp_config
40        .mcp_json_config
41        .clone()
42        .expect("mcp_json_config is required");
43
44    let mcp_server_config = McpServerConfig::try_from(mcp_json_config)?;
45
46    // Use the integrated method to create the server
47    integrate_server_with_axum(
48        mcp_server_config.clone(),
49        mcp_router_path.clone(),
50        mcp_config.clone(),
51    )
52    .await
53}
54
55/// Integrate MCP server with axum router
56///
57/// This function:
58/// 1. Determines backend protocol (stdio, SSE, or Streamable HTTP)
59/// 2. Creates the appropriate server using Builder APIs
60/// 3. Registers the handler with ProxyManager
61/// 4. Sets up dynamic routing
62pub async fn integrate_server_with_axum(
63    mcp_config: McpServerConfig,
64    mcp_router_path: McpRouterPath,
65    full_mcp_config: McpConfig,
66) -> Result<(axum::Router, tokio_util::sync::CancellationToken)> {
67    let mcp_type = full_mcp_config.mcp_type.clone();
68    let base_path = mcp_router_path.base_path.clone();
69    let mcp_id = mcp_router_path.mcp_id.clone();
70
71    // Determine backend protocol from configuration
72    let backend_protocol = match &mcp_config {
73        // Command-line config: use stdio protocol
74        McpServerConfig::Command(_) => McpProtocol::Stdio,
75        // URL config: parse type field or auto-detect
76        McpServerConfig::Url(url_config) => {
77            // Merge headers + auth_token for protocol detection
78            let mut detection_headers = url_config.headers.clone().unwrap_or_default();
79            if let Some(auth_token) = &url_config.auth_token {
80                detection_headers.insert("Authorization".to_string(), auth_token.clone());
81            }
82            let detection_headers_ref = if detection_headers.is_empty() {
83                None
84            } else {
85                Some(&detection_headers)
86            };
87
88            // Check type field first
89            if let Some(type_str) = &url_config.r#type {
90                match type_str.parse::<McpProtocol>() {
91                    Ok(protocol) => {
92                        debug!(
93                            "Using configured protocol type: {} -> {:?}",
94                            type_str, protocol
95                        );
96                        protocol
97                    }
98                    Err(_) => {
99                        // If parsing fails, auto-detect
100                        debug!("Protocol type '{}' unrecognized, auto-detecting", type_str);
101                        let detected_protocol = crate::server::detect_mcp_protocol_with_headers(
102                            url_config.get_url(),
103                            detection_headers_ref,
104                        )
105                        .await
106                        .map_err(|e| {
107                            anyhow::anyhow!(
108                                "Protocol type '{}' unrecognized and auto-detection failed: {}",
109                                type_str,
110                                e
111                            )
112                        })?;
113                        debug!(
114                            "Auto-detected protocol: {:?} (original config: '{}')",
115                            detected_protocol, type_str
116                        );
117                        detected_protocol
118                    }
119                }
120            } else {
121                // No type field, auto-detect
122                debug!("No type field specified, auto-detecting protocol");
123
124                crate::server::detect_mcp_protocol_with_headers(
125                    url_config.get_url(),
126                    detection_headers_ref,
127                )
128                .await
129                .map_err(|e| anyhow::anyhow!("Auto-detection failed: {}", e))?
130            }
131        }
132    };
133
134    debug!(
135        "MCP ID: {}, client protocol: {:?}, backend protocol: {:?}",
136        mcp_id, mcp_router_path.mcp_protocol, backend_protocol
137    );
138
139    // Create server based on client protocol using Builder APIs
140    let (router, ct, handler) = match mcp_router_path.mcp_protocol.clone() {
141        // ================ Client uses SSE protocol ================
142        McpProtocol::Sse => {
143            let sse_path = match &mcp_router_path.mcp_protocol_path {
144                McpProtocolPath::SsePath(sse_path) => sse_path,
145                _ => unreachable!(),
146            };
147
148            // Build backend config for SSE
149            let backend_config = if matches!(backend_protocol, McpProtocol::Stream) {
150                // Streamable HTTP backend: connect via mcp-streamable-proxy (rmcp 1.4.0),
151                // then pass as BackendBridge to decouple mcp-sse-proxy from mcp-streamable-proxy
152                let bridge = connect_stream_backend(&mcp_config, &mcp_id).await?;
153                SseBackendConfig::BackendBridge(bridge)
154            } else {
155                build_sse_backend_config(&mcp_config, backend_protocol)?
156            };
157
158            debug!(
159                "Creating SSE server, sse_path={}, post_path={}",
160                sse_path.sse_path, sse_path.message_path
161            );
162
163            // 对于 OneShot 服务,使用更短的 keep_alive 间隔(5秒)来保持后端活跃
164            // 防止后端进程因空闲超时而退出
165            let keep_alive_secs = if matches!(mcp_type, McpType::OneShot) {
166                5
167            } else {
168                15
169            };
170
171            // 对于 OneShot 服务,禁用 stateful 模式以加快响应速度
172            // stateful=false 会跳过 MCP 初始化步骤,直接处理请求
173            let stateful = !matches!(mcp_type, McpType::OneShot);
174
175            let (router, ct, handler) = SseServerBuilder::new(backend_config)
176                .mcp_id(mcp_id.clone())
177                .sse_path(sse_path.sse_path.clone())
178                .post_path(sse_path.message_path.clone())
179                .keep_alive(keep_alive_secs)
180                .stateful(stateful)
181                .build()
182                .await
183                .with_context(|| {
184                    format!(
185                        "SSE server build failed - MCP ID: {}, type: {:?}",
186                        mcp_id, mcp_type
187                    )
188                })?;
189
190            info!(
191                "SSE server started - MCP ID: {}, type: {:?}",
192                mcp_router_path.mcp_id, mcp_type
193            );
194
195            (router, ct, McpHandler::Sse(Box::new(handler)))
196        }
197
198        // ================ Client uses Streamable HTTP protocol ================
199        McpProtocol::Stream => {
200            // Build backend config for Stream
201            let backend_config = build_stream_backend_config(&mcp_config, backend_protocol)?;
202
203            let (router, ct, handler) = StreamServerBuilder::new(backend_config)
204                .mcp_id(mcp_id.clone())
205                .stateful(false)
206                .build()
207                .await
208                .with_context(|| {
209                    format!(
210                        "Stream server build failed - MCP ID: {}, type: {:?}",
211                        mcp_id, mcp_type
212                    )
213                })?;
214
215            info!(
216                "Streamable HTTP server started - MCP ID: {}, type: {:?}",
217                mcp_router_path.mcp_id, mcp_type
218            );
219
220            (router, ct, McpHandler::Stream(Box::new(handler)))
221        }
222
223        // Client stdio protocol is not supported in server mode
224        McpProtocol::Stdio => {
225            return Err(anyhow::anyhow!(
226                "Client protocol cannot be Stdio. McpRouterPath::new does not support creating Stdio protocol router paths"
227            ));
228        }
229    };
230
231    // Clone cancellation token for monitoring
232    let ct_clone = ct.clone();
233    let mcp_id_clone = mcp_id.clone();
234
235    // Store MCP service status with full mcp_config for auto-restart
236    let mcp_service_status = McpServiceStatus::new(
237        mcp_id_clone.clone(),
238        mcp_type.clone(),
239        mcp_router_path.clone(),
240        ct_clone.clone(),
241        CheckMcpStatusResponseStatus::Ready,
242    )
243    .with_mcp_config(full_mcp_config.clone());
244
245    // Add MCP service status and proxy handler to global manager
246    let proxy_manager = get_proxy_manager();
247    proxy_manager.add_mcp_service_status_and_proxy(mcp_service_status, Some(handler));
248
249    // ===== 新增:注册配置到缓存 =====
250    proxy_manager
251        .register_mcp_config(&mcp_id, full_mcp_config.clone())
252        .await;
253
254    // Add base path fallback handler for SSE protocol
255    let router = if matches!(mcp_router_path.mcp_protocol, McpProtocol::Sse) {
256        let modified_router = router.fallback(base_path_fallback_handler);
257        info!("SSE base path handler added, base_path: {}", base_path);
258        modified_router
259    } else {
260        router
261    };
262
263    // Register route to global route table
264    info!(
265        "Registering route: base_path={}, mcp_id={}",
266        base_path, mcp_id
267    );
268    info!(
269        "SSE path config: sse_path={}, post_path={}",
270        match &mcp_router_path.mcp_protocol_path {
271            McpProtocolPath::SsePath(sse_path) => &sse_path.sse_path,
272            _ => "N/A",
273        },
274        match &mcp_router_path.mcp_protocol_path {
275            McpProtocolPath::SsePath(sse_path) => &sse_path.message_path,
276            _ => "N/A",
277        }
278    );
279    DynamicRouterService::register_route(&base_path, router.clone());
280    info!("Route registration complete: base_path={}", base_path);
281
282    // 记录重启时间戳(仅在服务成功启动后)
283    GLOBAL_RESTART_TRACKER.record_restart(&mcp_id);
284
285    Ok((router, ct))
286}
287
288/// Connect to a Streamable HTTP backend and return a BackendBridge
289///
290/// This lives in mcp-proxy (not mcp-sse-proxy) because it uses mcp-streamable-proxy types.
291/// The returned `Arc<dyn BackendBridge>` is protocol-agnostic, allowing mcp-sse-proxy
292/// to use it without depending on mcp-streamable-proxy.
293async fn connect_stream_backend(
294    mcp_config: &McpServerConfig,
295    mcp_id: &str,
296) -> Result<std::sync::Arc<dyn mcp_common::BackendBridge>> {
297    use crate::proxy::{StreamClientConnection, StreamProxyHandler};
298
299    let url_config = match mcp_config {
300        McpServerConfig::Url(url_config) => url_config,
301        _ => {
302            return Err(anyhow::anyhow!(
303                "Stream backend requires URL-based config"
304            ))
305        }
306    };
307
308    let url = url_config.get_url();
309    info!(
310        "Connecting to Streamable HTTP backend (SSE frontend) \
311         - MCP ID: {}, URL: {}",
312        mcp_id, url
313    );
314
315    let mut config = mcp_common::McpClientConfig::new(url.to_string());
316    if let Some(ref headers) = url_config.headers {
317        for (k, v) in headers {
318            config = config.with_header(k, v);
319        }
320    }
321    // auth_token 合并到 Authorization header(与 build_sse_backend_config 逻辑一致)
322    if let Some(ref auth_token) = url_config.auth_token {
323        config = config.with_header("Authorization", auth_token);
324    }
325
326    let conn = StreamClientConnection::connect(config).await?;
327    let proxy_handler =
328        StreamProxyHandler::with_mcp_id(conn.into_running_service(), mcp_id.to_string());
329
330    Ok(std::sync::Arc::new(proxy_handler))
331}
332
333/// Build SSE backend configuration from MCP server config
334fn build_sse_backend_config(
335    mcp_config: &McpServerConfig,
336    backend_protocol: McpProtocol,
337) -> Result<SseBackendConfig> {
338    match mcp_config {
339        McpServerConfig::Command(cmd_config) => {
340            log_command_details(cmd_config);
341            Ok(SseBackendConfig::Stdio {
342                command: cmd_config.command.clone(),
343                args: cmd_config.args.clone(),
344                env: cmd_config.env.clone(),
345            })
346        }
347        McpServerConfig::Url(url_config) => match backend_protocol {
348            McpProtocol::Stdio => Err(anyhow::anyhow!(
349                "URL-based MCP service cannot use Stdio protocol"
350            )),
351            McpProtocol::Sse => {
352                info!("Connecting to SSE backend: {}", url_config.get_url());
353                Ok(SseBackendConfig::SseUrl {
354                    url: url_config.get_url().to_string(),
355                    headers: url_config.headers.clone(),
356                })
357            }
358            McpProtocol::Stream => Err(anyhow::anyhow!(
359                "Stream backend should be handled via connect_stream_backend(), \
360                 not build_sse_backend_config()"
361            ))
362        },
363    }
364}
365
366/// Build Stream backend configuration from MCP server config
367fn build_stream_backend_config(
368    mcp_config: &McpServerConfig,
369    backend_protocol: McpProtocol,
370) -> Result<StreamBackendConfig> {
371    match mcp_config {
372        McpServerConfig::Command(cmd_config) => {
373            log_command_details(cmd_config);
374            Ok(StreamBackendConfig::Stdio {
375                command: cmd_config.command.clone(),
376                args: cmd_config.args.clone(),
377                env: cmd_config.env.clone(),
378            })
379        }
380        McpServerConfig::Url(url_config) => {
381            match backend_protocol {
382                McpProtocol::Stdio => Err(anyhow::anyhow!(
383                    "URL-based MCP service cannot use Stdio protocol"
384                )),
385                McpProtocol::Sse => {
386                    // Note: StreamServerBuilder currently only supports Streamable HTTP URL backend
387                    // SSE backend with Stream frontend would require protocol conversion
388                    // For now, we return an error for this combination
389                    Err(anyhow::anyhow!(
390                        "SSE backend with Streamable HTTP frontend is not yet supported. \
391                         Please use SSE frontend or configure a Streamable HTTP backend."
392                    ))
393                }
394                McpProtocol::Stream => {
395                    info!(
396                        "Connecting to Streamable HTTP backend: {}",
397                        url_config.get_url()
398                    );
399                    Ok(StreamBackendConfig::Url {
400                        url: url_config.get_url().to_string(),
401                        headers: url_config.headers.clone(),
402                    })
403                }
404            }
405        }
406    }
407}
408
409/// Log command execution details for debugging
410fn log_command_details(mcp_config: &McpServerCommandConfig) {
411    let args_str = mcp_config
412        .args
413        .as_ref()
414        .map_or(String::new(), |args| args.join(" "));
415
416    info!("Executing command: {} {}", mcp_config.command, args_str);
417
418    // 只输出 env 变量的 key 列表,避免泄露敏感 value
419    if let Some(env_vars) = &mcp_config.env {
420        let keys: Vec<&String> = env_vars.keys().collect();
421        if !keys.is_empty() {
422            debug!("Config env keys: {:?}", keys);
423        }
424    }
425
426    // 输出进程级关键环境变量(PATH 摘要 + 镜像变量)
427    debug!(
428        "Process PATH: {}",
429        mcp_common::diagnostic::format_path_summary(3)
430    );
431    for (key, val) in mcp_common::diagnostic::collect_mirror_env_vars() {
432        debug!("Process env: {}={}", key, val);
433    }
434}
435
436/// Base path fallback handler - supports direct access to base path with automatic redirection
437#[axum::debug_handler]
438async fn base_path_fallback_handler(
439    method: axum::http::Method,
440    uri: axum::http::Uri,
441    headers: axum::http::HeaderMap,
442) -> impl axum::response::IntoResponse {
443    let path = uri.path();
444    info!("Base path handler: {} {}", method, path);
445
446    // Determine if SSE or Stream protocol
447    if path.contains("/sse/proxy/") {
448        // SSE protocol handling
449        match method {
450            axum::http::Method::GET => {
451                // Extract MCP ID from path
452                let mcp_id = path.split("/sse/proxy/").nth(1);
453
454                if let Some(mcp_id) = mcp_id {
455                    // Check if MCP service exists
456                    let proxy_manager = get_proxy_manager();
457                    if proxy_manager.get_mcp_service_status(mcp_id).is_none() {
458                        // MCP service not found
459                        (
460                            axum::http::StatusCode::NOT_FOUND,
461                            [("Content-Type", "text/plain".to_string())],
462                            format!("MCP service '{}' not found", mcp_id).to_string(),
463                        )
464                    } else {
465                        // MCP service exists, check Accept header
466                        let accept_header = headers.get("accept");
467                        if let Some(accept) = accept_header {
468                            let accept_str = accept.to_str().unwrap_or("");
469                            if accept_str.contains("text/event-stream") {
470                                // Correct Accept header, redirect to /sse
471                                let redirect_uri = format!("{}/sse", path);
472                                info!("SSE redirect to: {}", redirect_uri);
473                                (
474                                    axum::http::StatusCode::FOUND,
475                                    [("Location", redirect_uri.to_string())],
476                                    "Redirecting to SSE endpoint".to_string(),
477                                )
478                            } else {
479                                // Incorrect Accept header
480                                (
481                                    axum::http::StatusCode::BAD_REQUEST,
482                                    [("Content-Type", "text/plain".to_string())],
483                                    "SSE error: Invalid Accept header, expected 'text/event-stream'".to_string(),
484                                )
485                            }
486                        } else {
487                            // No Accept header
488                            (
489                                axum::http::StatusCode::BAD_REQUEST,
490                                [("Content-Type", "text/plain".to_string())],
491                                "SSE error: Missing Accept header, expected 'text/event-stream'"
492                                    .to_string(),
493                            )
494                        }
495                    }
496                } else {
497                    // Cannot extract MCP ID from path
498                    (
499                        axum::http::StatusCode::BAD_REQUEST,
500                        [("Content-Type", "text/plain".to_string())],
501                        "SSE error: Invalid SSE path".to_string(),
502                    )
503                }
504            }
505            axum::http::Method::POST => {
506                // POST request redirect to /message
507                let redirect_uri = format!("{}/message", path);
508                info!("SSE redirect to: {}", redirect_uri);
509                (
510                    axum::http::StatusCode::FOUND,
511                    [("Location", redirect_uri.to_string())],
512                    "Redirecting to message endpoint".to_string(),
513                )
514            }
515            _ => {
516                // Other methods return 405 Method Not Allowed
517                (
518                    axum::http::StatusCode::METHOD_NOT_ALLOWED,
519                    [("Allow", "GET, POST".to_string())],
520                    "Only GET and POST methods are allowed".to_string(),
521                )
522            }
523        }
524    } else if path.contains("/stream/proxy/") {
525        // Stream protocol handling - return success directly without redirect
526        match method {
527            axum::http::Method::GET => {
528                // GET request returns server info
529                (
530                    axum::http::StatusCode::OK,
531                    [("Content-Type", "application/json".to_string())],
532                    r#"{"jsonrpc":"2.0","result":{"info":"Streamable MCP Server","version":"1.0"}}"#.to_string(),
533                )
534            }
535            axum::http::Method::POST => {
536                // POST request returns success, let StreamableHttpService handle
537                (
538                    axum::http::StatusCode::OK,
539                    [("Content-Type", "application/json".to_string())],
540                    r#"{"jsonrpc":"2.0","result":{"message":"Stream request received","protocol":"streamable-http"}}"#.to_string(),
541                )
542            }
543            _ => {
544                // Other methods return 405 Method Not Allowed
545                (
546                    axum::http::StatusCode::METHOD_NOT_ALLOWED,
547                    [("Allow", "GET, POST".to_string())],
548                    "Only GET and POST methods are allowed".to_string(),
549                )
550            }
551        }
552    } else {
553        // Unknown protocol
554        (
555            axum::http::StatusCode::BAD_REQUEST,
556            [("Content-Type", "text/plain".to_string())],
557            "Unknown protocol or path".to_string(),
558        )
559    }
560}