1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6use fastmcp_console::config::{BannerStyle, ConsoleConfig, TrafficVerbosity};
7use fastmcp_console::stats::ServerStats;
8use fastmcp_protocol::{
9 LoggingCapability, PromptsCapability, ResourceTemplate, ResourcesCapability,
10 ServerCapabilities, ServerInfo, TasksCapability, ToolsCapability,
11};
12use log::{Level, LevelFilter};
13
14use crate::proxy::{ProxyPromptHandler, ProxyResourceHandler, ProxyToolHandler};
15use crate::tasks::SharedTaskManager;
16use crate::{
17 AuthProvider, DuplicateBehavior, HttpServerConfig, LifespanHooks, LoggingConfig, PromptHandler,
18 ProxyCatalog, ProxyClient, ResourceHandler, Router, Server, ToolHandler,
19};
20
21const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
23
24pub struct ServerBuilder {
26 info: ServerInfo,
27 capabilities: ServerCapabilities,
28 router: Router,
29 instructions: Option<String>,
30 request_timeout_secs: u64,
32 stats_enabled: bool,
34 mask_error_details: bool,
36 logging: LoggingConfig,
38 console_config: ConsoleConfig,
40 lifespan: LifespanHooks,
42 auth_provider: Option<Arc<dyn AuthProvider>>,
44 middleware: Vec<Box<dyn crate::Middleware>>,
46 task_manager: Option<SharedTaskManager>,
48 on_duplicate: DuplicateBehavior,
50 strict_input_validation: bool,
52 http_config: HttpServerConfig,
54}
55
56impl ServerBuilder {
57 #[must_use]
65 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
66 Self {
67 info: ServerInfo {
68 name: name.into(),
69 version: version.into(),
70 },
71 capabilities: ServerCapabilities {
72 logging: Some(LoggingCapability::default()),
73 ..ServerCapabilities::default()
74 },
75 router: Router::new(),
76 instructions: None,
77 request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
78 stats_enabled: true,
79 mask_error_details: false, logging: LoggingConfig::from_env(),
81 console_config: ConsoleConfig::from_env(),
82 lifespan: LifespanHooks::default(),
83 auth_provider: None,
84 middleware: Vec::new(),
85 task_manager: None,
86 on_duplicate: DuplicateBehavior::default(),
87 strict_input_validation: false,
88 http_config: HttpServerConfig::default(),
89 }
90 }
91
92 #[must_use]
112 pub fn on_duplicate(mut self, behavior: DuplicateBehavior) -> Self {
113 self.on_duplicate = behavior;
114 self
115 }
116
117 #[must_use]
119 pub fn auth_provider<P: AuthProvider + 'static>(mut self, provider: P) -> Self {
120 self.auth_provider = Some(Arc::new(provider));
121 self
122 }
123
124 #[must_use]
131 pub fn without_stats(mut self) -> Self {
132 self.stats_enabled = false;
133 self
134 }
135
136 #[must_use]
141 pub fn request_timeout(mut self, secs: u64) -> Self {
142 self.request_timeout_secs = secs;
143 self
144 }
145
146 #[must_use]
152 pub fn list_page_size(mut self, page_size: usize) -> Self {
153 self.router.set_list_page_size(Some(page_size));
154 self
155 }
156
157 #[must_use]
178 pub fn mask_error_details(mut self, enabled: bool) -> Self {
179 self.mask_error_details = enabled;
180 self
181 }
182
183 #[must_use]
201 pub fn auto_mask_errors(mut self) -> Self {
202 if let Ok(val) = std::env::var("FASTMCP_MASK_ERRORS") {
204 match val.to_lowercase().as_str() {
205 "true" | "1" | "yes" => {
206 self.mask_error_details = true;
207 return self;
208 }
209 "false" | "0" | "no" => {
210 self.mask_error_details = false;
211 return self;
212 }
213 _ => {} }
215 }
216
217 if let Ok(env) = std::env::var("FASTMCP_ENV") {
219 if env.to_lowercase() == "production" {
220 self.mask_error_details = true;
221 return self;
222 }
223 }
224
225 self.mask_error_details = cfg!(not(debug_assertions));
227 self
228 }
229
230 #[must_use]
232 pub fn is_error_masking_enabled(&self) -> bool {
233 self.mask_error_details
234 }
235
236 #[must_use]
252 pub fn strict_input_validation(mut self, enabled: bool) -> Self {
253 self.strict_input_validation = enabled;
254 self
255 }
256
257 #[must_use]
259 pub fn is_strict_input_validation_enabled(&self) -> bool {
260 self.strict_input_validation
261 }
262
263 #[must_use]
278 pub fn http_config(mut self, config: HttpServerConfig) -> Self {
279 self.http_config = config;
280 self
281 }
282
283 #[must_use]
285 pub fn middleware<M: crate::Middleware + 'static>(mut self, middleware: M) -> Self {
286 self.middleware.push(Box::new(middleware));
287 self
288 }
289
290 #[must_use]
296 pub fn tool<H: ToolHandler + 'static>(mut self, handler: H) -> Self {
297 if let Err(e) = self
298 .router
299 .add_tool_with_behavior(handler, self.on_duplicate)
300 {
301 log::error!(target: "fastmcp_rust::builder", "Failed to register tool: {}", e);
302 } else {
303 self.capabilities.tools = Some(ToolsCapability::default());
304 }
305 self
306 }
307
308 #[must_use]
314 pub fn resource<H: ResourceHandler + 'static>(mut self, handler: H) -> Self {
315 if let Err(e) = self
316 .router
317 .add_resource_with_behavior(handler, self.on_duplicate)
318 {
319 log::error!(target: "fastmcp_rust::builder", "Failed to register resource: {}", e);
320 } else {
321 self.capabilities.resources = Some(ResourcesCapability::default());
322 }
323 self
324 }
325
326 #[must_use]
328 pub fn resource_template(mut self, template: ResourceTemplate) -> Self {
329 self.router.add_resource_template(template);
330 self.capabilities.resources = Some(ResourcesCapability::default());
331 self
332 }
333
334 #[must_use]
340 pub fn prompt<H: PromptHandler + 'static>(mut self, handler: H) -> Self {
341 if let Err(e) = self
342 .router
343 .add_prompt_with_behavior(handler, self.on_duplicate)
344 {
345 log::error!(target: "fastmcp_rust::builder", "Failed to register prompt: {}", e);
346 } else {
347 self.capabilities.prompts = Some(PromptsCapability::default());
348 }
349 self
350 }
351
352 #[must_use]
357 pub fn proxy(mut self, client: ProxyClient, catalog: ProxyCatalog) -> Self {
358 let has_tools = !catalog.tools.is_empty();
359 let has_resources = !catalog.resources.is_empty() || !catalog.resource_templates.is_empty();
360 let has_prompts = !catalog.prompts.is_empty();
361
362 for tool in catalog.tools {
363 self.router
364 .add_tool(ProxyToolHandler::new(tool, client.clone()));
365 }
366
367 for resource in catalog.resources {
368 self.router
369 .add_resource(ProxyResourceHandler::new(resource, client.clone()));
370 }
371
372 for template in catalog.resource_templates {
373 self.router
374 .add_resource(ProxyResourceHandler::from_template(
375 template,
376 client.clone(),
377 ));
378 }
379
380 for prompt in catalog.prompts {
381 self.router
382 .add_prompt(ProxyPromptHandler::new(prompt, client.clone()));
383 }
384
385 if has_tools {
386 self.capabilities.tools = Some(ToolsCapability::default());
387 }
388 if has_resources {
389 self.capabilities.resources = Some(ResourcesCapability::default());
390 }
391 if has_prompts {
392 self.capabilities.prompts = Some(PromptsCapability::default());
393 }
394
395 self
396 }
397
398 pub fn as_proxy(
427 mut self,
428 prefix: &str,
429 client: fastmcp_client::Client,
430 ) -> Result<Self, fastmcp_core::McpError> {
431 let proxy_client = ProxyClient::from_client(client);
433 let catalog = proxy_client.catalog()?;
434
435 let tool_count = catalog.tools.len();
437 let resource_count = catalog.resources.len();
438 let template_count = catalog.resource_templates.len();
439 let prompt_count = catalog.prompts.len();
440
441 let has_tools = tool_count > 0;
442 let has_resources = resource_count > 0 || template_count > 0;
443 let has_prompts = prompt_count > 0;
444
445 for tool in catalog.tools {
447 log::debug!(
448 target: "fastmcp_rust::proxy",
449 "Registering proxied tool: {}/{}", prefix, tool.name
450 );
451 self.router.add_tool(ProxyToolHandler::with_prefix(
452 tool,
453 prefix,
454 proxy_client.clone(),
455 ));
456 }
457
458 for resource in catalog.resources {
460 log::debug!(
461 target: "fastmcp_rust::proxy",
462 "Registering proxied resource: {}/{}", prefix, resource.uri
463 );
464 self.router.add_resource(ProxyResourceHandler::with_prefix(
465 resource,
466 prefix,
467 proxy_client.clone(),
468 ));
469 }
470
471 for template in catalog.resource_templates {
473 log::debug!(
474 target: "fastmcp_rust::proxy",
475 "Registering proxied template: {}/{}", prefix, template.uri_template
476 );
477 self.router
478 .add_resource(ProxyResourceHandler::from_template_with_prefix(
479 template,
480 prefix,
481 proxy_client.clone(),
482 ));
483 }
484
485 for prompt in catalog.prompts {
487 log::debug!(
488 target: "fastmcp_rust::proxy",
489 "Registering proxied prompt: {}/{}", prefix, prompt.name
490 );
491 self.router.add_prompt(ProxyPromptHandler::with_prefix(
492 prompt,
493 prefix,
494 proxy_client.clone(),
495 ));
496 }
497
498 if has_tools {
500 self.capabilities.tools = Some(ToolsCapability::default());
501 }
502 if has_resources {
503 self.capabilities.resources = Some(ResourcesCapability::default());
504 }
505 if has_prompts {
506 self.capabilities.prompts = Some(PromptsCapability::default());
507 }
508
509 log::info!(
510 target: "fastmcp_rust::proxy",
511 "Proxied {} tools, {} resources, {} templates, {} prompts with prefix '{}'",
512 tool_count,
513 resource_count,
514 template_count,
515 prompt_count,
516 prefix
517 );
518
519 Ok(self)
520 }
521
522 pub fn as_proxy_raw(
536 self,
537 client: fastmcp_client::Client,
538 ) -> Result<Self, fastmcp_core::McpError> {
539 let proxy_client = ProxyClient::from_client(client);
540 let catalog = proxy_client.catalog()?;
541 Ok(self.proxy(proxy_client, catalog))
542 }
543
544 #[must_use]
578 pub fn mount(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
579 let has_tools = server.has_tools();
580 let has_resources = server.has_resources();
581 let has_prompts = server.has_prompts();
582
583 let source_router = server.into_router();
584 let result = self.router.mount(source_router, prefix);
585
586 for warning in &result.warnings {
588 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
589 }
590
591 if has_tools && result.tools > 0 {
593 self.capabilities.tools = Some(ToolsCapability::default());
594 }
595 if has_resources && (result.resources > 0 || result.resource_templates > 0) {
596 self.capabilities.resources = Some(ResourcesCapability::default());
597 }
598 if has_prompts && result.prompts > 0 {
599 self.capabilities.prompts = Some(PromptsCapability::default());
600 }
601
602 self
603 }
604
605 #[must_use]
624 pub fn mount_tools(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
625 let source_router = server.into_router();
626 let result = self.router.mount_tools(source_router, prefix);
627
628 for warning in &result.warnings {
630 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
631 }
632
633 if result.tools > 0 {
635 self.capabilities.tools = Some(ToolsCapability::default());
636 }
637
638 self
639 }
640
641 #[must_use]
660 pub fn mount_resources(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
661 let source_router = server.into_router();
662 let result = self.router.mount_resources(source_router, prefix);
663
664 for warning in &result.warnings {
666 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
667 }
668
669 if result.resources > 0 || result.resource_templates > 0 {
671 self.capabilities.resources = Some(ResourcesCapability::default());
672 }
673
674 self
675 }
676
677 #[must_use]
696 pub fn mount_prompts(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
697 let source_router = server.into_router();
698 let result = self.router.mount_prompts(source_router, prefix);
699
700 for warning in &result.warnings {
702 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
703 }
704
705 if result.prompts > 0 {
707 self.capabilities.prompts = Some(PromptsCapability::default());
708 }
709
710 self
711 }
712
713 #[must_use]
715 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
716 self.instructions = Some(instructions.into());
717 self
718 }
719
720 #[must_use]
724 pub fn log_level(mut self, level: Level) -> Self {
725 self.logging.level = level;
726 self
727 }
728
729 #[must_use]
731 pub fn log_level_filter(mut self, filter: LevelFilter) -> Self {
732 self.logging.level = filter.to_level().unwrap_or(Level::Info);
733 self
734 }
735
736 #[must_use]
740 pub fn log_timestamps(mut self, show: bool) -> Self {
741 self.logging.timestamps = show;
742 self
743 }
744
745 #[must_use]
749 pub fn log_targets(mut self, show: bool) -> Self {
750 self.logging.targets = show;
751 self
752 }
753
754 #[must_use]
756 pub fn logging(mut self, config: LoggingConfig) -> Self {
757 self.logging = config;
758 self
759 }
760
761 #[must_use]
784 pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
785 self.console_config = config;
786 self
787 }
788
789 #[must_use]
794 pub fn with_banner(mut self, style: BannerStyle) -> Self {
795 self.console_config = self.console_config.with_banner(style);
796 self
797 }
798
799 #[must_use]
801 pub fn without_banner(mut self) -> Self {
802 self.console_config = self.console_config.without_banner();
803 self
804 }
805
806 #[must_use]
814 pub fn with_traffic_logging(mut self, verbosity: TrafficVerbosity) -> Self {
815 self.console_config = self.console_config.with_traffic(verbosity);
816 self
817 }
818
819 #[must_use]
824 pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
825 self.console_config = self.console_config.with_periodic_stats(interval_secs);
826 self
827 }
828
829 #[must_use]
835 pub fn plain_mode(mut self) -> Self {
836 self.console_config = self.console_config.plain_mode();
837 self
838 }
839
840 #[must_use]
842 pub fn force_color(mut self) -> Self {
843 self.console_config = self.console_config.force_color(true);
844 self
845 }
846
847 #[must_use]
849 pub fn console_config(&self) -> &ConsoleConfig {
850 &self.console_config
851 }
852
853 #[must_use]
877 pub fn on_startup<F, E>(mut self, hook: F) -> Self
878 where
879 F: FnOnce() -> Result<(), E> + Send + 'static,
880 E: std::error::Error + Send + Sync + 'static,
881 {
882 self.lifespan.on_startup = Some(Box::new(move || {
883 hook().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
884 }));
885 self
886 }
887
888 #[must_use]
908 pub fn on_shutdown<F>(mut self, hook: F) -> Self
909 where
910 F: FnOnce() + Send + 'static,
911 {
912 self.lifespan.on_shutdown = Some(Box::new(hook));
913 self
914 }
915
916 #[must_use]
932 pub fn with_task_manager(mut self, task_manager: SharedTaskManager) -> Self {
933 self.task_manager = Some(task_manager);
934 let mut capability = TasksCapability::default();
935 if let Some(manager) = &self.task_manager {
936 capability.list_changed = manager.has_list_changed_notifications();
937 }
938 self.capabilities.tasks = Some(capability);
939 self
940 }
941
942 #[cfg(test)]
944 fn request_timeout_secs(&self) -> u64 {
945 self.request_timeout_secs
946 }
947
948 #[must_use]
950 pub fn build(mut self) -> Server {
951 self.router
953 .set_strict_input_validation(self.strict_input_validation);
954
955 Server {
956 info: self.info,
957 capabilities: self.capabilities,
958 router: self.router,
959 instructions: self.instructions,
960 request_timeout_secs: self.request_timeout_secs,
961 stats: if self.stats_enabled {
962 Some(ServerStats::new())
963 } else {
964 None
965 },
966 mask_error_details: self.mask_error_details,
967 logging: self.logging,
968 console_config: self.console_config,
969 lifespan: Mutex::new(Some(self.lifespan)),
970 auth_provider: self.auth_provider,
971 middleware: Arc::new(self.middleware),
972 active_requests: Mutex::new(HashMap::new()),
973 task_manager: self.task_manager,
974 pending_requests: std::sync::Arc::new(crate::bidirectional::PendingRequests::new()),
975 http_config: self.http_config,
976 }
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use super::*;
983 use fastmcp_core::{McpContext, McpResult};
984 use fastmcp_protocol::{Content, Prompt, Resource, ResourceContent, Tool};
985
986 struct TestTool;
989 impl crate::ToolHandler for TestTool {
990 fn definition(&self) -> Tool {
991 Tool {
992 name: "test_tool".to_string(),
993 description: Some("a test tool".to_string()),
994 input_schema: serde_json::json!({"type": "object"}),
995 output_schema: None,
996 icon: None,
997 version: None,
998 tags: vec![],
999 annotations: None,
1000 }
1001 }
1002 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1003 Ok(vec![Content::text("ok")])
1004 }
1005 }
1006
1007 struct TestResource;
1008 impl crate::ResourceHandler for TestResource {
1009 fn definition(&self) -> Resource {
1010 Resource {
1011 uri: "file:///test".to_string(),
1012 name: "test_res".to_string(),
1013 description: None,
1014 mime_type: None,
1015 icon: None,
1016 version: None,
1017 tags: vec![],
1018 }
1019 }
1020 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1021 Ok(vec![ResourceContent {
1022 uri: "file:///test".to_string(),
1023 mime_type: None,
1024 text: Some("content".to_string()),
1025 blob: None,
1026 }])
1027 }
1028 }
1029
1030 struct TestPrompt;
1031 impl crate::PromptHandler for TestPrompt {
1032 fn definition(&self) -> Prompt {
1033 Prompt {
1034 name: "test_prompt".to_string(),
1035 description: None,
1036 arguments: vec![],
1037 icon: None,
1038 version: None,
1039 tags: vec![],
1040 }
1041 }
1042 fn get(
1043 &self,
1044 _ctx: &McpContext,
1045 _args: std::collections::HashMap<String, String>,
1046 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1047 Ok(vec![])
1048 }
1049 }
1050
1051 #[test]
1054 fn builder_new_sets_info() {
1055 let builder = ServerBuilder::new("my-server", "2.0.0");
1056 let server = builder.build();
1057 assert_eq!(server.info().name, "my-server");
1058 assert_eq!(server.info().version, "2.0.0");
1059 }
1060
1061 #[test]
1062 fn builder_default_has_logging_capability() {
1063 let builder = ServerBuilder::new("srv", "1.0");
1064 let server = builder.build();
1065 assert!(server.capabilities().logging.is_some());
1066 }
1067
1068 #[test]
1069 fn builder_default_has_no_tool_resource_prompt_capabilities() {
1070 let builder = ServerBuilder::new("srv", "1.0");
1071 let server = builder.build();
1072 assert!(server.capabilities().tools.is_none());
1073 assert!(server.capabilities().resources.is_none());
1074 assert!(server.capabilities().prompts.is_none());
1075 }
1076
1077 #[test]
1078 fn builder_default_stats_enabled() {
1079 let server = ServerBuilder::new("srv", "1.0").build();
1080 assert!(server.stats().is_some());
1081 }
1082
1083 #[test]
1084 fn builder_default_request_timeout() {
1085 let builder = ServerBuilder::new("srv", "1.0");
1086 assert_eq!(builder.request_timeout_secs(), DEFAULT_REQUEST_TIMEOUT_SECS);
1087 }
1088
1089 #[test]
1090 fn builder_default_error_masking_disabled() {
1091 let builder = ServerBuilder::new("srv", "1.0");
1092 assert!(!builder.is_error_masking_enabled());
1093 }
1094
1095 #[test]
1096 fn builder_default_strict_validation_disabled() {
1097 let builder = ServerBuilder::new("srv", "1.0");
1098 assert!(!builder.is_strict_input_validation_enabled());
1099 }
1100
1101 #[test]
1104 fn builder_request_timeout() {
1105 let builder = ServerBuilder::new("srv", "1.0").request_timeout(60);
1106 assert_eq!(builder.request_timeout_secs(), 60);
1107 }
1108
1109 #[test]
1110 fn builder_request_timeout_zero_disables() {
1111 let builder = ServerBuilder::new("srv", "1.0").request_timeout(0);
1112 assert_eq!(builder.request_timeout_secs(), 0);
1113 }
1114
1115 #[test]
1116 fn builder_without_stats() {
1117 let server = ServerBuilder::new("srv", "1.0").without_stats().build();
1118 assert!(server.stats().is_none());
1119 }
1120
1121 #[test]
1122 fn builder_mask_error_details() {
1123 let builder = ServerBuilder::new("srv", "1.0").mask_error_details(true);
1124 assert!(builder.is_error_masking_enabled());
1125 }
1126
1127 #[test]
1128 fn builder_strict_input_validation() {
1129 let builder = ServerBuilder::new("srv", "1.0").strict_input_validation(true);
1130 assert!(builder.is_strict_input_validation_enabled());
1131 }
1132
1133 #[test]
1134 fn builder_instructions() {
1135 let server = ServerBuilder::new("srv", "1.0")
1136 .instructions("Use this server wisely")
1137 .build();
1138 let _ = server;
1140 }
1141
1142 #[test]
1143 fn builder_log_level() {
1144 let _builder = ServerBuilder::new("srv", "1.0").log_level(Level::Debug);
1145 }
1146
1147 #[test]
1148 fn builder_log_level_filter() {
1149 let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Warn);
1150 }
1151
1152 #[test]
1153 fn builder_log_timestamps_and_targets() {
1154 let _builder = ServerBuilder::new("srv", "1.0")
1155 .log_timestamps(false)
1156 .log_targets(false);
1157 }
1158
1159 #[test]
1162 fn builder_without_banner() {
1163 let builder = ServerBuilder::new("srv", "1.0").without_banner();
1164 let config = builder.console_config();
1165 assert_eq!(config.banner_style, BannerStyle::None);
1166 }
1167
1168 #[test]
1169 fn builder_with_banner_compact() {
1170 let builder = ServerBuilder::new("srv", "1.0").with_banner(BannerStyle::Compact);
1171 let config = builder.console_config();
1172 assert_eq!(config.banner_style, BannerStyle::Compact);
1173 }
1174
1175 #[test]
1176 fn builder_plain_mode() {
1177 let builder = ServerBuilder::new("srv", "1.0").plain_mode();
1178 let _config = builder.console_config();
1179 }
1180
1181 #[test]
1184 fn builder_tool_enables_capability() {
1185 let server = ServerBuilder::new("srv", "1.0").tool(TestTool).build();
1186 assert!(server.capabilities().tools.is_some());
1187 assert!(server.has_tools());
1188 }
1189
1190 #[test]
1191 fn builder_resource_enables_capability() {
1192 let server = ServerBuilder::new("srv", "1.0")
1193 .resource(TestResource)
1194 .build();
1195 assert!(server.capabilities().resources.is_some());
1196 assert!(server.has_resources());
1197 }
1198
1199 #[test]
1200 fn builder_prompt_enables_capability() {
1201 let server = ServerBuilder::new("srv", "1.0").prompt(TestPrompt).build();
1202 assert!(server.capabilities().prompts.is_some());
1203 assert!(server.has_prompts());
1204 }
1205
1206 #[test]
1207 fn builder_all_handlers() {
1208 let server = ServerBuilder::new("srv", "1.0")
1209 .tool(TestTool)
1210 .resource(TestResource)
1211 .prompt(TestPrompt)
1212 .build();
1213 assert!(server.has_tools());
1214 assert!(server.has_resources());
1215 assert!(server.has_prompts());
1216 }
1217
1218 #[test]
1219 fn builder_no_handlers_means_no_capabilities() {
1220 let server = ServerBuilder::new("srv", "1.0").build();
1221 assert!(!server.has_tools());
1222 assert!(!server.has_resources());
1223 assert!(!server.has_prompts());
1224 }
1225
1226 #[test]
1229 fn builder_on_duplicate_default_is_warn() {
1230 let _builder = ServerBuilder::new("srv", "1.0");
1231 }
1233
1234 #[test]
1235 fn builder_on_duplicate_ignore() {
1236 let server = ServerBuilder::new("srv", "1.0")
1237 .on_duplicate(DuplicateBehavior::Ignore)
1238 .tool(TestTool)
1239 .build();
1240 assert!(server.has_tools());
1241 }
1242
1243 #[test]
1244 fn builder_on_duplicate_replace() {
1245 let server = ServerBuilder::new("srv", "1.0")
1246 .on_duplicate(DuplicateBehavior::Replace)
1247 .tool(TestTool)
1248 .build();
1249 assert!(server.has_tools());
1250 }
1251
1252 #[test]
1255 fn builder_on_startup_builds() {
1256 let server = ServerBuilder::new("srv", "1.0")
1257 .on_startup(|| -> Result<(), std::io::Error> { Ok(()) })
1258 .build();
1259 let _ = server;
1260 }
1261
1262 #[test]
1263 fn builder_on_shutdown_builds() {
1264 let server = ServerBuilder::new("srv", "1.0").on_shutdown(|| {}).build();
1265 let _ = server;
1266 }
1267
1268 #[test]
1271 fn built_server_console_config_matches_builder() {
1272 let server = ServerBuilder::new("srv", "1.0").without_banner().build();
1273 assert_eq!(server.console_config().banner_style, BannerStyle::None);
1274 }
1275
1276 #[test]
1279 fn builder_chaining_fluent_api() {
1280 let server = ServerBuilder::new("chain", "3.0")
1281 .request_timeout(120)
1282 .mask_error_details(true)
1283 .strict_input_validation(true)
1284 .without_banner()
1285 .plain_mode()
1286 .tool(TestTool)
1287 .resource(TestResource)
1288 .prompt(TestPrompt)
1289 .on_shutdown(|| {})
1290 .build();
1291
1292 assert_eq!(server.info().name, "chain");
1293 assert_eq!(server.info().version, "3.0");
1294 assert!(server.has_tools());
1295 assert!(server.has_resources());
1296 assert!(server.has_prompts());
1297 }
1298
1299 #[test]
1302 fn builder_with_console_config() {
1303 let config = ConsoleConfig::new().with_banner(BannerStyle::None);
1304 let builder = ServerBuilder::new("srv", "1.0").with_console_config(config);
1305 assert_eq!(builder.console_config().banner_style, BannerStyle::None);
1306 }
1307
1308 #[test]
1309 fn builder_with_traffic_logging() {
1310 let builder = ServerBuilder::new("srv", "1.0").with_traffic_logging(TrafficVerbosity::Full);
1311 let config = builder.console_config();
1312 assert_eq!(config.traffic_verbosity, TrafficVerbosity::Full);
1313 }
1314
1315 #[test]
1316 fn builder_with_periodic_stats() {
1317 let builder = ServerBuilder::new("srv", "1.0").with_periodic_stats(30);
1318 let config = builder.console_config();
1319 assert_eq!(config.stats_interval_secs, 30);
1320 }
1321
1322 #[test]
1323 fn builder_force_color() {
1324 let builder = ServerBuilder::new("srv", "1.0").force_color();
1325 let _config = builder.console_config();
1326 }
1328
1329 #[test]
1332 fn builder_logging_full_config() {
1333 let config = LoggingConfig {
1334 level: Level::Trace,
1335 timestamps: false,
1336 targets: false,
1337 file_line: true,
1338 };
1339 let _builder = ServerBuilder::new("srv", "1.0").logging(config);
1340 }
1341
1342 #[test]
1345 fn builder_list_page_size() {
1346 let server = ServerBuilder::new("srv", "1.0")
1347 .list_page_size(50)
1348 .tool(TestTool)
1349 .build();
1350 assert!(server.has_tools());
1351 }
1352
1353 #[test]
1356 fn builder_resource_template_enables_capability() {
1357 let template = ResourceTemplate {
1358 uri_template: "file://{path}".to_string(),
1359 name: "Template".to_string(),
1360 description: None,
1361 mime_type: None,
1362 icon: None,
1363 version: None,
1364 tags: vec![],
1365 };
1366 let server = ServerBuilder::new("srv", "1.0")
1367 .resource_template(template)
1368 .build();
1369 assert!(server.capabilities().resources.is_some());
1370 }
1371
1372 struct NoopMiddleware;
1375 impl crate::Middleware for NoopMiddleware {}
1376
1377 #[test]
1378 fn builder_middleware() {
1379 let server = ServerBuilder::new("srv", "1.0")
1380 .middleware(NoopMiddleware)
1381 .build();
1382 let _ = server;
1383 }
1384
1385 #[test]
1386 fn builder_multiple_middleware() {
1387 let server = ServerBuilder::new("srv", "1.0")
1388 .middleware(NoopMiddleware)
1389 .middleware(NoopMiddleware)
1390 .build();
1391 let _ = server;
1392 }
1393
1394 struct TestAuthProvider;
1397 impl crate::AuthProvider for TestAuthProvider {
1398 fn authenticate(
1399 &self,
1400 _ctx: &McpContext,
1401 _request: crate::auth::AuthRequest<'_>,
1402 ) -> McpResult<fastmcp_core::AuthContext> {
1403 Ok(fastmcp_core::AuthContext::with_subject("test-user"))
1404 }
1405 }
1406
1407 #[test]
1408 fn builder_auth_provider() {
1409 let server = ServerBuilder::new("srv", "1.0")
1410 .auth_provider(TestAuthProvider)
1411 .build();
1412 let _ = server;
1413 }
1414
1415 #[test]
1418 fn builder_auto_mask_errors() {
1419 let builder = ServerBuilder::new("srv", "1.0").auto_mask_errors();
1421 assert!(!builder.is_error_masking_enabled());
1423 }
1424
1425 struct DupTool(&'static str);
1428 impl crate::ToolHandler for DupTool {
1429 fn definition(&self) -> Tool {
1430 Tool {
1431 name: self.0.to_string(),
1432 description: None,
1433 input_schema: serde_json::json!({"type": "object"}),
1434 output_schema: None,
1435 icon: None,
1436 version: None,
1437 tags: vec![],
1438 annotations: None,
1439 }
1440 }
1441 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1442 Ok(vec![Content::text("ok")])
1443 }
1444 }
1445
1446 #[test]
1447 fn builder_on_duplicate_error_logs_but_continues() {
1448 let server = ServerBuilder::new("srv", "1.0")
1451 .on_duplicate(DuplicateBehavior::Error)
1452 .tool(DupTool("dup"))
1453 .tool(DupTool("dup")) .build();
1455 assert!(server.has_tools());
1456 }
1457
1458 #[test]
1461 fn builder_mount_with_prefix() {
1462 let source = ServerBuilder::new("sub", "1.0")
1463 .tool(TestTool)
1464 .resource(TestResource)
1465 .prompt(TestPrompt)
1466 .build();
1467
1468 let main = ServerBuilder::new("main", "1.0")
1469 .mount(source, Some("sub"))
1470 .build();
1471
1472 assert!(main.has_tools());
1473 assert!(main.has_resources());
1474 assert!(main.has_prompts());
1475 }
1476
1477 #[test]
1478 fn builder_mount_without_prefix() {
1479 let source = ServerBuilder::new("sub", "1.0").tool(TestTool).build();
1480
1481 let main = ServerBuilder::new("main", "1.0")
1482 .mount(source, None)
1483 .build();
1484
1485 assert!(main.has_tools());
1486 }
1487
1488 #[test]
1489 fn builder_mount_tools_only() {
1490 let source = ServerBuilder::new("sub", "1.0")
1491 .tool(TestTool)
1492 .resource(TestResource)
1493 .prompt(TestPrompt)
1494 .build();
1495
1496 let main = ServerBuilder::new("main", "1.0")
1497 .mount_tools(source, Some("sub"))
1498 .build();
1499
1500 assert!(main.has_tools());
1501 assert!(!main.has_resources());
1503 assert!(!main.has_prompts());
1504 }
1505
1506 #[test]
1507 fn builder_mount_resources_only() {
1508 let source = ServerBuilder::new("sub", "1.0")
1509 .tool(TestTool)
1510 .resource(TestResource)
1511 .prompt(TestPrompt)
1512 .build();
1513
1514 let main = ServerBuilder::new("main", "1.0")
1515 .mount_resources(source, Some("data"))
1516 .build();
1517
1518 assert!(!main.has_tools());
1519 assert!(main.has_resources());
1520 assert!(!main.has_prompts());
1521 }
1522
1523 #[test]
1524 fn builder_mount_prompts_only() {
1525 let source = ServerBuilder::new("sub", "1.0")
1526 .tool(TestTool)
1527 .resource(TestResource)
1528 .prompt(TestPrompt)
1529 .build();
1530
1531 let main = ServerBuilder::new("main", "1.0")
1532 .mount_prompts(source, Some("tmpl"))
1533 .build();
1534
1535 assert!(!main.has_tools());
1536 assert!(!main.has_resources());
1537 assert!(main.has_prompts());
1538 }
1539
1540 #[test]
1541 fn builder_mount_empty_server() {
1542 let source = ServerBuilder::new("empty", "1.0").build();
1543
1544 let main = ServerBuilder::new("main", "1.0")
1545 .mount(source, Some("empty"))
1546 .build();
1547
1548 assert!(!main.has_tools());
1549 assert!(!main.has_resources());
1550 assert!(!main.has_prompts());
1551 }
1552
1553 #[test]
1556 fn builder_proxy_with_catalog() {
1557 use crate::proxy::{ProxyCatalog, ProxyClient};
1558
1559 struct DummyBackend;
1560 impl crate::proxy::ProxyBackend for DummyBackend {
1561 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
1562 Ok(vec![])
1563 }
1564 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
1565 Ok(vec![])
1566 }
1567 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
1568 Ok(vec![])
1569 }
1570 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
1571 Ok(vec![])
1572 }
1573 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
1574 Ok(vec![])
1575 }
1576 fn call_tool_with_progress(
1577 &mut self,
1578 _: &str,
1579 _: serde_json::Value,
1580 _: crate::proxy::ProgressCallback<'_>,
1581 ) -> McpResult<Vec<Content>> {
1582 Ok(vec![])
1583 }
1584 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
1585 Ok(vec![])
1586 }
1587 fn get_prompt(
1588 &mut self,
1589 _: &str,
1590 _: std::collections::HashMap<String, String>,
1591 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1592 Ok(vec![])
1593 }
1594 }
1595
1596 let client = ProxyClient::from_backend(DummyBackend);
1597 let catalog = ProxyCatalog {
1598 tools: vec![Tool {
1599 name: "proxy-tool".to_string(),
1600 description: None,
1601 input_schema: serde_json::json!({}),
1602 output_schema: None,
1603 icon: None,
1604 version: None,
1605 tags: vec![],
1606 annotations: None,
1607 }],
1608 ..ProxyCatalog::default()
1609 };
1610
1611 let server = ServerBuilder::new("srv", "1.0")
1612 .proxy(client, catalog)
1613 .build();
1614 assert!(server.has_tools());
1615 }
1616
1617 #[test]
1620 fn default_request_timeout_constant() {
1621 assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30);
1622 }
1623
1624 #[test]
1627 fn builder_mask_error_details_toggle() {
1628 let builder = ServerBuilder::new("srv", "1.0")
1629 .mask_error_details(true)
1630 .mask_error_details(false);
1631 assert!(!builder.is_error_masking_enabled());
1632 }
1633
1634 #[test]
1637 fn builder_strict_validation_toggle() {
1638 let builder = ServerBuilder::new("srv", "1.0")
1639 .strict_input_validation(true)
1640 .strict_input_validation(false);
1641 assert!(!builder.is_strict_input_validation_enabled());
1642 }
1643
1644 #[test]
1647 fn builder_with_task_manager_enables_capability() {
1648 use crate::tasks::TaskManager;
1649 let tm = TaskManager::new().into_shared();
1650 let server = ServerBuilder::new("srv", "1.0")
1651 .with_task_manager(tm)
1652 .build();
1653 assert!(server.capabilities().tasks.is_some());
1654 }
1655
1656 #[test]
1657 fn builder_with_task_manager_list_changed_true() {
1658 use crate::tasks::TaskManager;
1659 let tm = TaskManager::with_list_changed_notifications().into_shared();
1660 let server = ServerBuilder::new("srv", "1.0")
1661 .with_task_manager(tm)
1662 .build();
1663 let cap = server.capabilities().tasks.as_ref().unwrap();
1664 assert!(cap.list_changed);
1665 }
1666
1667 #[test]
1668 fn builder_with_task_manager_list_changed_false() {
1669 use crate::tasks::TaskManager;
1670 let tm = TaskManager::new().into_shared();
1671 let server = ServerBuilder::new("srv", "1.0")
1672 .with_task_manager(tm)
1673 .build();
1674 let cap = server.capabilities().tasks.as_ref().unwrap();
1675 assert!(!cap.list_changed);
1676 }
1677
1678 struct DupResource(&'static str);
1681 impl crate::ResourceHandler for DupResource {
1682 fn definition(&self) -> Resource {
1683 Resource {
1684 uri: format!("file:///{}", self.0),
1685 name: self.0.to_string(),
1686 description: None,
1687 mime_type: None,
1688 icon: None,
1689 version: None,
1690 tags: vec![],
1691 }
1692 }
1693 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1694 Ok(vec![])
1695 }
1696 }
1697
1698 struct DupPrompt(&'static str);
1699 impl crate::PromptHandler for DupPrompt {
1700 fn definition(&self) -> Prompt {
1701 Prompt {
1702 name: self.0.to_string(),
1703 description: None,
1704 arguments: vec![],
1705 icon: None,
1706 version: None,
1707 tags: vec![],
1708 }
1709 }
1710 fn get(
1711 &self,
1712 _ctx: &McpContext,
1713 _args: std::collections::HashMap<String, String>,
1714 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1715 Ok(vec![])
1716 }
1717 }
1718
1719 #[test]
1720 fn builder_on_duplicate_error_resource_logs_but_continues() {
1721 let server = ServerBuilder::new("srv", "1.0")
1722 .on_duplicate(DuplicateBehavior::Error)
1723 .resource(DupResource("dup"))
1724 .resource(DupResource("dup"))
1725 .build();
1726 assert!(server.has_resources());
1727 }
1728
1729 #[test]
1730 fn builder_on_duplicate_error_prompt_logs_but_continues() {
1731 let server = ServerBuilder::new("srv", "1.0")
1732 .on_duplicate(DuplicateBehavior::Error)
1733 .prompt(DupPrompt("dup"))
1734 .prompt(DupPrompt("dup"))
1735 .build();
1736 assert!(server.has_prompts());
1737 }
1738
1739 #[test]
1742 fn builder_proxy_with_resources_and_prompts() {
1743 use crate::proxy::{ProxyCatalog, ProxyClient};
1744
1745 struct DummyBackend2;
1746 impl crate::proxy::ProxyBackend for DummyBackend2 {
1747 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
1748 Ok(vec![])
1749 }
1750 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
1751 Ok(vec![])
1752 }
1753 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
1754 Ok(vec![])
1755 }
1756 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
1757 Ok(vec![])
1758 }
1759 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
1760 Ok(vec![])
1761 }
1762 fn call_tool_with_progress(
1763 &mut self,
1764 _: &str,
1765 _: serde_json::Value,
1766 _: crate::proxy::ProgressCallback<'_>,
1767 ) -> McpResult<Vec<Content>> {
1768 Ok(vec![])
1769 }
1770 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
1771 Ok(vec![])
1772 }
1773 fn get_prompt(
1774 &mut self,
1775 _: &str,
1776 _: std::collections::HashMap<String, String>,
1777 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1778 Ok(vec![])
1779 }
1780 }
1781
1782 let client = ProxyClient::from_backend(DummyBackend2);
1783 let catalog = ProxyCatalog {
1784 resources: vec![Resource {
1785 uri: "file:///proxy-res".to_string(),
1786 name: "proxy-res".to_string(),
1787 description: None,
1788 mime_type: None,
1789 icon: None,
1790 version: None,
1791 tags: vec![],
1792 }],
1793 prompts: vec![Prompt {
1794 name: "proxy-prompt".to_string(),
1795 description: None,
1796 arguments: vec![],
1797 icon: None,
1798 version: None,
1799 tags: vec![],
1800 }],
1801 resource_templates: vec![ResourceTemplate {
1802 uri_template: "db://{table}".to_string(),
1803 name: "db".to_string(),
1804 description: None,
1805 mime_type: None,
1806 icon: None,
1807 version: None,
1808 tags: vec![],
1809 }],
1810 ..ProxyCatalog::default()
1811 };
1812
1813 let server = ServerBuilder::new("srv", "1.0")
1814 .proxy(client, catalog)
1815 .build();
1816 assert!(server.has_resources());
1817 assert!(server.has_prompts());
1818 assert!(!server.has_tools());
1819 }
1820
1821 #[test]
1824 fn build_propagates_strict_validation_to_router() {
1825 let server = ServerBuilder::new("srv", "1.0")
1826 .strict_input_validation(true)
1827 .build();
1828 let router = server.into_router();
1829 assert!(router.strict_input_validation());
1830 }
1831
1832 #[test]
1833 fn build_propagates_strict_validation_false_to_router() {
1834 let server = ServerBuilder::new("srv", "1.0")
1835 .strict_input_validation(false)
1836 .build();
1837 let router = server.into_router();
1838 assert!(!router.strict_input_validation());
1839 }
1840
1841 #[test]
1844 fn builder_log_level_filter_off() {
1845 let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Off);
1846 }
1848
1849 #[test]
1852 fn builder_mount_no_op_leaves_capabilities_unchanged() {
1853 let source = ServerBuilder::new("sub", "1.0").build();
1854 let main = ServerBuilder::new("main", "1.0")
1855 .mount(source, Some("ns"))
1856 .build();
1857 assert!(!main.has_tools());
1858 assert!(!main.has_resources());
1859 assert!(!main.has_prompts());
1860 }
1861}