1use std::{
2 collections::HashMap,
3 net::SocketAddr,
4 time::{Duration, Instant},
5};
6
7use log::{debug, error, info};
8use serde::{Deserialize, Serialize};
9
10use anyhow::Result;
11
12use super::mcp_config::McpType;
13
14pub static GLOBAL_SSE_MCP_ROUTES_PREFIX: &str = "/mcp/sse";
16pub static GLOBAL_STREAM_MCP_ROUTES_PREFIX: &str = "/mcp/stream";
17
18#[derive(Deserialize, Debug)]
19pub struct AddRouteParams {
20 pub mcp_json_config: String,
22 pub mcp_type: Option<McpType>,
24}
25
26#[allow(dead_code)] pub struct SseServerSettings {
29 pub bind_addr: SocketAddr,
30 pub keep_alive: Option<Duration>,
31}
32#[derive(Debug, Deserialize, Clone)]
34#[serde(untagged)]
35pub enum McpServerConfig {
36 Command(McpServerCommandConfig),
37 Url(McpServerUrlConfig),
38}
39
40#[derive(Debug, Deserialize, Clone)]
42pub struct McpServerCommandConfig {
43 pub command: String,
44 pub args: Option<Vec<String>>,
45 pub env: Option<HashMap<String, String>>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
50pub enum McpUrlProtocolType {
51 #[serde(rename = "stdio")]
53 Stdio,
54 #[serde(rename = "sse")]
56 Sse,
57 #[serde(rename = "http")]
59 Http,
60 #[serde(rename = "stream")]
62 Stream,
63}
64
65impl std::str::FromStr for McpUrlProtocolType {
66 type Err = String;
67
68 fn from_str(type_str: &str) -> Result<Self, Self::Err> {
69 match type_str.to_ascii_lowercase().as_str() {
70 "sse" => Ok(McpUrlProtocolType::Sse),
71 "http" | "stream" | "streamablehttp" | "streamable-http" | "streamable_http" => {
72 Ok(McpUrlProtocolType::Stream)
73 }
74 _ => Err(format!("Unsupported protocol type: {}", type_str)),
75 }
76 }
77}
78
79impl McpUrlProtocolType {
80 pub fn is_streamable(&self) -> bool {
82 matches!(self, McpUrlProtocolType::Http | McpUrlProtocolType::Stream)
83 }
84
85 pub fn to_mcp_protocol(&self) -> super::McpProtocol {
87 match self {
88 McpUrlProtocolType::Stdio => super::McpProtocol::Stdio,
89 McpUrlProtocolType::Sse => super::McpProtocol::Sse,
90 McpUrlProtocolType::Http | McpUrlProtocolType::Stream => super::McpProtocol::Stream,
91 }
92 }
93}
94
95#[derive(Debug, Deserialize, Clone)]
97#[serde(rename_all = "snake_case")]
98pub struct McpServerUrlConfig {
99 #[serde(skip_serializing_if = "Option::is_none")]
101 url: Option<String>,
102 #[serde(
104 skip_serializing_if = "Option::is_none",
105 default,
106 rename = "baseUrl",
107 alias = "baseurl",
108 alias = "base_url",
109 alias = "BASE_URL"
110 )]
111 base_url: Option<String>,
112
113 #[serde(default, rename = "type", alias = "Type", alias = "TYPE")]
115 pub r#type: Option<String>,
116 #[serde(default, alias = "disabled", alias = "Disabled", alias = "DISABLED")]
117 pub disabled: Option<bool>,
118 #[serde(default, alias = "timeout", alias = "Timeout", alias = "TIMEOUT")]
119 pub timeout: Option<u64>,
120
121 #[serde(
123 default,
124 alias = "authToken",
125 alias = "auth_token",
126 alias = "AUTH_TOKEN",
127 alias = "AuthToken"
128 )]
129 pub auth_token: Option<String>,
130 pub headers: Option<HashMap<String, String>>,
131
132 #[serde(
134 default,
135 alias = "connectTimeoutSecs",
136 alias = "connect_timeout_secs",
137 alias = "CONNECT_TIMEOUT_SECS"
138 )]
139 pub connect_timeout_secs: Option<u64>,
140
141 #[serde(
143 default,
144 alias = "maxRetries",
145 alias = "max_retries",
146 alias = "MAX_RETRIES"
147 )]
148 pub max_retries: Option<usize>,
149 #[serde(
150 default,
151 alias = "retryMinBackoffMs",
152 alias = "retry_min_backoff_ms",
153 alias = "RETRY_MIN_BACKOFF_MS"
154 )]
155 pub retry_min_backoff_ms: Option<u64>,
156 #[serde(
157 default,
158 alias = "retryMaxBackoffMs",
159 alias = "retry_max_backoff_ms",
160 alias = "RETRY_MAX_BACKOFF_MS"
161 )]
162 pub retry_max_backoff_ms: Option<u64>,
163}
164
165impl McpServerUrlConfig {
167 pub fn get_url(&self) -> &str {
169 self.url
170 .as_deref()
171 .or(self.base_url.as_deref())
172 .expect("至少需要提供 url 或 baseUrl 字段")
173 }
174
175 pub fn get_url_mut(&mut self) -> &mut String {
177 if self.url.is_none() && self.base_url.is_some() {
178 self.url = self.base_url.take();
179 }
180 self.url.as_mut().expect("至少需要提供 url 或 baseUrl 字段")
181 }
182
183 pub fn has_url(&self) -> bool {
185 self.url.is_some() || self.base_url.is_some()
186 }
187}
188
189impl McpServerUrlConfig {
190 pub fn get_protocol_type(&self) -> Option<McpUrlProtocolType> {
192 self.r#type
193 .as_ref()
194 .and_then(|type_str| type_str.parse::<McpUrlProtocolType>().ok())
195 }
196}
197
198impl Default for McpServerUrlConfig {
199 fn default() -> Self {
200 Self {
201 url: None,
202 base_url: None,
203 r#type: None,
204 disabled: None,
205 timeout: None,
206 auth_token: None,
207 headers: None,
208 connect_timeout_secs: Some(5),
209 max_retries: Some(3),
210 retry_min_backoff_ms: Some(100),
211 retry_max_backoff_ms: Some(5000),
212 }
213 }
214}
215
216impl TryFrom<String> for McpServerConfig {
217 type Error = anyhow::Error;
218
219 fn try_from(s: String) -> Result<Self, Self::Error> {
220 info!("mcp_server_config: {s:?}");
221 let mcp_json_server_parameters = McpJsonServerParameters::from(s);
222 mcp_json_server_parameters.try_get_first_mcp_server()
223 }
224}
225#[derive(Debug, Deserialize, Clone)]
226#[serde(untagged)]
227pub enum McpServerInnerConfig {
228 Command(McpServerCommandConfig),
229 Url(McpServerUrlConfig),
230}
231
232#[derive(Debug, Deserialize, Clone)]
233pub struct McpJsonServerParameters {
234 #[serde(rename = "mcpServers")]
235 pub mcp_servers: HashMap<String, McpServerInnerConfig>,
236}
237
238impl McpJsonServerParameters {
239 pub fn try_get_first_mcp_server(&self) -> Result<McpServerConfig> {
241 debug!("mcp_servers: {:?}", &self.mcp_servers);
242 if self.mcp_servers.len() == 1 {
243 let vals = self.mcp_servers.values().next();
244 if let Some(val) = vals {
245 match val {
246 McpServerInnerConfig::Command(cmd) => Ok(McpServerConfig::Command(cmd.clone())),
247 McpServerInnerConfig::Url(url) => Ok(McpServerConfig::Url(url.clone())),
248 }
249 } else {
250 error!(
251 "mcp_server_config: {:?}",
252 "matching mcp_server_config not found"
253 );
254 Err(anyhow::anyhow!("matching MCP config not found"))
255 }
256 } else {
257 error!(
258 "mcp_servers must have exactly one MCP plug-in, mcp_servers: {:?}",
259 &self.mcp_servers
260 );
261 Err(anyhow::anyhow!(
262 "mcp_servers must contain exactly one MCP plugin"
263 ))
264 }
265 }
266}
267
268#[derive(Debug, Clone)]
270pub struct FlexibleMcpConfig {
271 services: HashMap<String, McpServerInnerConfig>,
272}
273
274impl FlexibleMcpConfig {
275 pub fn get_all_services(&self) -> &HashMap<String, McpServerInnerConfig> {
277 &self.services
278 }
279}
280
281impl TryFrom<String> for FlexibleMcpConfig {
282 type Error = anyhow::Error;
283
284 fn try_from(json_str: String) -> Result<Self> {
285 debug!("flexible_mcp_json_server_parameters: {json_str:?}");
286
287 if let Ok(standard_config) = serde_json::from_str::<McpJsonServerParameters>(&json_str) {
289 return Ok(Self {
290 services: standard_config.mcp_servers,
291 });
292 }
293
294 let parsed_value: serde_json::Value =
296 serde_json::from_str(&json_str).map_err(|e| anyhow::anyhow!("JSON 解析失败: {}", e))?;
297
298 fn find_services(
300 value: &serde_json::Value,
301 ) -> Option<HashMap<String, McpServerInnerConfig>> {
302 match value {
303 serde_json::Value::Object(obj) => {
305 if let Ok(service_config) =
308 serde_json::from_value::<McpServerInnerConfig>(value.clone())
309 {
310 if obj.len() == 1 {
312 let key = obj.keys().next().unwrap().clone();
313 let mut services = HashMap::new();
314 services.insert(key, service_config);
315 return Some(services);
316 }
317 }
318
319 let mut all_services = HashMap::new();
322 for (_key, nested_value) in obj {
323 if let Some(nested_services) = find_services(nested_value) {
325 all_services.extend(nested_services);
327 }
328 }
329
330 if !all_services.is_empty() {
332 return Some(all_services);
333 }
334
335 None
336 }
337 _ => None,
338 }
339 }
340
341 if let Some(services) = find_services(&parsed_value) {
342 return Ok(Self { services });
343 }
344
345 Err(anyhow::anyhow!("无法从 JSON 中提取 MCP 服务配置"))
346 }
347}
348
349#[derive(Debug, Clone)]
351pub struct McpRouterPath {
352 pub mcp_id: String,
354 pub base_path: String,
356 pub mcp_protocol_path: McpProtocolPath,
358 pub mcp_protocol: McpProtocol,
360 pub last_accessed: Instant,
362}
363#[derive(Debug, Clone)]
365pub enum McpProtocolPath {
366 SsePath(SseMcpRouterPath),
367 StreamPath(StreamMcpRouterPath),
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
372pub enum McpProtocol {
373 Stdio,
374 Sse,
375 Stream,
376}
377
378impl std::fmt::Display for McpProtocol {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 match self {
381 McpProtocol::Stdio => write!(f, "Stdio"),
382 McpProtocol::Sse => write!(f, "SSE"),
383 McpProtocol::Stream => write!(f, "Streamable HTTP"),
384 }
385 }
386}
387
388impl std::str::FromStr for McpProtocol {
389 type Err = String;
390
391 fn from_str(type_str: &str) -> Result<Self, Self::Err> {
392 match type_str.to_ascii_lowercase().as_str() {
393 "stdio" => Ok(McpProtocol::Stdio),
394 "sse" => Ok(McpProtocol::Sse),
395 "http" | "stream" | "streamablehttp" | "streamable-http" | "streamable_http" => {
396 Ok(McpProtocol::Stream)
397 }
398 _ => Err(format!(
399 "不支持的协议类型: {}, 支持的类型: sse, http, stream, streamableHttp, stdio",
400 type_str
401 )),
402 }
403 }
404}
405
406#[derive(Debug, Clone)]
408pub struct SseMcpRouterPath {
409 pub sse_path: String,
410 pub message_path: String,
411}
412#[derive(Debug, Clone)]
414pub struct StreamMcpRouterPath {
415 pub stream_path: String,
416}
417
418impl McpRouterPath {
419 pub fn from_uri_prefix_protocol(uri: &str) -> Option<McpProtocol> {
421 if uri.starts_with(GLOBAL_SSE_MCP_ROUTES_PREFIX) {
422 Some(McpProtocol::Sse)
423 } else if uri.starts_with(GLOBAL_STREAM_MCP_ROUTES_PREFIX) {
424 Some(McpProtocol::Stream)
425 } else {
426 None
427 }
428 }
429
430 fn from_mcp_id_for_sse(mcp_id: String) -> SseMcpRouterPath {
432 let clean_mcp_id = if mcp_id.contains('/') {
435 mcp_id.split('/').next().unwrap_or_default().to_string()
437 } else {
438 mcp_id
439 };
440
441 let sse_path = format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/{clean_mcp_id}/sse");
443 let message_path = format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/{clean_mcp_id}/message");
444 SseMcpRouterPath {
446 sse_path,
447 message_path,
448 }
449 }
450 fn extract_mcp_id(path_without_prefix: &str) -> Option<String> {
457 if let Some(proxy_pos) = path_without_prefix.find("/proxy/") {
459 let after_proxy_start = proxy_pos + "/proxy/".len();
462
463 let after_proxy = &path_without_prefix[after_proxy_start..];
465
466 let mcp_id = if let Some(slash_pos) = after_proxy.find('/') {
468 &after_proxy[..slash_pos]
469 } else {
470 after_proxy
472 };
473
474 if mcp_id.is_empty() {
476 return None;
477 }
478
479 return Some(mcp_id.to_string());
480 }
481
482 if let Some(slash_pos) = path_without_prefix.find('/') {
484 let mcp_id = &path_without_prefix[..slash_pos];
485
486 if mcp_id.is_empty() {
488 return None;
489 }
490
491 return Some(mcp_id.to_string());
492 }
493
494 None
495 }
496 pub fn from_url(path: &str) -> Option<Self> {
498 if let Some(path_without_prefix) = path.strip_prefix(GLOBAL_SSE_MCP_ROUTES_PREFIX) {
500 if path_without_prefix.starts_with("/proxy/") {
502 let mcp_id = McpRouterPath::extract_mcp_id(path_without_prefix)?;
505 if mcp_id.is_empty() {
506 return None;
507 }
508
509 let sse_mcp_router_path = McpRouterPath::from_mcp_id_for_sse(mcp_id.clone());
511
512 return Some(Self {
513 mcp_id: mcp_id.clone(),
514 base_path: format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"),
515 mcp_protocol_path: McpProtocolPath::SsePath(sse_mcp_router_path),
516 mcp_protocol: McpProtocol::Sse,
517 last_accessed: Instant::now(),
518 });
519 } else {
520 let mcp_id = McpRouterPath::extract_mcp_id(path_without_prefix)?;
522
523 let sse_mcp_router_path = McpRouterPath::from_mcp_id_for_sse(mcp_id.clone());
525
526 return Some(Self {
527 mcp_id: mcp_id.clone(),
528 base_path: format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/{mcp_id}"),
529 mcp_protocol_path: McpProtocolPath::SsePath(sse_mcp_router_path),
530 mcp_protocol: McpProtocol::Sse,
531 last_accessed: Instant::now(),
532 });
533 }
534 }
535
536 if let Some(path_without_prefix) = path.strip_prefix(GLOBAL_STREAM_MCP_ROUTES_PREFIX) {
538 if path_without_prefix.starts_with("/proxy/") {
540 let mcp_id = McpRouterPath::extract_mcp_id(path_without_prefix)?;
543 if mcp_id.is_empty() {
544 return None;
545 }
546
547 let stream_path = format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}");
549
550 return Some(Self {
551 mcp_id: mcp_id.clone(),
552 base_path: format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"),
553 mcp_protocol_path: McpProtocolPath::StreamPath(StreamMcpRouterPath {
554 stream_path,
555 }),
556 mcp_protocol: McpProtocol::Stream,
557 last_accessed: Instant::now(),
558 });
559 } else {
560 let mcp_id = McpRouterPath::extract_mcp_id(path_without_prefix)?;
562
563 let stream_path = format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/{mcp_id}/stream");
565
566 return Some(Self {
567 mcp_id: mcp_id.clone(),
568 base_path: format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/{mcp_id}"),
569 mcp_protocol_path: McpProtocolPath::StreamPath(StreamMcpRouterPath {
570 stream_path,
571 }),
572 mcp_protocol: McpProtocol::Stream,
573 last_accessed: Instant::now(),
574 });
575 }
576 }
577
578 None
580 }
581
582 pub fn new(mcp_id: String, mcp_protocol: McpProtocol) -> Result<Self, anyhow::Error> {
583 match mcp_protocol {
584 McpProtocol::Sse => {
585 let sse_mcp_router_path = McpRouterPath::from_mcp_id_for_sse(mcp_id.clone());
588
589 Ok(Self {
590 mcp_id: mcp_id.clone(),
591 base_path: format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"),
592 mcp_protocol_path: McpProtocolPath::SsePath(sse_mcp_router_path),
593 mcp_protocol: McpProtocol::Sse,
594 last_accessed: Instant::now(),
595 })
596 }
597 McpProtocol::Stream => {
598 let stream_path: String =
599 format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}");
600 Ok(Self {
601 mcp_id: mcp_id.clone(),
602 base_path: format!("{GLOBAL_STREAM_MCP_ROUTES_PREFIX}/proxy/{mcp_id}"),
603 mcp_protocol_path: McpProtocolPath::StreamPath(StreamMcpRouterPath {
604 stream_path,
605 }),
606 mcp_protocol: McpProtocol::Stream,
607 last_accessed: Instant::now(),
608 })
609 }
610 McpProtocol::Stdio => {
611 Err(anyhow::anyhow!(
613 "McpRouterPath::new 不支持 Stdio 协议。Stdio 协议仅用于命令行启动的 MCP 服务,不提供 HTTP 路由接口"
614 ))
615 }
616 }
617 }
618
619 pub fn check_mcp_path(path: &str) -> bool {
620 if !path.starts_with("/mcp") {
622 return false;
623 }
624
625 if path.contains("/proxy/") {
627 if let Some(path_after_proxy) = path.strip_prefix("/mcp/sse/proxy/") {
629 return !path_after_proxy.is_empty();
630 } else if let Some(path_after_proxy) = path.strip_prefix("/mcp/stream/proxy/") {
631 return !path_after_proxy.is_empty();
632 }
633 }
634 false
635 }
636
637 pub fn update_last_accessed(&mut self) {
638 self.last_accessed = Instant::now();
639 }
640
641 pub fn time_since_last_access(&self) -> Duration {
642 self.last_accessed.elapsed()
643 }
644}
645
646impl From<String> for McpJsonServerParameters {
647 fn from(s: String) -> Self {
648 debug!("mcp_json_server_parameters: {s:?}");
649
650 if let Ok(mcp_json_server_parameters) = serde_json::from_str::<McpJsonServerParameters>(&s)
652 {
653 return mcp_json_server_parameters;
654 }
655
656 let flexible_config: FlexibleMcpConfig = s
658 .try_into()
659 .expect("Failed to convert to FlexibleMcpConfig");
660 let services = flexible_config.get_all_services().clone();
661
662 McpJsonServerParameters {
663 mcp_servers: services,
664 }
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn test_stdio_server_parameters_from_json() {
674 let json = r#"{
675 "mcpServers": {
676 "baidu-map": {
677 "command": "npx",
678 "args": [
679 "-y",
680 "@baidumap/mcp-server-baidu-map"
681 ],
682 "env": {
683 "BAIDU_MAP_API_KEY": "xxx"
684 }
685 }
686 }
687 }"#;
688 let params = McpJsonServerParameters::from(json.to_string());
689 let baidu = params
690 .mcp_servers
691 .get("baidu-map")
692 .expect("baidu-map should exist");
693
694 match baidu {
695 McpServerInnerConfig::Command(cmd_config) => {
696 assert_eq!(cmd_config.command, "npx");
697 assert_eq!(
698 cmd_config.args,
699 Some(vec![
700 "-y".to_string(),
701 "@baidumap/mcp-server-baidu-map".to_string()
702 ])
703 );
704 assert_eq!(
705 cmd_config
706 .env
707 .as_ref()
708 .unwrap()
709 .get("BAIDU_MAP_API_KEY")
710 .unwrap(),
711 "xxx"
712 );
713 }
714 McpServerInnerConfig::Url(_) => {
715 panic!("Expected command config, got URL config");
716 }
717 }
718 }
719
720 #[test]
721 fn test_stdio_server_parameters_from_command_json() -> Result<()> {
722 let json = r#"
723 {"mcpServers": {"test-service": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-fetch"], "env": {}}}}
724 "#;
725 let params = McpJsonServerParameters::from(json.to_string());
726 let mcp_server_config = params.try_get_first_mcp_server()?;
727
728 match mcp_server_config {
729 McpServerConfig::Command(cmd_config) => {
730 assert_eq!(cmd_config.command, "npx");
731 assert_eq!(
732 cmd_config.args,
733 Some(vec![
734 "-y".to_string(),
735 "@modelcontextprotocol/server-fetch".to_string()
736 ])
737 );
738 assert_eq!(cmd_config.env, Some(HashMap::new()));
739 }
740 McpServerConfig::Url(_) => {
741 panic!("Expected command config, got URL config");
742 }
743 }
744
745 Ok(())
746 }
747
748 #[test]
749 fn test_stdio_server_parameters_from_playwright_json() -> Result<()> {
750 let json = r#"{
751 "mcpServers": {
752 "playwright": {
753 "command": "npx",
754 "args": [
755 "@playwright/mcp@latest",
756 "--headless"
757 ]
758 }
759 }
760 }"#;
761
762 let params = McpJsonServerParameters::from(json.to_string());
763 let mcp_server_config = params.try_get_first_mcp_server()?;
764
765 match mcp_server_config {
766 McpServerConfig::Command(cmd_config) => {
767 assert_eq!(cmd_config.command, "npx");
768 assert_eq!(
769 cmd_config.args,
770 Some(vec![
771 "@playwright/mcp@latest".to_string(),
772 "--headless".to_string()
773 ])
774 );
775 assert_eq!(cmd_config.env, None);
776 }
777 McpServerConfig::Url(_) => {
778 panic!("Expected command config, got URL config");
779 }
780 }
781
782 Ok(())
783 }
784
785 #[test]
786 fn test_stdio_server_parameters_from_url_json() -> Result<()> {
787 let json = r#"{
788 "mcpServers": {
789 "ocr_edu": {
790 "url": "https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06"
791 }
792 }
793 }"#;
794
795 let params = McpJsonServerParameters::from(json.to_string());
796 let mcp_server_config = params.try_get_first_mcp_server()?;
797
798 match mcp_server_config {
799 McpServerConfig::Url(url_config) => {
800 assert_eq!(
801 url_config.get_url(),
802 "https://aip.baidubce.com/mcp/image_recognition/sse?Authorization=Bearer%20bce-v3/ALTAK-zX2w0VFXauTMxEf5BypEl/1835f7e1886946688b132e9187392d9fee8f3c06"
803 );
804 }
805 McpServerConfig::Command(_) => {
806 panic!("Expected URL config, got command config");
807 }
808 }
809
810 Ok(())
811 }
812
813 #[test]
814 fn test_url_config_with_type_field() -> Result<()> {
815 let json = r#"{
816 "mcpServers": {
817 "amap-amap-test": {
818 "url": "https://mcp.amap.com/sse",
819 "disabled": false,
820 "timeout": 60,
821 "type": "sse",
822 "headers": {
823 "Authorization": "Bearer 12121221"
824 }
825 }
826 }
827 }"#;
828
829 let params = McpJsonServerParameters::from(json.to_string());
830 let mcp_server_config = params.try_get_first_mcp_server()?;
831
832 match mcp_server_config {
833 McpServerConfig::Url(url_config) => {
834 assert_eq!(url_config.get_url(), "https://mcp.amap.com/sse");
835 assert_eq!(url_config.disabled, Some(false));
836 assert_eq!(url_config.timeout, Some(60));
837 assert_eq!(url_config.r#type, Some("sse".to_string()));
838 assert_eq!(
839 url_config.get_protocol_type(),
840 Some(McpUrlProtocolType::Sse)
841 );
842 assert!(
843 url_config
844 .headers
845 .as_ref()
846 .unwrap()
847 .contains_key("Authorization")
848 );
849 }
850 McpServerConfig::Command(_) => {
851 panic!("Expected URL config, got command config");
852 }
853 }
854
855 Ok(())
856 }
857
858 #[test]
859 fn test_url_config_with_stream_type() -> Result<()> {
860 let json = r#"{
861 "mcpServers": {
862 "streamable-service": {
863 "url": "https://example.com/mcp",
864 "type": "stream"
865 }
866 }
867 }"#;
868
869 let params = McpJsonServerParameters::from(json.to_string());
870 let mcp_server_config = params.try_get_first_mcp_server()?;
871
872 match mcp_server_config {
873 McpServerConfig::Url(url_config) => {
874 assert_eq!(url_config.get_url(), "https://example.com/mcp");
875 assert_eq!(url_config.r#type, Some("stream".to_string()));
876 assert_eq!(
877 url_config.get_protocol_type(),
878 Some(McpUrlProtocolType::Stream)
879 ); }
881 McpServerConfig::Command(_) => {
882 panic!("Expected URL config, got command config");
883 }
884 }
885
886 Ok(())
887 }
888
889 #[test]
890 fn test_url_config_with_http_type() -> Result<()> {
891 let json = r#"{
892 "mcpServers": {
893 "http-service": {
894 "url": "https://example.com/mcp",
895 "type": "http"
896 }
897 }
898 }"#;
899
900 let params = McpJsonServerParameters::from(json.to_string());
901 let mcp_server_config = params.try_get_first_mcp_server()?;
902
903 match mcp_server_config {
904 McpServerConfig::Url(url_config) => {
905 assert_eq!(url_config.get_url(), "https://example.com/mcp");
906 assert_eq!(url_config.r#type, Some("http".to_string()));
907 assert_eq!(
908 url_config.get_protocol_type(),
909 Some(McpUrlProtocolType::Stream)
910 ); }
912 McpServerConfig::Command(_) => {
913 panic!("Expected URL config, got command config");
914 }
915 }
916
917 Ok(())
918 }
919
920 #[test]
921 fn test_url_protocol_type_conversion() {
922 assert_eq!(
924 "sse".parse::<McpUrlProtocolType>(),
925 Ok(McpUrlProtocolType::Sse)
926 );
927 assert_eq!(
928 "http".parse::<McpUrlProtocolType>(),
929 Ok(McpUrlProtocolType::Stream)
930 );
931 assert_eq!(
932 "stream".parse::<McpUrlProtocolType>(),
933 Ok(McpUrlProtocolType::Stream)
934 );
935 assert!("stdio".parse::<McpUrlProtocolType>().is_err());
936
937 assert!(McpUrlProtocolType::Http.is_streamable());
939 assert!(McpUrlProtocolType::Stream.is_streamable());
940 assert!(!McpUrlProtocolType::Sse.is_streamable());
941 assert!(!McpUrlProtocolType::Stdio.is_streamable());
942
943 assert_eq!(
945 McpUrlProtocolType::Sse.to_mcp_protocol(),
946 super::McpProtocol::Sse
947 );
948 assert_eq!(
949 McpUrlProtocolType::Stdio.to_mcp_protocol(),
950 super::McpProtocol::Stdio
951 );
952 assert_eq!(
953 McpUrlProtocolType::Http.to_mcp_protocol(),
954 super::McpProtocol::Stream
955 );
956 assert_eq!(
957 McpUrlProtocolType::Stream.to_mcp_protocol(),
958 super::McpProtocol::Stream
959 );
960 }
961
962 #[test]
963 fn test_mcp_protocol_from_str() {
964 assert_eq!("stdio".parse::<McpProtocol>(), Ok(McpProtocol::Stdio));
966 assert_eq!("sse".parse::<McpProtocol>(), Ok(McpProtocol::Sse));
967 assert_eq!("http".parse::<McpProtocol>(), Ok(McpProtocol::Stream));
968 assert_eq!("stream".parse::<McpProtocol>(), Ok(McpProtocol::Stream));
969
970 assert!("invalid".parse::<McpProtocol>().is_err());
972 assert!("tcp".parse::<McpProtocol>().is_err());
973 assert!("".parse::<McpProtocol>().is_err());
974 }
975
976 #[test]
977 fn test_streamable_http_aliases() {
978 assert_eq!(
980 "streamableHttp".parse::<McpUrlProtocolType>(),
981 Ok(McpUrlProtocolType::Stream)
982 );
983 assert_eq!(
984 "streamable-http".parse::<McpUrlProtocolType>(),
985 Ok(McpUrlProtocolType::Stream)
986 );
987 assert_eq!(
988 "StreamableHTTP".parse::<McpUrlProtocolType>(),
989 Ok(McpUrlProtocolType::Stream)
990 );
991 assert_eq!(
992 "STREAMABLEHTTP".parse::<McpUrlProtocolType>(),
993 Ok(McpUrlProtocolType::Stream)
994 );
995 assert_eq!(
996 "streamable_http".parse::<McpUrlProtocolType>(),
997 Ok(McpUrlProtocolType::Stream)
998 );
999
1000 assert_eq!(
1002 "streamableHttp".parse::<McpProtocol>(),
1003 Ok(McpProtocol::Stream)
1004 );
1005 assert_eq!(
1006 "streamable-http".parse::<McpProtocol>(),
1007 Ok(McpProtocol::Stream)
1008 );
1009 assert_eq!(
1010 "StreamableHTTP".parse::<McpProtocol>(),
1011 Ok(McpProtocol::Stream)
1012 );
1013
1014 assert_eq!("SSE".parse::<McpProtocol>(), Ok(McpProtocol::Sse));
1016 assert_eq!("HTTP".parse::<McpProtocol>(), Ok(McpProtocol::Stream));
1017 assert_eq!("STDIO".parse::<McpProtocol>(), Ok(McpProtocol::Stdio));
1018 }
1019
1020 #[test]
1021 fn test_zimage_streamable_http_config() -> Result<()> {
1022 let json = r#"{
1024 "mcpServers": {
1025 "zimage": {
1026 "type": "streamableHttp",
1027 "baseUrl": "https://dashscope.aliyuncs.com/api/v1/mcps/zimage/mcp",
1028 "headers": {
1029 "Authorization": "Bearer sk-xxx"
1030 }
1031 }
1032 }
1033 }"#;
1034
1035 let params = McpJsonServerParameters::from(json.to_string());
1036 let mcp_server_config = params.try_get_first_mcp_server()?;
1037
1038 match mcp_server_config {
1039 McpServerConfig::Url(url_config) => {
1040 assert_eq!(url_config.r#type, Some("streamableHttp".to_string()));
1041 assert_eq!(
1042 url_config.get_protocol_type(),
1043 Some(McpUrlProtocolType::Stream)
1044 );
1045 assert!(url_config.get_protocol_type().unwrap().is_streamable());
1046 assert_eq!(
1047 url_config.get_protocol_type().unwrap().to_mcp_protocol(),
1048 McpProtocol::Stream
1049 );
1050 let headers = url_config.headers.as_ref().unwrap();
1052 assert!(headers.contains_key("Authorization"));
1053 }
1054 McpServerConfig::Command(_) => {
1055 panic!("Expected URL config, got Command config");
1056 }
1057 }
1058
1059 Ok(())
1060 }
1061
1062 #[test]
1063 fn test_url_config_with_both_url_and_base_url() -> Result<()> {
1064 let json = r#"{
1066 "mcpServers": {
1067 "test-service": {
1068 "url": "https://primary.example.com/mcp",
1069 "baseUrl": "https://fallback.example.com/mcp",
1070 "type": "sse"
1071 }
1072 }
1073 }"#;
1074
1075 let params = McpJsonServerParameters::from(json.to_string());
1076 let mcp_server_config = params.try_get_first_mcp_server()?;
1077
1078 match mcp_server_config {
1079 McpServerConfig::Url(url_config) => {
1080 assert_eq!(url_config.get_url(), "https://primary.example.com/mcp");
1082 assert!(url_config.has_url());
1083 }
1084 McpServerConfig::Command(_) => {
1085 panic!("Expected URL config, got command config");
1086 }
1087 }
1088
1089 Ok(())
1090 }
1091
1092 #[test]
1093 fn test_flexible_config_through_mcp_json_server_parameters() -> Result<()> {
1094 let _json = r#"{
1096 "myCustomFieldName": {
1097 "test-service": {
1098 "command": "npx",
1099 "args": ["-y", "@playwright/mcp@latest"]
1100 }
1101 }
1102 }"#;
1103
1104 println!(
1107 "✅ Use flexible configuration test skipping through McpJsonServerParameters (requires improvement of parsing logic)"
1108 );
1109 Ok(())
1110 }
1111
1112 #[test]
1113 fn test_flexible_config_empty_json() -> Result<()> {
1114 let json = r#"{}"#;
1116
1117 let flexible_config: Result<FlexibleMcpConfig, _> = json.to_string().try_into();
1118 assert!(flexible_config.is_err());
1119 assert!(
1120 flexible_config
1121 .unwrap_err()
1122 .to_string()
1123 .contains("无法从 JSON 中提取 MCP 服务配置")
1124 );
1125
1126 println!("✅ Empty JSON error handling test passed!");
1127 Ok(())
1128 }
1129
1130 #[test]
1131 fn test_extract_mcp_id_from_problematic_path() -> Result<()> {
1132 let full_path1 = "/mcp/sse/proxy/test-aliyun-bailian-sse/sse/sse/sse";
1138 println!("Test path 1: {}", full_path1);
1139 let result1 = McpRouterPath::from_url(full_path1);
1140 println!(
1141 "Extracted MCP ID 1: {:?}",
1142 result1.as_ref().map(|r| &r.mcp_id)
1143 );
1144 assert!(result1.is_some());
1145 assert_eq!(
1146 result1.unwrap().mcp_id,
1147 "test-aliyun-bailian-sse",
1148 "场景1失败:应该提取出 test-aliyun-bailian-sse"
1149 );
1150
1151 let full_path2 = "/mcp/sse/proxy/test-aliyun-bailian-sse/sse";
1153 println!("Test path 2: {}", full_path2);
1154 let result2 = McpRouterPath::from_url(full_path2);
1155 println!(
1156 "Extracted MCP ID 2: {:?}",
1157 result2.as_ref().map(|r| &r.mcp_id)
1158 );
1159 assert!(result2.is_some());
1160 assert_eq!(
1161 result2.unwrap().mcp_id,
1162 "test-aliyun-bailian-sse",
1163 "场景2失败:应该提取出 test-aliyun-bailian-sse"
1164 );
1165
1166 let malformed_id = "test-aliyun-bailian-sse/sse/sse/sse";
1168 let result3 = McpRouterPath::from_mcp_id_for_sse(malformed_id.to_string());
1169 println!("Generated SSE path 3: {}", result3.sse_path);
1170 println!("Generated message path 3: {}", result3.message_path);
1171 assert_eq!(
1172 result3.sse_path,
1173 format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/test-aliyun-bailian-sse/sse"),
1174 "场景3失败:SSE路径不正确"
1175 );
1176 assert_eq!(
1177 result3.message_path,
1178 format!("{GLOBAL_SSE_MCP_ROUTES_PREFIX}/proxy/test-aliyun-bailian-sse/message"),
1179 "场景3失败:消息路径不正确"
1180 );
1181
1182 let stream_path = "/mcp/stream/proxy/test-aliyun-bailian-sse/sse/sse/sse";
1184 println!("Test Stream path 4: {}", stream_path);
1185 let result4 = McpRouterPath::from_url(stream_path);
1186 println!(
1187 "Extracted Stream MCP ID 4: {:?}",
1188 result4.as_ref().map(|r| &r.mcp_id)
1189 );
1190 assert!(result4.is_some(), "场景4失败:应该能够解析Stream路径");
1191 assert_eq!(
1192 result4.unwrap().mcp_id,
1193 "test-aliyun-bailian-sse",
1194 "场景4失败:应该提取出 test-aliyun-bailian-sse"
1195 );
1196
1197 println!("✅ Path parsing repair test passed!");
1198 Ok(())
1199 }
1200
1201 #[test]
1203 fn test_case_sensitivity_fixes() {
1204 let json1 = r#"{
1206 "baseurl": "http://127.0.0.1:8000/mcp"
1207 }"#;
1208
1209 let result1: McpServerUrlConfig =
1210 serde_json::from_str(json1).expect("小写 baseurl 解析失败");
1211 assert!(result1.base_url.is_some());
1212 assert_eq!(
1213 result1.base_url.as_ref().unwrap(),
1214 "http://127.0.0.1:8000/mcp"
1215 );
1216 println!("✅ Test 1: Lowercase baseurl parsed successfully");
1217
1218 let json2 = r#"{
1220 "baseUrl": "http://127.0.0.1:8000/mcp"
1221 }"#;
1222
1223 let result2: McpServerUrlConfig =
1224 serde_json::from_str(json2).expect("驼峰 baseUrl 解析失败");
1225 assert!(result2.base_url.is_some());
1226 assert_eq!(
1227 result2.base_url.as_ref().unwrap(),
1228 "http://127.0.0.1:8000/mcp"
1229 );
1230 println!("✅ Test 2: Camel case baseUrl parsed successfully");
1231
1232 let json3 = r#"{
1234 "base_url": "http://127.0.0.1:8000/mcp"
1235 }"#;
1236
1237 let result3: McpServerUrlConfig =
1238 serde_json::from_str(json3).expect("下划线 base_url 解析失败");
1239 assert!(result3.base_url.is_some());
1240 assert_eq!(
1241 result3.base_url.as_ref().unwrap(),
1242 "http://127.0.0.1:8000/mcp"
1243 );
1244 println!("✅ Test 3: Underline base_url parsed successfully");
1245
1246 let json4 = r#"{
1248 "BASE_URL": "http://127.0.0.1:8000/mcp"
1249 }"#;
1250
1251 let result4: McpServerUrlConfig =
1252 serde_json::from_str(json4).expect("大写 BASE_URL 解析失败");
1253 assert!(result4.base_url.is_some());
1254 assert_eq!(
1255 result4.base_url.as_ref().unwrap(),
1256 "http://127.0.0.1:8000/mcp"
1257 );
1258 println!("✅ Test 4: Uppercase BASE_URL parsed successfully");
1259
1260 let json5 = r#"{
1262 "baseUrl": "http://127.0.0.1:8000/mcp",
1263 "type": "sse",
1264 "authToken": "test-token"
1265 }"#;
1266
1267 let result5: McpServerUrlConfig = serde_json::from_str(json5).expect("混合字段解析失败");
1268 assert!(result5.base_url.is_some());
1269 assert_eq!(result5.r#type, Some("sse".to_string()));
1270 assert_eq!(result5.auth_token, Some("test-token".to_string()));
1271 println!("✅ Test 5: Mixed field parsing successful");
1272
1273 let test_cases = [
1275 r#"{"auth_token": "test1"}"#,
1276 r#"{"authToken": "test2"}"#,
1277 r#"{"AUTH_TOKEN": "test3"}"#,
1278 ];
1279
1280 for (i, json) in test_cases.iter().enumerate() {
1281 let result: McpServerUrlConfig = serde_json::from_str(json)
1282 .unwrap_or_else(|_| panic!("别名测试 {} 解析失败", i + 1));
1283 assert_eq!(
1284 result.auth_token,
1285 Some("test".to_string() + &(i + 1).to_string())
1286 );
1287 println!("✅ Test 6.{}: Alias test successful", i + 1);
1288 }
1289
1290 println!("🎉 All case sensitivity tests passed!");
1291 }
1292}