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, LifespanHooks, LoggingConfig, PromptHandler, ProxyCatalog,
18 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}
53
54impl ServerBuilder {
55 #[must_use]
63 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
64 Self {
65 info: ServerInfo {
66 name: name.into(),
67 version: version.into(),
68 },
69 capabilities: ServerCapabilities {
70 logging: Some(LoggingCapability::default()),
71 ..ServerCapabilities::default()
72 },
73 router: Router::new(),
74 instructions: None,
75 request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
76 stats_enabled: true,
77 mask_error_details: false, logging: LoggingConfig::from_env(),
79 console_config: ConsoleConfig::from_env(),
80 lifespan: LifespanHooks::default(),
81 auth_provider: None,
82 middleware: Vec::new(),
83 task_manager: None,
84 on_duplicate: DuplicateBehavior::default(),
85 strict_input_validation: false,
86 }
87 }
88
89 #[must_use]
109 pub fn on_duplicate(mut self, behavior: DuplicateBehavior) -> Self {
110 self.on_duplicate = behavior;
111 self
112 }
113
114 #[must_use]
116 pub fn auth_provider<P: AuthProvider + 'static>(mut self, provider: P) -> Self {
117 self.auth_provider = Some(Arc::new(provider));
118 self
119 }
120
121 #[must_use]
128 pub fn without_stats(mut self) -> Self {
129 self.stats_enabled = false;
130 self
131 }
132
133 #[must_use]
138 pub fn request_timeout(mut self, secs: u64) -> Self {
139 self.request_timeout_secs = secs;
140 self
141 }
142
143 #[must_use]
149 pub fn list_page_size(mut self, page_size: usize) -> Self {
150 self.router.set_list_page_size(Some(page_size));
151 self
152 }
153
154 #[must_use]
175 pub fn mask_error_details(mut self, enabled: bool) -> Self {
176 self.mask_error_details = enabled;
177 self
178 }
179
180 #[must_use]
198 pub fn auto_mask_errors(mut self) -> Self {
199 if let Ok(val) = std::env::var("FASTMCP_MASK_ERRORS") {
201 match val.to_lowercase().as_str() {
202 "true" | "1" | "yes" => {
203 self.mask_error_details = true;
204 return self;
205 }
206 "false" | "0" | "no" => {
207 self.mask_error_details = false;
208 return self;
209 }
210 _ => {} }
212 }
213
214 if let Ok(env) = std::env::var("FASTMCP_ENV") {
216 if env.to_lowercase() == "production" {
217 self.mask_error_details = true;
218 return self;
219 }
220 }
221
222 self.mask_error_details = cfg!(not(debug_assertions));
224 self
225 }
226
227 #[must_use]
229 pub fn is_error_masking_enabled(&self) -> bool {
230 self.mask_error_details
231 }
232
233 #[must_use]
249 pub fn strict_input_validation(mut self, enabled: bool) -> Self {
250 self.strict_input_validation = enabled;
251 self
252 }
253
254 #[must_use]
256 pub fn is_strict_input_validation_enabled(&self) -> bool {
257 self.strict_input_validation
258 }
259
260 #[must_use]
262 pub fn middleware<M: crate::Middleware + 'static>(mut self, middleware: M) -> Self {
263 self.middleware.push(Box::new(middleware));
264 self
265 }
266
267 #[must_use]
273 pub fn tool<H: ToolHandler + 'static>(mut self, handler: H) -> Self {
274 if let Err(e) = self
275 .router
276 .add_tool_with_behavior(handler, self.on_duplicate)
277 {
278 log::error!(target: "fastmcp_rust::builder", "Failed to register tool: {}", e);
279 } else {
280 self.capabilities.tools = Some(ToolsCapability::default());
281 }
282 self
283 }
284
285 #[must_use]
291 pub fn resource<H: ResourceHandler + 'static>(mut self, handler: H) -> Self {
292 if let Err(e) = self
293 .router
294 .add_resource_with_behavior(handler, self.on_duplicate)
295 {
296 log::error!(target: "fastmcp_rust::builder", "Failed to register resource: {}", e);
297 } else {
298 self.capabilities.resources = Some(ResourcesCapability::default());
299 }
300 self
301 }
302
303 #[must_use]
305 pub fn resource_template(mut self, template: ResourceTemplate) -> Self {
306 self.router.add_resource_template(template);
307 self.capabilities.resources = Some(ResourcesCapability::default());
308 self
309 }
310
311 #[must_use]
317 pub fn prompt<H: PromptHandler + 'static>(mut self, handler: H) -> Self {
318 if let Err(e) = self
319 .router
320 .add_prompt_with_behavior(handler, self.on_duplicate)
321 {
322 log::error!(target: "fastmcp_rust::builder", "Failed to register prompt: {}", e);
323 } else {
324 self.capabilities.prompts = Some(PromptsCapability::default());
325 }
326 self
327 }
328
329 #[must_use]
334 pub fn proxy(mut self, client: ProxyClient, catalog: ProxyCatalog) -> Self {
335 let has_tools = !catalog.tools.is_empty();
336 let has_resources = !catalog.resources.is_empty() || !catalog.resource_templates.is_empty();
337 let has_prompts = !catalog.prompts.is_empty();
338
339 for tool in catalog.tools {
340 self.router
341 .add_tool(ProxyToolHandler::new(tool, client.clone()));
342 }
343
344 for resource in catalog.resources {
345 self.router
346 .add_resource(ProxyResourceHandler::new(resource, client.clone()));
347 }
348
349 for template in catalog.resource_templates {
350 self.router
351 .add_resource(ProxyResourceHandler::from_template(
352 template,
353 client.clone(),
354 ));
355 }
356
357 for prompt in catalog.prompts {
358 self.router
359 .add_prompt(ProxyPromptHandler::new(prompt, client.clone()));
360 }
361
362 if has_tools {
363 self.capabilities.tools = Some(ToolsCapability::default());
364 }
365 if has_resources {
366 self.capabilities.resources = Some(ResourcesCapability::default());
367 }
368 if has_prompts {
369 self.capabilities.prompts = Some(PromptsCapability::default());
370 }
371
372 self
373 }
374
375 pub fn as_proxy(
404 mut self,
405 prefix: &str,
406 client: fastmcp_client::Client,
407 ) -> Result<Self, fastmcp_core::McpError> {
408 let proxy_client = ProxyClient::from_client(client);
410 let catalog = proxy_client.catalog()?;
411
412 let tool_count = catalog.tools.len();
414 let resource_count = catalog.resources.len();
415 let template_count = catalog.resource_templates.len();
416 let prompt_count = catalog.prompts.len();
417
418 let has_tools = tool_count > 0;
419 let has_resources = resource_count > 0 || template_count > 0;
420 let has_prompts = prompt_count > 0;
421
422 for tool in catalog.tools {
424 log::debug!(
425 target: "fastmcp_rust::proxy",
426 "Registering proxied tool: {}/{}", prefix, tool.name
427 );
428 self.router.add_tool(ProxyToolHandler::with_prefix(
429 tool,
430 prefix,
431 proxy_client.clone(),
432 ));
433 }
434
435 for resource in catalog.resources {
437 log::debug!(
438 target: "fastmcp_rust::proxy",
439 "Registering proxied resource: {}/{}", prefix, resource.uri
440 );
441 self.router.add_resource(ProxyResourceHandler::with_prefix(
442 resource,
443 prefix,
444 proxy_client.clone(),
445 ));
446 }
447
448 for template in catalog.resource_templates {
450 log::debug!(
451 target: "fastmcp_rust::proxy",
452 "Registering proxied template: {}/{}", prefix, template.uri_template
453 );
454 self.router
455 .add_resource(ProxyResourceHandler::from_template_with_prefix(
456 template,
457 prefix,
458 proxy_client.clone(),
459 ));
460 }
461
462 for prompt in catalog.prompts {
464 log::debug!(
465 target: "fastmcp_rust::proxy",
466 "Registering proxied prompt: {}/{}", prefix, prompt.name
467 );
468 self.router.add_prompt(ProxyPromptHandler::with_prefix(
469 prompt,
470 prefix,
471 proxy_client.clone(),
472 ));
473 }
474
475 if has_tools {
477 self.capabilities.tools = Some(ToolsCapability::default());
478 }
479 if has_resources {
480 self.capabilities.resources = Some(ResourcesCapability::default());
481 }
482 if has_prompts {
483 self.capabilities.prompts = Some(PromptsCapability::default());
484 }
485
486 log::info!(
487 target: "fastmcp_rust::proxy",
488 "Proxied {} tools, {} resources, {} templates, {} prompts with prefix '{}'",
489 tool_count,
490 resource_count,
491 template_count,
492 prompt_count,
493 prefix
494 );
495
496 Ok(self)
497 }
498
499 pub fn as_proxy_raw(
513 self,
514 client: fastmcp_client::Client,
515 ) -> Result<Self, fastmcp_core::McpError> {
516 let proxy_client = ProxyClient::from_client(client);
517 let catalog = proxy_client.catalog()?;
518 Ok(self.proxy(proxy_client, catalog))
519 }
520
521 #[must_use]
555 pub fn mount(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
556 let has_tools = server.has_tools();
557 let has_resources = server.has_resources();
558 let has_prompts = server.has_prompts();
559
560 let source_router = server.into_router();
561 let result = self.router.mount(source_router, prefix);
562
563 for warning in &result.warnings {
565 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
566 }
567
568 if has_tools && result.tools > 0 {
570 self.capabilities.tools = Some(ToolsCapability::default());
571 }
572 if has_resources && (result.resources > 0 || result.resource_templates > 0) {
573 self.capabilities.resources = Some(ResourcesCapability::default());
574 }
575 if has_prompts && result.prompts > 0 {
576 self.capabilities.prompts = Some(PromptsCapability::default());
577 }
578
579 self
580 }
581
582 #[must_use]
601 pub fn mount_tools(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
602 let source_router = server.into_router();
603 let result = self.router.mount_tools(source_router, prefix);
604
605 for warning in &result.warnings {
607 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
608 }
609
610 if result.tools > 0 {
612 self.capabilities.tools = Some(ToolsCapability::default());
613 }
614
615 self
616 }
617
618 #[must_use]
637 pub fn mount_resources(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
638 let source_router = server.into_router();
639 let result = self.router.mount_resources(source_router, prefix);
640
641 for warning in &result.warnings {
643 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
644 }
645
646 if result.resources > 0 || result.resource_templates > 0 {
648 self.capabilities.resources = Some(ResourcesCapability::default());
649 }
650
651 self
652 }
653
654 #[must_use]
673 pub fn mount_prompts(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
674 let source_router = server.into_router();
675 let result = self.router.mount_prompts(source_router, prefix);
676
677 for warning in &result.warnings {
679 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
680 }
681
682 if result.prompts > 0 {
684 self.capabilities.prompts = Some(PromptsCapability::default());
685 }
686
687 self
688 }
689
690 #[must_use]
692 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
693 self.instructions = Some(instructions.into());
694 self
695 }
696
697 #[must_use]
701 pub fn log_level(mut self, level: Level) -> Self {
702 self.logging.level = level;
703 self
704 }
705
706 #[must_use]
708 pub fn log_level_filter(mut self, filter: LevelFilter) -> Self {
709 self.logging.level = filter.to_level().unwrap_or(Level::Info);
710 self
711 }
712
713 #[must_use]
717 pub fn log_timestamps(mut self, show: bool) -> Self {
718 self.logging.timestamps = show;
719 self
720 }
721
722 #[must_use]
726 pub fn log_targets(mut self, show: bool) -> Self {
727 self.logging.targets = show;
728 self
729 }
730
731 #[must_use]
733 pub fn logging(mut self, config: LoggingConfig) -> Self {
734 self.logging = config;
735 self
736 }
737
738 #[must_use]
761 pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
762 self.console_config = config;
763 self
764 }
765
766 #[must_use]
771 pub fn with_banner(mut self, style: BannerStyle) -> Self {
772 self.console_config = self.console_config.with_banner(style);
773 self
774 }
775
776 #[must_use]
778 pub fn without_banner(mut self) -> Self {
779 self.console_config = self.console_config.without_banner();
780 self
781 }
782
783 #[must_use]
791 pub fn with_traffic_logging(mut self, verbosity: TrafficVerbosity) -> Self {
792 self.console_config = self.console_config.with_traffic(verbosity);
793 self
794 }
795
796 #[must_use]
801 pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
802 self.console_config = self.console_config.with_periodic_stats(interval_secs);
803 self
804 }
805
806 #[must_use]
812 pub fn plain_mode(mut self) -> Self {
813 self.console_config = self.console_config.plain_mode();
814 self
815 }
816
817 #[must_use]
819 pub fn force_color(mut self) -> Self {
820 self.console_config = self.console_config.force_color(true);
821 self
822 }
823
824 #[must_use]
826 pub fn console_config(&self) -> &ConsoleConfig {
827 &self.console_config
828 }
829
830 #[must_use]
854 pub fn on_startup<F, E>(mut self, hook: F) -> Self
855 where
856 F: FnOnce() -> Result<(), E> + Send + 'static,
857 E: std::error::Error + Send + Sync + 'static,
858 {
859 self.lifespan.on_startup = Some(Box::new(move || {
860 hook().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
861 }));
862 self
863 }
864
865 #[must_use]
885 pub fn on_shutdown<F>(mut self, hook: F) -> Self
886 where
887 F: FnOnce() + Send + 'static,
888 {
889 self.lifespan.on_shutdown = Some(Box::new(hook));
890 self
891 }
892
893 #[must_use]
909 pub fn with_task_manager(mut self, task_manager: SharedTaskManager) -> Self {
910 self.task_manager = Some(task_manager);
911 let mut capability = TasksCapability::default();
912 if let Some(manager) = &self.task_manager {
913 capability.list_changed = manager.has_list_changed_notifications();
914 }
915 self.capabilities.tasks = Some(capability);
916 self
917 }
918
919 #[cfg(test)]
921 fn request_timeout_secs(&self) -> u64 {
922 self.request_timeout_secs
923 }
924
925 #[must_use]
927 pub fn build(mut self) -> Server {
928 self.router
930 .set_strict_input_validation(self.strict_input_validation);
931
932 Server {
933 info: self.info,
934 capabilities: self.capabilities,
935 router: self.router,
936 instructions: self.instructions,
937 request_timeout_secs: self.request_timeout_secs,
938 stats: if self.stats_enabled {
939 Some(ServerStats::new())
940 } else {
941 None
942 },
943 mask_error_details: self.mask_error_details,
944 logging: self.logging,
945 console_config: self.console_config,
946 lifespan: Mutex::new(Some(self.lifespan)),
947 auth_provider: self.auth_provider,
948 middleware: Arc::new(self.middleware),
949 active_requests: Mutex::new(HashMap::new()),
950 task_manager: self.task_manager,
951 pending_requests: std::sync::Arc::new(crate::bidirectional::PendingRequests::new()),
952 }
953 }
954}
955
956#[cfg(test)]
957mod tests {
958 use super::*;
959 use fastmcp_core::{McpContext, McpResult};
960 use fastmcp_protocol::{Content, Prompt, Resource, ResourceContent, Tool};
961
962 struct TestTool;
965 impl crate::ToolHandler for TestTool {
966 fn definition(&self) -> Tool {
967 Tool {
968 name: "test_tool".to_string(),
969 description: Some("a test tool".to_string()),
970 input_schema: serde_json::json!({"type": "object"}),
971 output_schema: None,
972 icon: None,
973 version: None,
974 tags: vec![],
975 annotations: None,
976 }
977 }
978 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
979 Ok(vec![Content::text("ok")])
980 }
981 }
982
983 struct TestResource;
984 impl crate::ResourceHandler for TestResource {
985 fn definition(&self) -> Resource {
986 Resource {
987 uri: "file:///test".to_string(),
988 name: "test_res".to_string(),
989 description: None,
990 mime_type: None,
991 icon: None,
992 version: None,
993 tags: vec![],
994 }
995 }
996 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
997 Ok(vec![ResourceContent {
998 uri: "file:///test".to_string(),
999 mime_type: None,
1000 text: Some("content".to_string()),
1001 blob: None,
1002 }])
1003 }
1004 }
1005
1006 struct TestPrompt;
1007 impl crate::PromptHandler for TestPrompt {
1008 fn definition(&self) -> Prompt {
1009 Prompt {
1010 name: "test_prompt".to_string(),
1011 description: None,
1012 arguments: vec![],
1013 icon: None,
1014 version: None,
1015 tags: vec![],
1016 }
1017 }
1018 fn get(
1019 &self,
1020 _ctx: &McpContext,
1021 _args: std::collections::HashMap<String, String>,
1022 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1023 Ok(vec![])
1024 }
1025 }
1026
1027 #[test]
1030 fn builder_new_sets_info() {
1031 let builder = ServerBuilder::new("my-server", "2.0.0");
1032 let server = builder.build();
1033 assert_eq!(server.info().name, "my-server");
1034 assert_eq!(server.info().version, "2.0.0");
1035 }
1036
1037 #[test]
1038 fn builder_default_has_logging_capability() {
1039 let builder = ServerBuilder::new("srv", "1.0");
1040 let server = builder.build();
1041 assert!(server.capabilities().logging.is_some());
1042 }
1043
1044 #[test]
1045 fn builder_default_has_no_tool_resource_prompt_capabilities() {
1046 let builder = ServerBuilder::new("srv", "1.0");
1047 let server = builder.build();
1048 assert!(server.capabilities().tools.is_none());
1049 assert!(server.capabilities().resources.is_none());
1050 assert!(server.capabilities().prompts.is_none());
1051 }
1052
1053 #[test]
1054 fn builder_default_stats_enabled() {
1055 let server = ServerBuilder::new("srv", "1.0").build();
1056 assert!(server.stats().is_some());
1057 }
1058
1059 #[test]
1060 fn builder_default_request_timeout() {
1061 let builder = ServerBuilder::new("srv", "1.0");
1062 assert_eq!(builder.request_timeout_secs(), DEFAULT_REQUEST_TIMEOUT_SECS);
1063 }
1064
1065 #[test]
1066 fn builder_default_error_masking_disabled() {
1067 let builder = ServerBuilder::new("srv", "1.0");
1068 assert!(!builder.is_error_masking_enabled());
1069 }
1070
1071 #[test]
1072 fn builder_default_strict_validation_disabled() {
1073 let builder = ServerBuilder::new("srv", "1.0");
1074 assert!(!builder.is_strict_input_validation_enabled());
1075 }
1076
1077 #[test]
1080 fn builder_request_timeout() {
1081 let builder = ServerBuilder::new("srv", "1.0").request_timeout(60);
1082 assert_eq!(builder.request_timeout_secs(), 60);
1083 }
1084
1085 #[test]
1086 fn builder_request_timeout_zero_disables() {
1087 let builder = ServerBuilder::new("srv", "1.0").request_timeout(0);
1088 assert_eq!(builder.request_timeout_secs(), 0);
1089 }
1090
1091 #[test]
1092 fn builder_without_stats() {
1093 let server = ServerBuilder::new("srv", "1.0").without_stats().build();
1094 assert!(server.stats().is_none());
1095 }
1096
1097 #[test]
1098 fn builder_mask_error_details() {
1099 let builder = ServerBuilder::new("srv", "1.0").mask_error_details(true);
1100 assert!(builder.is_error_masking_enabled());
1101 }
1102
1103 #[test]
1104 fn builder_strict_input_validation() {
1105 let builder = ServerBuilder::new("srv", "1.0").strict_input_validation(true);
1106 assert!(builder.is_strict_input_validation_enabled());
1107 }
1108
1109 #[test]
1110 fn builder_instructions() {
1111 let server = ServerBuilder::new("srv", "1.0")
1112 .instructions("Use this server wisely")
1113 .build();
1114 let _ = server;
1116 }
1117
1118 #[test]
1119 fn builder_log_level() {
1120 let _builder = ServerBuilder::new("srv", "1.0").log_level(Level::Debug);
1121 }
1122
1123 #[test]
1124 fn builder_log_level_filter() {
1125 let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Warn);
1126 }
1127
1128 #[test]
1129 fn builder_log_timestamps_and_targets() {
1130 let _builder = ServerBuilder::new("srv", "1.0")
1131 .log_timestamps(false)
1132 .log_targets(false);
1133 }
1134
1135 #[test]
1138 fn builder_without_banner() {
1139 let builder = ServerBuilder::new("srv", "1.0").without_banner();
1140 let config = builder.console_config();
1141 assert_eq!(config.banner_style, BannerStyle::None);
1142 }
1143
1144 #[test]
1145 fn builder_with_banner_compact() {
1146 let builder = ServerBuilder::new("srv", "1.0").with_banner(BannerStyle::Compact);
1147 let config = builder.console_config();
1148 assert_eq!(config.banner_style, BannerStyle::Compact);
1149 }
1150
1151 #[test]
1152 fn builder_plain_mode() {
1153 let builder = ServerBuilder::new("srv", "1.0").plain_mode();
1154 let _config = builder.console_config();
1155 }
1156
1157 #[test]
1160 fn builder_tool_enables_capability() {
1161 let server = ServerBuilder::new("srv", "1.0").tool(TestTool).build();
1162 assert!(server.capabilities().tools.is_some());
1163 assert!(server.has_tools());
1164 }
1165
1166 #[test]
1167 fn builder_resource_enables_capability() {
1168 let server = ServerBuilder::new("srv", "1.0")
1169 .resource(TestResource)
1170 .build();
1171 assert!(server.capabilities().resources.is_some());
1172 assert!(server.has_resources());
1173 }
1174
1175 #[test]
1176 fn builder_prompt_enables_capability() {
1177 let server = ServerBuilder::new("srv", "1.0").prompt(TestPrompt).build();
1178 assert!(server.capabilities().prompts.is_some());
1179 assert!(server.has_prompts());
1180 }
1181
1182 #[test]
1183 fn builder_all_handlers() {
1184 let server = ServerBuilder::new("srv", "1.0")
1185 .tool(TestTool)
1186 .resource(TestResource)
1187 .prompt(TestPrompt)
1188 .build();
1189 assert!(server.has_tools());
1190 assert!(server.has_resources());
1191 assert!(server.has_prompts());
1192 }
1193
1194 #[test]
1195 fn builder_no_handlers_means_no_capabilities() {
1196 let server = ServerBuilder::new("srv", "1.0").build();
1197 assert!(!server.has_tools());
1198 assert!(!server.has_resources());
1199 assert!(!server.has_prompts());
1200 }
1201
1202 #[test]
1205 fn builder_on_duplicate_default_is_warn() {
1206 let _builder = ServerBuilder::new("srv", "1.0");
1207 }
1209
1210 #[test]
1211 fn builder_on_duplicate_ignore() {
1212 let server = ServerBuilder::new("srv", "1.0")
1213 .on_duplicate(DuplicateBehavior::Ignore)
1214 .tool(TestTool)
1215 .build();
1216 assert!(server.has_tools());
1217 }
1218
1219 #[test]
1220 fn builder_on_duplicate_replace() {
1221 let server = ServerBuilder::new("srv", "1.0")
1222 .on_duplicate(DuplicateBehavior::Replace)
1223 .tool(TestTool)
1224 .build();
1225 assert!(server.has_tools());
1226 }
1227
1228 #[test]
1231 fn builder_on_startup_builds() {
1232 let server = ServerBuilder::new("srv", "1.0")
1233 .on_startup(|| -> Result<(), std::io::Error> { Ok(()) })
1234 .build();
1235 let _ = server;
1236 }
1237
1238 #[test]
1239 fn builder_on_shutdown_builds() {
1240 let server = ServerBuilder::new("srv", "1.0").on_shutdown(|| {}).build();
1241 let _ = server;
1242 }
1243
1244 #[test]
1247 fn built_server_console_config_matches_builder() {
1248 let server = ServerBuilder::new("srv", "1.0").without_banner().build();
1249 assert_eq!(server.console_config().banner_style, BannerStyle::None);
1250 }
1251
1252 #[test]
1255 fn builder_chaining_fluent_api() {
1256 let server = ServerBuilder::new("chain", "3.0")
1257 .request_timeout(120)
1258 .mask_error_details(true)
1259 .strict_input_validation(true)
1260 .without_banner()
1261 .plain_mode()
1262 .tool(TestTool)
1263 .resource(TestResource)
1264 .prompt(TestPrompt)
1265 .on_shutdown(|| {})
1266 .build();
1267
1268 assert_eq!(server.info().name, "chain");
1269 assert_eq!(server.info().version, "3.0");
1270 assert!(server.has_tools());
1271 assert!(server.has_resources());
1272 assert!(server.has_prompts());
1273 }
1274
1275 #[test]
1278 fn builder_with_console_config() {
1279 let config = ConsoleConfig::new().with_banner(BannerStyle::None);
1280 let builder = ServerBuilder::new("srv", "1.0").with_console_config(config);
1281 assert_eq!(builder.console_config().banner_style, BannerStyle::None);
1282 }
1283
1284 #[test]
1285 fn builder_with_traffic_logging() {
1286 let builder = ServerBuilder::new("srv", "1.0").with_traffic_logging(TrafficVerbosity::Full);
1287 let config = builder.console_config();
1288 assert_eq!(config.traffic_verbosity, TrafficVerbosity::Full);
1289 }
1290
1291 #[test]
1292 fn builder_with_periodic_stats() {
1293 let builder = ServerBuilder::new("srv", "1.0").with_periodic_stats(30);
1294 let config = builder.console_config();
1295 assert_eq!(config.stats_interval_secs, 30);
1296 }
1297
1298 #[test]
1299 fn builder_force_color() {
1300 let builder = ServerBuilder::new("srv", "1.0").force_color();
1301 let _config = builder.console_config();
1302 }
1304
1305 #[test]
1308 fn builder_logging_full_config() {
1309 let config = LoggingConfig {
1310 level: Level::Trace,
1311 timestamps: false,
1312 targets: false,
1313 file_line: true,
1314 };
1315 let _builder = ServerBuilder::new("srv", "1.0").logging(config);
1316 }
1317
1318 #[test]
1321 fn builder_list_page_size() {
1322 let server = ServerBuilder::new("srv", "1.0")
1323 .list_page_size(50)
1324 .tool(TestTool)
1325 .build();
1326 assert!(server.has_tools());
1327 }
1328
1329 #[test]
1332 fn builder_resource_template_enables_capability() {
1333 let template = ResourceTemplate {
1334 uri_template: "file://{path}".to_string(),
1335 name: "Template".to_string(),
1336 description: None,
1337 mime_type: None,
1338 icon: None,
1339 version: None,
1340 tags: vec![],
1341 };
1342 let server = ServerBuilder::new("srv", "1.0")
1343 .resource_template(template)
1344 .build();
1345 assert!(server.capabilities().resources.is_some());
1346 }
1347
1348 struct NoopMiddleware;
1351 impl crate::Middleware for NoopMiddleware {}
1352
1353 #[test]
1354 fn builder_middleware() {
1355 let server = ServerBuilder::new("srv", "1.0")
1356 .middleware(NoopMiddleware)
1357 .build();
1358 let _ = server;
1359 }
1360
1361 #[test]
1362 fn builder_multiple_middleware() {
1363 let server = ServerBuilder::new("srv", "1.0")
1364 .middleware(NoopMiddleware)
1365 .middleware(NoopMiddleware)
1366 .build();
1367 let _ = server;
1368 }
1369
1370 struct TestAuthProvider;
1373 impl crate::AuthProvider for TestAuthProvider {
1374 fn authenticate(
1375 &self,
1376 _ctx: &McpContext,
1377 _request: crate::auth::AuthRequest<'_>,
1378 ) -> McpResult<fastmcp_core::AuthContext> {
1379 Ok(fastmcp_core::AuthContext::with_subject("test-user"))
1380 }
1381 }
1382
1383 #[test]
1384 fn builder_auth_provider() {
1385 let server = ServerBuilder::new("srv", "1.0")
1386 .auth_provider(TestAuthProvider)
1387 .build();
1388 let _ = server;
1389 }
1390
1391 #[test]
1394 fn builder_auto_mask_errors() {
1395 let builder = ServerBuilder::new("srv", "1.0").auto_mask_errors();
1397 assert!(!builder.is_error_masking_enabled());
1399 }
1400
1401 struct DupTool(&'static str);
1404 impl crate::ToolHandler for DupTool {
1405 fn definition(&self) -> Tool {
1406 Tool {
1407 name: self.0.to_string(),
1408 description: None,
1409 input_schema: serde_json::json!({"type": "object"}),
1410 output_schema: None,
1411 icon: None,
1412 version: None,
1413 tags: vec![],
1414 annotations: None,
1415 }
1416 }
1417 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1418 Ok(vec![Content::text("ok")])
1419 }
1420 }
1421
1422 #[test]
1423 fn builder_on_duplicate_error_logs_but_continues() {
1424 let server = ServerBuilder::new("srv", "1.0")
1427 .on_duplicate(DuplicateBehavior::Error)
1428 .tool(DupTool("dup"))
1429 .tool(DupTool("dup")) .build();
1431 assert!(server.has_tools());
1432 }
1433
1434 #[test]
1437 fn builder_mount_with_prefix() {
1438 let source = ServerBuilder::new("sub", "1.0")
1439 .tool(TestTool)
1440 .resource(TestResource)
1441 .prompt(TestPrompt)
1442 .build();
1443
1444 let main = ServerBuilder::new("main", "1.0")
1445 .mount(source, Some("sub"))
1446 .build();
1447
1448 assert!(main.has_tools());
1449 assert!(main.has_resources());
1450 assert!(main.has_prompts());
1451 }
1452
1453 #[test]
1454 fn builder_mount_without_prefix() {
1455 let source = ServerBuilder::new("sub", "1.0").tool(TestTool).build();
1456
1457 let main = ServerBuilder::new("main", "1.0")
1458 .mount(source, None)
1459 .build();
1460
1461 assert!(main.has_tools());
1462 }
1463
1464 #[test]
1465 fn builder_mount_tools_only() {
1466 let source = ServerBuilder::new("sub", "1.0")
1467 .tool(TestTool)
1468 .resource(TestResource)
1469 .prompt(TestPrompt)
1470 .build();
1471
1472 let main = ServerBuilder::new("main", "1.0")
1473 .mount_tools(source, Some("sub"))
1474 .build();
1475
1476 assert!(main.has_tools());
1477 assert!(!main.has_resources());
1479 assert!(!main.has_prompts());
1480 }
1481
1482 #[test]
1483 fn builder_mount_resources_only() {
1484 let source = ServerBuilder::new("sub", "1.0")
1485 .tool(TestTool)
1486 .resource(TestResource)
1487 .prompt(TestPrompt)
1488 .build();
1489
1490 let main = ServerBuilder::new("main", "1.0")
1491 .mount_resources(source, Some("data"))
1492 .build();
1493
1494 assert!(!main.has_tools());
1495 assert!(main.has_resources());
1496 assert!(!main.has_prompts());
1497 }
1498
1499 #[test]
1500 fn builder_mount_prompts_only() {
1501 let source = ServerBuilder::new("sub", "1.0")
1502 .tool(TestTool)
1503 .resource(TestResource)
1504 .prompt(TestPrompt)
1505 .build();
1506
1507 let main = ServerBuilder::new("main", "1.0")
1508 .mount_prompts(source, Some("tmpl"))
1509 .build();
1510
1511 assert!(!main.has_tools());
1512 assert!(!main.has_resources());
1513 assert!(main.has_prompts());
1514 }
1515
1516 #[test]
1517 fn builder_mount_empty_server() {
1518 let source = ServerBuilder::new("empty", "1.0").build();
1519
1520 let main = ServerBuilder::new("main", "1.0")
1521 .mount(source, Some("empty"))
1522 .build();
1523
1524 assert!(!main.has_tools());
1525 assert!(!main.has_resources());
1526 assert!(!main.has_prompts());
1527 }
1528
1529 #[test]
1532 fn builder_proxy_with_catalog() {
1533 use crate::proxy::{ProxyCatalog, ProxyClient};
1534
1535 struct DummyBackend;
1536 impl crate::proxy::ProxyBackend for DummyBackend {
1537 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
1538 Ok(vec![])
1539 }
1540 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
1541 Ok(vec![])
1542 }
1543 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
1544 Ok(vec![])
1545 }
1546 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
1547 Ok(vec![])
1548 }
1549 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
1550 Ok(vec![])
1551 }
1552 fn call_tool_with_progress(
1553 &mut self,
1554 _: &str,
1555 _: serde_json::Value,
1556 _: crate::proxy::ProgressCallback<'_>,
1557 ) -> McpResult<Vec<Content>> {
1558 Ok(vec![])
1559 }
1560 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
1561 Ok(vec![])
1562 }
1563 fn get_prompt(
1564 &mut self,
1565 _: &str,
1566 _: std::collections::HashMap<String, String>,
1567 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1568 Ok(vec![])
1569 }
1570 }
1571
1572 let client = ProxyClient::from_backend(DummyBackend);
1573 let catalog = ProxyCatalog {
1574 tools: vec![Tool {
1575 name: "proxy-tool".to_string(),
1576 description: None,
1577 input_schema: serde_json::json!({}),
1578 output_schema: None,
1579 icon: None,
1580 version: None,
1581 tags: vec![],
1582 annotations: None,
1583 }],
1584 ..ProxyCatalog::default()
1585 };
1586
1587 let server = ServerBuilder::new("srv", "1.0")
1588 .proxy(client, catalog)
1589 .build();
1590 assert!(server.has_tools());
1591 }
1592
1593 #[test]
1596 fn default_request_timeout_constant() {
1597 assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30);
1598 }
1599
1600 #[test]
1603 fn builder_mask_error_details_toggle() {
1604 let builder = ServerBuilder::new("srv", "1.0")
1605 .mask_error_details(true)
1606 .mask_error_details(false);
1607 assert!(!builder.is_error_masking_enabled());
1608 }
1609
1610 #[test]
1613 fn builder_strict_validation_toggle() {
1614 let builder = ServerBuilder::new("srv", "1.0")
1615 .strict_input_validation(true)
1616 .strict_input_validation(false);
1617 assert!(!builder.is_strict_input_validation_enabled());
1618 }
1619
1620 #[test]
1623 fn builder_with_task_manager_enables_capability() {
1624 use crate::tasks::TaskManager;
1625 let tm = TaskManager::new().into_shared();
1626 let server = ServerBuilder::new("srv", "1.0")
1627 .with_task_manager(tm)
1628 .build();
1629 assert!(server.capabilities().tasks.is_some());
1630 }
1631
1632 #[test]
1633 fn builder_with_task_manager_list_changed_true() {
1634 use crate::tasks::TaskManager;
1635 let tm = TaskManager::with_list_changed_notifications().into_shared();
1636 let server = ServerBuilder::new("srv", "1.0")
1637 .with_task_manager(tm)
1638 .build();
1639 let cap = server.capabilities().tasks.as_ref().unwrap();
1640 assert!(cap.list_changed);
1641 }
1642
1643 #[test]
1644 fn builder_with_task_manager_list_changed_false() {
1645 use crate::tasks::TaskManager;
1646 let tm = TaskManager::new().into_shared();
1647 let server = ServerBuilder::new("srv", "1.0")
1648 .with_task_manager(tm)
1649 .build();
1650 let cap = server.capabilities().tasks.as_ref().unwrap();
1651 assert!(!cap.list_changed);
1652 }
1653
1654 struct DupResource(&'static str);
1657 impl crate::ResourceHandler for DupResource {
1658 fn definition(&self) -> Resource {
1659 Resource {
1660 uri: format!("file:///{}", self.0),
1661 name: self.0.to_string(),
1662 description: None,
1663 mime_type: None,
1664 icon: None,
1665 version: None,
1666 tags: vec![],
1667 }
1668 }
1669 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1670 Ok(vec![])
1671 }
1672 }
1673
1674 struct DupPrompt(&'static str);
1675 impl crate::PromptHandler for DupPrompt {
1676 fn definition(&self) -> Prompt {
1677 Prompt {
1678 name: self.0.to_string(),
1679 description: None,
1680 arguments: vec![],
1681 icon: None,
1682 version: None,
1683 tags: vec![],
1684 }
1685 }
1686 fn get(
1687 &self,
1688 _ctx: &McpContext,
1689 _args: std::collections::HashMap<String, String>,
1690 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1691 Ok(vec![])
1692 }
1693 }
1694
1695 #[test]
1696 fn builder_on_duplicate_error_resource_logs_but_continues() {
1697 let server = ServerBuilder::new("srv", "1.0")
1698 .on_duplicate(DuplicateBehavior::Error)
1699 .resource(DupResource("dup"))
1700 .resource(DupResource("dup"))
1701 .build();
1702 assert!(server.has_resources());
1703 }
1704
1705 #[test]
1706 fn builder_on_duplicate_error_prompt_logs_but_continues() {
1707 let server = ServerBuilder::new("srv", "1.0")
1708 .on_duplicate(DuplicateBehavior::Error)
1709 .prompt(DupPrompt("dup"))
1710 .prompt(DupPrompt("dup"))
1711 .build();
1712 assert!(server.has_prompts());
1713 }
1714
1715 #[test]
1718 fn builder_proxy_with_resources_and_prompts() {
1719 use crate::proxy::{ProxyCatalog, ProxyClient};
1720
1721 struct DummyBackend2;
1722 impl crate::proxy::ProxyBackend for DummyBackend2 {
1723 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
1724 Ok(vec![])
1725 }
1726 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
1727 Ok(vec![])
1728 }
1729 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
1730 Ok(vec![])
1731 }
1732 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
1733 Ok(vec![])
1734 }
1735 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
1736 Ok(vec![])
1737 }
1738 fn call_tool_with_progress(
1739 &mut self,
1740 _: &str,
1741 _: serde_json::Value,
1742 _: crate::proxy::ProgressCallback<'_>,
1743 ) -> McpResult<Vec<Content>> {
1744 Ok(vec![])
1745 }
1746 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
1747 Ok(vec![])
1748 }
1749 fn get_prompt(
1750 &mut self,
1751 _: &str,
1752 _: std::collections::HashMap<String, String>,
1753 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1754 Ok(vec![])
1755 }
1756 }
1757
1758 let client = ProxyClient::from_backend(DummyBackend2);
1759 let catalog = ProxyCatalog {
1760 resources: vec![Resource {
1761 uri: "file:///proxy-res".to_string(),
1762 name: "proxy-res".to_string(),
1763 description: None,
1764 mime_type: None,
1765 icon: None,
1766 version: None,
1767 tags: vec![],
1768 }],
1769 prompts: vec![Prompt {
1770 name: "proxy-prompt".to_string(),
1771 description: None,
1772 arguments: vec![],
1773 icon: None,
1774 version: None,
1775 tags: vec![],
1776 }],
1777 resource_templates: vec![ResourceTemplate {
1778 uri_template: "db://{table}".to_string(),
1779 name: "db".to_string(),
1780 description: None,
1781 mime_type: None,
1782 icon: None,
1783 version: None,
1784 tags: vec![],
1785 }],
1786 ..ProxyCatalog::default()
1787 };
1788
1789 let server = ServerBuilder::new("srv", "1.0")
1790 .proxy(client, catalog)
1791 .build();
1792 assert!(server.has_resources());
1793 assert!(server.has_prompts());
1794 assert!(!server.has_tools());
1795 }
1796
1797 #[test]
1800 fn build_propagates_strict_validation_to_router() {
1801 let server = ServerBuilder::new("srv", "1.0")
1802 .strict_input_validation(true)
1803 .build();
1804 let router = server.into_router();
1805 assert!(router.strict_input_validation());
1806 }
1807
1808 #[test]
1809 fn build_propagates_strict_validation_false_to_router() {
1810 let server = ServerBuilder::new("srv", "1.0")
1811 .strict_input_validation(false)
1812 .build();
1813 let router = server.into_router();
1814 assert!(!router.strict_input_validation());
1815 }
1816
1817 #[test]
1820 fn builder_log_level_filter_off() {
1821 let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Off);
1822 }
1824
1825 #[test]
1828 fn builder_mount_no_op_leaves_capabilities_unchanged() {
1829 let source = ServerBuilder::new("sub", "1.0").build();
1830 let main = ServerBuilder::new("main", "1.0")
1831 .mount(source, Some("ns"))
1832 .build();
1833 assert!(!main.has_tools());
1834 assert!(!main.has_resources());
1835 assert!(!main.has_prompts());
1836 }
1837}