1use std::collections::HashMap;
7use std::sync::Arc;
8use std::time::Duration;
9
10use anyhow::{Context as _, Result};
11use tokio_util::sync::CancellationToken;
12use tracing::{debug, info, warn};
13
14use process_wrap::tokio::{KillOnDrop, TokioCommandWrap};
16
17#[cfg(unix)]
18use process_wrap::tokio::ProcessGroup;
19
20#[cfg(windows)]
21use process_wrap::tokio::JobObject;
22
23use rmcp::{
24 ServiceExt,
25 model::{ClientCapabilities, ClientInfo, ProtocolVersion},
26 transport::{
27 SseClientTransport, TokioChildProcess,
28 sse_client::SseClientConfig,
29 sse_server::{SseServer, SseServerConfig},
30 },
31};
32
33use crate::{
34 SseHandler, ToolFilter,
35 sse_handler::{BackendSessionHandler, SseServerHandler},
36};
37
38const STDIO_SLOW_THRESHOLD_SECS: u64 = 30;
40
41const HTTP_SLOW_THRESHOLD_SECS: u64 = 10;
43
44#[derive(Clone)]
48pub enum BackendConfig {
49 Stdio {
51 command: String,
53 args: Option<Vec<String>>,
55 env: Option<HashMap<String, String>>,
57 },
58 SseUrl {
60 url: String,
62 headers: Option<HashMap<String, String>>,
64 },
65 BackendBridge(Arc<dyn mcp_common::BackendBridge>),
72}
73
74impl std::fmt::Debug for BackendConfig {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 BackendConfig::Stdio { command, args, .. } => f
78 .debug_struct("Stdio")
79 .field("command", command)
80 .field("args", args)
81 .finish(),
82 BackendConfig::SseUrl { url, .. } => {
83 f.debug_struct("SseUrl").field("url", url).finish()
84 }
85 BackendConfig::BackendBridge(bridge) => f
86 .debug_struct("BackendBridge")
87 .field("mcp_id", &bridge.mcp_id())
88 .finish(),
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
95pub struct SseServerBuilderConfig {
96 pub sse_path: String,
98 pub post_path: String,
100 pub mcp_id: Option<String>,
102 pub tool_filter: Option<ToolFilter>,
104 pub keep_alive_secs: u64,
106 pub stateful: bool,
109}
110
111impl Default for SseServerBuilderConfig {
112 fn default() -> Self {
113 Self {
114 sse_path: "/sse".into(),
115 post_path: "/message".into(),
116 mcp_id: None,
117 tool_filter: None,
118 keep_alive_secs: 15,
119 stateful: true,
120 }
121 }
122}
123
124fn log_connection_timing(
135 mcp_id: &str,
136 backend_type: &str,
137 total_duration: Duration,
138 breakdown: &[(&str, Duration)],
139 warn_threshold_secs: u64,
140 warn_message: &str,
141) {
142 let breakdown_str: Vec<String> = breakdown
143 .iter()
144 .map(|(name, dur)| format!("{}: {:?}", name, dur))
145 .collect();
146
147 info!(
148 "[SseServerBuilder] {} backend connected successfully - MCP ID: {}, total: {:?} ({})",
149 backend_type,
150 mcp_id,
151 total_duration,
152 breakdown_str.join(", ")
153 );
154
155 if total_duration.as_secs() >= warn_threshold_secs {
156 warn!(
157 "[SseServerBuilder] {} backend connection takes a long time - MCP ID: {}, time: {:?}, {}",
158 backend_type, mcp_id, total_duration, warn_message
159 );
160 }
161}
162
163pub struct SseServerBuilder {
186 backend_config: BackendConfig,
187 server_config: SseServerBuilderConfig,
188}
189
190impl SseServerBuilder {
191 pub fn new(backend: BackendConfig) -> Self {
193 Self {
194 backend_config: backend,
195 server_config: SseServerBuilderConfig::default(),
196 }
197 }
198
199 pub fn sse_path(mut self, path: impl Into<String>) -> Self {
201 self.server_config.sse_path = path.into();
202 self
203 }
204
205 pub fn post_path(mut self, path: impl Into<String>) -> Self {
207 self.server_config.post_path = path.into();
208 self
209 }
210
211 pub fn mcp_id(mut self, id: impl Into<String>) -> Self {
215 self.server_config.mcp_id = Some(id.into());
216 self
217 }
218
219 pub fn tool_filter(mut self, filter: ToolFilter) -> Self {
221 self.server_config.tool_filter = Some(filter);
222 self
223 }
224
225 pub fn keep_alive(mut self, secs: u64) -> Self {
227 self.server_config.keep_alive_secs = secs;
228 self
229 }
230
231 pub fn stateful(mut self, stateful: bool) -> Self {
236 self.server_config.stateful = stateful;
237 self
238 }
239
240 pub async fn build(self) -> Result<(axum::Router, CancellationToken, SseServerHandler)> {
246 let mcp_id = self
247 .server_config
248 .mcp_id
249 .clone()
250 .unwrap_or_else(|| "sse-proxy".into());
251
252 let client_info = ClientInfo {
254 protocol_version: ProtocolVersion::V_2024_11_05,
255 capabilities: ClientCapabilities::builder()
256 .enable_experimental()
257 .enable_roots()
258 .enable_roots_list_changed()
259 .enable_sampling()
260 .build(),
261 ..Default::default()
262 };
263
264 let client = match &self.backend_config {
266 BackendConfig::Stdio { command, args, env } => {
267 self.connect_stdio(command, args, env, &client_info).await?
268 }
269 BackendConfig::SseUrl { url, headers } => {
270 self.connect_sse_url(url, headers, &client_info).await?
271 }
272 BackendConfig::BackendBridge(bridge) => {
273 let handler = BackendSessionHandler::new(bridge.clone(), mcp_id.clone());
274 let (router, ct, handler_for_return) =
275 self.create_server_with_backend_session(handler).await?;
276
277 info!(
278 "[SseServerBuilder] Server created with backend bridge \
279 - mcp_id: {}, sse_path: {}, post_path: {}",
280 mcp_id, self.server_config.sse_path, self.server_config.post_path
281 );
282
283 return Ok((
284 router,
285 ct,
286 SseServerHandler::BackendSession(handler_for_return),
287 ));
288 }
289 };
290
291 let sse_handler = if let Some(ref tool_filter) = self.server_config.tool_filter {
293 SseHandler::with_tool_filter(client, mcp_id.clone(), tool_filter.clone())
294 } else {
295 SseHandler::with_mcp_id(client, mcp_id.clone())
296 };
297
298 let handler_for_return = sse_handler.clone();
300
301 let (router, ct) = self.create_server(sse_handler)?;
303
304 info!(
305 "[SseServerBuilder] Server created - mcp_id: {}, sse_path: {}, post_path: {}",
306 mcp_id, self.server_config.sse_path, self.server_config.post_path
307 );
308
309 Ok((router, ct, SseServerHandler::Sse(handler_for_return)))
310 }
311
312 async fn connect_stdio(
314 &self,
315 command: &str,
316 args: &Option<Vec<String>>,
317 env: &Option<HashMap<String, String>>,
318 client_info: &ClientInfo,
319 ) -> Result<rmcp::service::RunningService<rmcp::RoleClient, ClientInfo>> {
320 use std::time::Instant;
321
322 let start_time = Instant::now();
323 let mcp_id = self
324 .server_config
325 .mcp_id
326 .clone()
327 .unwrap_or_else(|| "unknown".into());
328
329 let mut wrapped_cmd = TokioCommandWrap::with_new(command, |cmd| {
334 if let Some(cmd_args) = args {
335 cmd.args(cmd_args);
336 }
337 if let Some(env_vars) = env {
339 for (k, v) in env_vars {
340 cmd.env(k, v);
341 }
342 }
343 });
344
345 #[cfg(unix)]
347 wrapped_cmd.wrap(ProcessGroup::leader());
348 #[cfg(windows)]
350 {
351 use process_wrap::tokio::CreationFlags;
352 use windows::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
353 wrapped_cmd.wrap(CreationFlags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP));
354 wrapped_cmd.wrap(JobObject);
355 }
356
357 wrapped_cmd.wrap(KillOnDrop);
359
360 info!(
361 "[SseServerBuilder] Starting child process - MCP ID: {}, command: {}, args: {:?}",
362 mcp_id,
363 command,
364 args.as_ref().unwrap_or(&vec![])
365 );
366
367 mcp_common::diagnostic::log_stdio_spawn_context("SseServerBuilder", &mcp_id, env);
369
370 let process_start = Instant::now();
371 let (tokio_process, child_stderr) = TokioChildProcess::builder(wrapped_cmd)
374 .stderr(std::process::Stdio::piped())
375 .spawn()
376 .map_err(|e| {
377 anyhow::anyhow!(
378 "{}",
379 mcp_common::diagnostic::format_spawn_error(&mcp_id, command, args, e)
380 )
381 })?;
382
383 if let Some(stderr_pipe) = child_stderr {
385 mcp_common::spawn_stderr_reader(stderr_pipe, mcp_id.clone());
386 }
387
388 let process_duration = process_start.elapsed();
389
390 debug!(
391 "[SseServerBuilder] Child process spawned - MCP ID: {}, spawn time: {:?}",
392 mcp_id, process_duration
393 );
394
395 let serve_start = Instant::now();
396 let client = client_info.clone().serve(tokio_process).await?;
397 let serve_duration = serve_start.elapsed();
398 let total_duration = start_time.elapsed();
399
400 let warn_msg = "建议的优化方案: \
401 1) 检查网络连接速度 (npm 包下载) \
402 2) 配置国内 npm 镜像 (如淘宝镜像: npm config set registry https://registry.npmmirror.com) \
403 3) 预热服务 (启动 mcp-proxy 时预先加载常用服务) \
404 4) 检查命令参数是否正确";
405
406 log_connection_timing(
407 &mcp_id,
408 "Stdio",
409 total_duration,
410 &[("spawn", process_duration), ("serve", serve_duration)],
411 STDIO_SLOW_THRESHOLD_SECS,
412 warn_msg,
413 );
414
415 Ok(client)
416 }
417
418 async fn connect_sse_url(
420 &self,
421 url: &str,
422 headers: &Option<HashMap<String, String>>,
423 client_info: &ClientInfo,
424 ) -> Result<rmcp::service::RunningService<rmcp::RoleClient, ClientInfo>> {
425 use std::time::Instant;
426
427 let start_time = Instant::now();
428 let mcp_id = self
429 .server_config
430 .mcp_id
431 .clone()
432 .unwrap_or_else(|| "unknown".into());
433
434 info!(
435 "[SseServerBuilder] Connecting to SSE URL backend - MCP ID: {}, URL: {}",
436 mcp_id, url
437 );
438
439 let mut req_headers = reqwest::header::HeaderMap::new();
441
442 if let Some(config_headers) = headers {
443 for (key, value) in config_headers {
444 req_headers.insert(
445 reqwest::header::HeaderName::try_from(key)
446 .with_context(|| format!("Invalid header name '{key}'"))?,
447 value
448 .parse()
449 .with_context(|| format!("Invalid header value for '{key}'"))?,
450 );
451 }
452 }
453
454 let http_client = reqwest::Client::builder()
455 .default_headers(req_headers)
456 .build()
457 .context("Failed to create HTTP client")?;
458
459 let sse_config = SseClientConfig {
461 sse_endpoint: url.to_string().into(),
462 ..Default::default()
463 };
464
465 let transport_start = Instant::now();
466 let sse_transport = SseClientTransport::start_with_client(http_client, sse_config).await?;
467 let transport_duration = transport_start.elapsed();
468
469 let serve_start = Instant::now();
470 let client = client_info.clone().serve(sse_transport).await?;
471 let serve_duration = serve_start.elapsed();
472 let total_duration = start_time.elapsed();
473
474 log_connection_timing(
475 &mcp_id,
476 "SSE",
477 total_duration,
478 &[("transport", transport_duration), ("serve", serve_duration)],
479 HTTP_SLOW_THRESHOLD_SECS,
480 "建议: 检查网络连接和后端服务状态",
481 );
482
483 Ok(client)
484 }
485
486 fn create_server(&self, sse_handler: SseHandler) -> Result<(axum::Router, CancellationToken)> {
488 let config = SseServerConfig {
491 bind: "0.0.0.0:0".parse()?,
492 sse_path: self.server_config.sse_path.clone(),
493 post_path: self.server_config.post_path.clone(),
494 ct: CancellationToken::new(),
495 sse_keep_alive: Some(std::time::Duration::from_secs(
496 self.server_config.keep_alive_secs,
497 )),
498 };
499
500 let (sse_server, router) = SseServer::new(config);
501
502 let ct = if self.server_config.stateful {
505 sse_server.with_service(move || sse_handler.clone())
506 } else {
507 sse_server.with_service_directly(move || sse_handler.clone())
508 };
509
510 Ok((router, ct))
511 }
512
513 async fn create_server_with_backend_session(
515 &self,
516 handler: BackendSessionHandler,
517 ) -> Result<(axum::Router, CancellationToken, BackendSessionHandler)> {
518 let config = SseServerConfig {
519 bind: "0.0.0.0:0".parse()?,
520 sse_path: self.server_config.sse_path.clone(),
521 post_path: self.server_config.post_path.clone(),
522 ct: CancellationToken::new(),
523 sse_keep_alive: Some(std::time::Duration::from_secs(
524 self.server_config.keep_alive_secs,
525 )),
526 };
527
528 let (sse_server, router) = SseServer::new(config);
529
530 let handler_for_return = handler.clone();
532 let ct = if self.server_config.stateful {
533 sse_server.with_service(move || handler.clone())
534 } else {
535 sse_server.with_service_directly(move || handler.clone())
536 };
537
538 Ok((router, ct, handler_for_return))
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn test_builder_creation() {
548 let builder = SseServerBuilder::new(BackendConfig::Stdio {
549 command: "echo".into(),
550 args: Some(vec!["hello".into()]),
551 env: None,
552 })
553 .mcp_id("test")
554 .sse_path("/custom/sse")
555 .post_path("/custom/message");
556
557 assert!(builder.server_config.mcp_id.is_some());
558 assert_eq!(builder.server_config.mcp_id.as_deref(), Some("test"));
559 assert_eq!(builder.server_config.sse_path, "/custom/sse");
560 assert_eq!(builder.server_config.post_path, "/custom/message");
561 }
562
563 #[test]
564 fn test_default_config() {
565 let config = SseServerBuilderConfig::default();
566 assert_eq!(config.sse_path, "/sse");
567 assert_eq!(config.post_path, "/message");
568 assert_eq!(config.keep_alive_secs, 15);
569 assert!(
570 config.stateful,
571 "default stateful should be true for backward compatibility"
572 );
573 }
574
575 #[test]
576 fn test_stateful_flag_default() {
577 let builder = SseServerBuilder::new(BackendConfig::Stdio {
578 command: "echo".into(),
579 args: None,
580 env: None,
581 });
582 assert!(
583 builder.server_config.stateful,
584 "stateful should default to true"
585 );
586 }
587
588 #[test]
589 fn test_stateful_flag_disabled() {
590 let builder = SseServerBuilder::new(BackendConfig::Stdio {
591 command: "echo".into(),
592 args: None,
593 env: None,
594 })
595 .stateful(false);
596 assert!(
597 !builder.server_config.stateful,
598 "stateful should be false when set"
599 );
600 }
601
602 #[test]
603 fn test_stateful_flag_enabled() {
604 let builder = SseServerBuilder::new(BackendConfig::Stdio {
605 command: "echo".into(),
606 args: None,
607 env: None,
608 })
609 .stateful(true);
610 assert!(
611 builder.server_config.stateful,
612 "stateful should be true when set"
613 );
614 }
615
616 #[test]
617 fn test_timing_constants() {
618 assert_eq!(STDIO_SLOW_THRESHOLD_SECS, 30);
619 assert_eq!(HTTP_SLOW_THRESHOLD_SECS, 10);
620 }
621
622 #[test]
623 fn test_log_connection_timing_format() {
624 use std::time::Duration;
625 log_connection_timing(
627 "test-mcp",
628 "TestBackend",
629 Duration::from_millis(1500),
630 &[
631 ("step1", Duration::from_millis(500)),
632 ("step2", Duration::from_millis(1000)),
633 ],
634 10,
635 "Test warning message",
636 );
637 }
639
640 #[test]
641 fn test_log_connection_timing_no_breakdown() {
642 use std::time::Duration;
643 log_connection_timing(
645 "test-mcp",
646 "TestBackend",
647 Duration::from_millis(500),
648 &[],
649 10,
650 "Test warning message",
651 );
652 }
653}