fastmcp_server/builder.rs
1//! Server builder for configuring MCP servers.
2
3use 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
21/// Default request timeout in seconds.
22const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
23
24/// Builder for configuring an MCP server.
25pub struct ServerBuilder {
26 info: ServerInfo,
27 capabilities: ServerCapabilities,
28 router: Router,
29 instructions: Option<String>,
30 /// Request timeout in seconds (0 = no timeout).
31 request_timeout_secs: u64,
32 /// Whether to enable statistics collection.
33 stats_enabled: bool,
34 /// Whether to mask internal error details in responses.
35 mask_error_details: bool,
36 /// Logging configuration.
37 logging: LoggingConfig,
38 /// Console configuration for rich output.
39 console_config: ConsoleConfig,
40 /// Lifecycle hooks for startup/shutdown.
41 lifespan: LifespanHooks,
42 /// Optional authentication provider.
43 auth_provider: Option<Arc<dyn AuthProvider>>,
44 /// Registered middleware.
45 middleware: Vec<Box<dyn crate::Middleware>>,
46 /// Optional task manager for background tasks (Docket/SEP-1686).
47 task_manager: Option<SharedTaskManager>,
48 /// Behavior when registering duplicate component names.
49 on_duplicate: DuplicateBehavior,
50 /// Whether to use strict input validation (reject extra properties).
51 strict_input_validation: bool,
52}
53
54impl ServerBuilder {
55 /// Creates a new server builder.
56 ///
57 /// Statistics collection is enabled by default. Use [`without_stats`](Self::without_stats)
58 /// to disable it for performance-critical scenarios.
59 ///
60 /// Console configuration defaults to environment-based settings. Use
61 /// [`with_console_config`](Self::with_console_config) for programmatic control.
62 #[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, // Disabled by default for development
78 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 /// Sets the behavior when registering duplicate component names.
90 ///
91 /// Controls what happens when a tool, resource, or prompt is registered
92 /// with a name that already exists:
93 ///
94 /// - [`DuplicateBehavior::Error`]: Fail with an error
95 /// - [`DuplicateBehavior::Warn`]: Log warning, keep original (default)
96 /// - [`DuplicateBehavior::Replace`]: Replace with new component
97 /// - [`DuplicateBehavior::Ignore`]: Silently keep original
98 ///
99 /// # Example
100 ///
101 /// ```ignore
102 /// Server::new("demo", "1.0")
103 /// .on_duplicate(DuplicateBehavior::Error) // Strict mode
104 /// .tool(handler1)
105 /// .tool(handler2) // Fails if name conflicts
106 /// .build();
107 /// ```
108 #[must_use]
109 pub fn on_duplicate(mut self, behavior: DuplicateBehavior) -> Self {
110 self.on_duplicate = behavior;
111 self
112 }
113
114 /// Sets an authentication provider.
115 #[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 /// Disables statistics collection.
122 ///
123 /// Use this for performance-critical scenarios where the overhead
124 /// of atomic operations for stats tracking is undesirable.
125 /// The overhead is minimal (typically nanoseconds per request),
126 /// so this is rarely needed.
127 #[must_use]
128 pub fn without_stats(mut self) -> Self {
129 self.stats_enabled = false;
130 self
131 }
132
133 /// Sets the request timeout in seconds.
134 ///
135 /// Set to 0 to disable timeout enforcement.
136 /// Default is 30 seconds.
137 #[must_use]
138 pub fn request_timeout(mut self, secs: u64) -> Self {
139 self.request_timeout_secs = secs;
140 self
141 }
142
143 /// Enables or disables error detail masking.
144 ///
145 /// When enabled, internal error details are hidden from client responses:
146 /// - Stack traces removed
147 /// - File paths sanitized
148 /// - Internal state not exposed
149 /// - Generic "Internal server error" message returned
150 ///
151 /// Client errors (invalid request, method not found, etc.) are preserved
152 /// since they don't contain sensitive internal details.
153 ///
154 /// Default is `false` (disabled) for development convenience.
155 ///
156 /// # Example
157 ///
158 /// ```ignore
159 /// let server = Server::new("api", "1.0")
160 /// .mask_error_details(true) // Always mask in production
161 /// .build();
162 /// ```
163 #[must_use]
164 pub fn mask_error_details(mut self, enabled: bool) -> Self {
165 self.mask_error_details = enabled;
166 self
167 }
168
169 /// Automatically masks error details based on environment.
170 ///
171 /// Masking is enabled when:
172 /// - `FASTMCP_ENV` is set to "production"
173 /// - `FASTMCP_MASK_ERRORS` is set to "true" or "1"
174 /// - The build is a release build (`cfg!(not(debug_assertions))`)
175 ///
176 /// Masking is explicitly disabled when:
177 /// - `FASTMCP_MASK_ERRORS` is set to "false" or "0"
178 ///
179 /// # Example
180 ///
181 /// ```ignore
182 /// let server = Server::new("api", "1.0")
183 /// .auto_mask_errors()
184 /// .build();
185 /// ```
186 #[must_use]
187 pub fn auto_mask_errors(mut self) -> Self {
188 // Check for explicit override first
189 if let Ok(val) = std::env::var("FASTMCP_MASK_ERRORS") {
190 match val.to_lowercase().as_str() {
191 "true" | "1" | "yes" => {
192 self.mask_error_details = true;
193 return self;
194 }
195 "false" | "0" | "no" => {
196 self.mask_error_details = false;
197 return self;
198 }
199 _ => {} // Fall through to other checks
200 }
201 }
202
203 // Check for production environment
204 if let Ok(env) = std::env::var("FASTMCP_ENV") {
205 if env.to_lowercase() == "production" {
206 self.mask_error_details = true;
207 return self;
208 }
209 }
210
211 // Default: mask in release builds, don't mask in debug builds
212 self.mask_error_details = cfg!(not(debug_assertions));
213 self
214 }
215
216 /// Returns whether error masking is enabled.
217 #[must_use]
218 pub fn is_error_masking_enabled(&self) -> bool {
219 self.mask_error_details
220 }
221
222 /// Enables or disables strict input validation.
223 ///
224 /// When enabled, tool input validation will reject any properties not
225 /// explicitly defined in the tool's input schema (enforces `additionalProperties: false`).
226 ///
227 /// When disabled (default), extra properties are allowed unless the schema
228 /// explicitly sets `additionalProperties: false`.
229 ///
230 /// # Example
231 ///
232 /// ```ignore
233 /// let server = Server::new("api", "1.0")
234 /// .strict_input_validation(true) // Reject unknown properties
235 /// .build();
236 /// ```
237 #[must_use]
238 pub fn strict_input_validation(mut self, enabled: bool) -> Self {
239 self.strict_input_validation = enabled;
240 self
241 }
242
243 /// Returns whether strict input validation is enabled.
244 #[must_use]
245 pub fn is_strict_input_validation_enabled(&self) -> bool {
246 self.strict_input_validation
247 }
248
249 /// Registers a middleware.
250 #[must_use]
251 pub fn middleware<M: crate::Middleware + 'static>(mut self, middleware: M) -> Self {
252 self.middleware.push(Box::new(middleware));
253 self
254 }
255
256 /// Registers a tool handler.
257 ///
258 /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
259 /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
260 /// an error will be logged and the tool will not be registered.
261 #[must_use]
262 pub fn tool<H: ToolHandler + 'static>(mut self, handler: H) -> Self {
263 if let Err(e) = self
264 .router
265 .add_tool_with_behavior(handler, self.on_duplicate)
266 {
267 log::error!(target: "fastmcp::builder", "Failed to register tool: {}", e);
268 } else {
269 self.capabilities.tools = Some(ToolsCapability::default());
270 }
271 self
272 }
273
274 /// Registers a resource handler.
275 ///
276 /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
277 /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
278 /// an error will be logged and the resource will not be registered.
279 #[must_use]
280 pub fn resource<H: ResourceHandler + 'static>(mut self, handler: H) -> Self {
281 if let Err(e) = self
282 .router
283 .add_resource_with_behavior(handler, self.on_duplicate)
284 {
285 log::error!(target: "fastmcp::builder", "Failed to register resource: {}", e);
286 } else {
287 self.capabilities.resources = Some(ResourcesCapability::default());
288 }
289 self
290 }
291
292 /// Registers a resource template.
293 #[must_use]
294 pub fn resource_template(mut self, template: ResourceTemplate) -> Self {
295 self.router.add_resource_template(template);
296 self.capabilities.resources = Some(ResourcesCapability::default());
297 self
298 }
299
300 /// Registers a prompt handler.
301 ///
302 /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
303 /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
304 /// an error will be logged and the prompt will not be registered.
305 #[must_use]
306 pub fn prompt<H: PromptHandler + 'static>(mut self, handler: H) -> Self {
307 if let Err(e) = self
308 .router
309 .add_prompt_with_behavior(handler, self.on_duplicate)
310 {
311 log::error!(target: "fastmcp::builder", "Failed to register prompt: {}", e);
312 } else {
313 self.capabilities.prompts = Some(PromptsCapability::default());
314 }
315 self
316 }
317
318 /// Registers proxy handlers for a remote MCP server.
319 ///
320 /// Use [`ProxyCatalog::from_client`] or [`ProxyClient::catalog`] to fetch
321 /// definitions before calling this method.
322 #[must_use]
323 pub fn proxy(mut self, client: ProxyClient, catalog: ProxyCatalog) -> Self {
324 let has_tools = !catalog.tools.is_empty();
325 let has_resources = !catalog.resources.is_empty() || !catalog.resource_templates.is_empty();
326 let has_prompts = !catalog.prompts.is_empty();
327
328 for tool in catalog.tools {
329 self.router
330 .add_tool(ProxyToolHandler::new(tool, client.clone()));
331 }
332
333 for resource in catalog.resources {
334 self.router
335 .add_resource(ProxyResourceHandler::new(resource, client.clone()));
336 }
337
338 for template in catalog.resource_templates {
339 self.router
340 .add_resource(ProxyResourceHandler::from_template(
341 template,
342 client.clone(),
343 ));
344 }
345
346 for prompt in catalog.prompts {
347 self.router
348 .add_prompt(ProxyPromptHandler::new(prompt, client.clone()));
349 }
350
351 if has_tools {
352 self.capabilities.tools = Some(ToolsCapability::default());
353 }
354 if has_resources {
355 self.capabilities.resources = Some(ResourcesCapability::default());
356 }
357 if has_prompts {
358 self.capabilities.prompts = Some(PromptsCapability::default());
359 }
360
361 self
362 }
363
364 /// Creates a proxy to an external MCP server with automatic discovery.
365 ///
366 /// This is a convenience method that combines connection, discovery, and
367 /// handler registration. The client should already be initialized (connected
368 /// to the server).
369 ///
370 /// All tools, resources, and prompts from the external server are registered
371 /// as proxy handlers with the specified prefix.
372 ///
373 /// # Example
374 ///
375 /// ```ignore
376 /// use fastmcp_client::Client;
377 ///
378 /// // Create and initialize client
379 /// let mut client = Client::new(transport)?;
380 /// client.initialize()?;
381 ///
382 /// // Create main server with proxy to external
383 /// let main = Server::new("main", "1.0")
384 /// .tool(local_tool)
385 /// .as_proxy("ext", client)? // ext/external_tool, etc.
386 /// .build();
387 /// ```
388 ///
389 /// # Errors
390 ///
391 /// Returns an error if the catalog fetch fails.
392 pub fn as_proxy(
393 mut self,
394 prefix: &str,
395 client: fastmcp_client::Client,
396 ) -> Result<Self, fastmcp_core::McpError> {
397 // Create proxy client and fetch catalog
398 let proxy_client = ProxyClient::from_client(client);
399 let catalog = proxy_client.catalog()?;
400
401 // Capture counts before consuming
402 let tool_count = catalog.tools.len();
403 let resource_count = catalog.resources.len();
404 let template_count = catalog.resource_templates.len();
405 let prompt_count = catalog.prompts.len();
406
407 let has_tools = tool_count > 0;
408 let has_resources = resource_count > 0 || template_count > 0;
409 let has_prompts = prompt_count > 0;
410
411 // Register tools with prefix
412 for tool in catalog.tools {
413 log::debug!(
414 target: "fastmcp::proxy",
415 "Registering proxied tool: {}/{}", prefix, tool.name
416 );
417 self.router.add_tool(ProxyToolHandler::with_prefix(
418 tool,
419 prefix,
420 proxy_client.clone(),
421 ));
422 }
423
424 // Register resources with prefix
425 for resource in catalog.resources {
426 log::debug!(
427 target: "fastmcp::proxy",
428 "Registering proxied resource: {}/{}", prefix, resource.uri
429 );
430 self.router.add_resource(ProxyResourceHandler::with_prefix(
431 resource,
432 prefix,
433 proxy_client.clone(),
434 ));
435 }
436
437 // Register resource templates with prefix
438 for template in catalog.resource_templates {
439 log::debug!(
440 target: "fastmcp::proxy",
441 "Registering proxied template: {}/{}", prefix, template.uri_template
442 );
443 self.router
444 .add_resource(ProxyResourceHandler::from_template_with_prefix(
445 template,
446 prefix,
447 proxy_client.clone(),
448 ));
449 }
450
451 // Register prompts with prefix
452 for prompt in catalog.prompts {
453 log::debug!(
454 target: "fastmcp::proxy",
455 "Registering proxied prompt: {}/{}", prefix, prompt.name
456 );
457 self.router.add_prompt(ProxyPromptHandler::with_prefix(
458 prompt,
459 prefix,
460 proxy_client.clone(),
461 ));
462 }
463
464 // Update capabilities
465 if has_tools {
466 self.capabilities.tools = Some(ToolsCapability::default());
467 }
468 if has_resources {
469 self.capabilities.resources = Some(ResourcesCapability::default());
470 }
471 if has_prompts {
472 self.capabilities.prompts = Some(PromptsCapability::default());
473 }
474
475 log::info!(
476 target: "fastmcp::proxy",
477 "Proxied {} tools, {} resources, {} templates, {} prompts with prefix '{}'",
478 tool_count,
479 resource_count,
480 template_count,
481 prompt_count,
482 prefix
483 );
484
485 Ok(self)
486 }
487
488 /// Creates a proxy to an external MCP server without a prefix.
489 ///
490 /// Similar to [`as_proxy`](Self::as_proxy), but tools/resources/prompts
491 /// keep their original names. Use this when proxying a single external
492 /// server or when you don't need namespace separation.
493 ///
494 /// # Example
495 ///
496 /// ```ignore
497 /// let main = Server::new("main", "1.0")
498 /// .as_proxy_raw(client)? // External tools appear with original names
499 /// .build();
500 /// ```
501 pub fn as_proxy_raw(
502 self,
503 client: fastmcp_client::Client,
504 ) -> Result<Self, fastmcp_core::McpError> {
505 let proxy_client = ProxyClient::from_client(client);
506 let catalog = proxy_client.catalog()?;
507 Ok(self.proxy(proxy_client, catalog))
508 }
509
510 // ─────────────────────────────────────────────────
511 // Server Composition (Mount)
512 // ─────────────────────────────────────────────────
513
514 /// Mounts another server's components into this server with an optional prefix.
515 ///
516 /// This consumes the source server and moves all its tools, resources, and prompts
517 /// into this server. Names/URIs are prefixed with `prefix/` if a prefix is provided.
518 ///
519 /// # Example
520 ///
521 /// ```ignore
522 /// let db_server = Server::new("db", "1.0")
523 /// .tool(query_tool)
524 /// .tool(insert_tool)
525 /// .build();
526 ///
527 /// let api_server = Server::new("api", "1.0")
528 /// .tool(endpoint_tool)
529 /// .build();
530 ///
531 /// let main = Server::new("main", "1.0")
532 /// .mount(db_server, Some("db")) // db/query, db/insert
533 /// .mount(api_server, Some("api")) // api/endpoint
534 /// .build();
535 /// ```
536 ///
537 /// # Prefix Rules
538 ///
539 /// - Prefixes must be alphanumeric plus underscores and hyphens
540 /// - Prefixes cannot contain slashes
541 /// - With prefix `"db"`, tool `"query"` becomes `"db/query"`
542 /// - Without prefix, names are preserved (may cause conflicts)
543 #[must_use]
544 pub fn mount(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
545 let has_tools = server.has_tools();
546 let has_resources = server.has_resources();
547 let has_prompts = server.has_prompts();
548
549 let source_router = server.into_router();
550 let result = self.router.mount(source_router, prefix);
551
552 // Log warnings if any
553 for warning in &result.warnings {
554 log::warn!(target: "fastmcp::mount", "{}", warning);
555 }
556
557 // Update capabilities based on what was mounted
558 if has_tools && result.tools > 0 {
559 self.capabilities.tools = Some(ToolsCapability::default());
560 }
561 if has_resources && (result.resources > 0 || result.resource_templates > 0) {
562 self.capabilities.resources = Some(ResourcesCapability::default());
563 }
564 if has_prompts && result.prompts > 0 {
565 self.capabilities.prompts = Some(PromptsCapability::default());
566 }
567
568 self
569 }
570
571 /// Mounts only tools from another server with an optional prefix.
572 ///
573 /// Similar to [`mount`](Self::mount), but only transfers tools, ignoring
574 /// resources and prompts.
575 ///
576 /// # Example
577 ///
578 /// ```ignore
579 /// let utils_server = Server::new("utils", "1.0")
580 /// .tool(format_tool)
581 /// .tool(parse_tool)
582 /// .resource(config_resource) // Will NOT be mounted
583 /// .build();
584 ///
585 /// let main = Server::new("main", "1.0")
586 /// .mount_tools(utils_server, Some("utils")) // Only tools
587 /// .build();
588 /// ```
589 #[must_use]
590 pub fn mount_tools(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
591 let source_router = server.into_router();
592 let result = self.router.mount_tools(source_router, prefix);
593
594 // Log warnings if any
595 for warning in &result.warnings {
596 log::warn!(target: "fastmcp::mount", "{}", warning);
597 }
598
599 // Update capabilities if tools were mounted
600 if result.tools > 0 {
601 self.capabilities.tools = Some(ToolsCapability::default());
602 }
603
604 self
605 }
606
607 /// Mounts only resources from another server with an optional prefix.
608 ///
609 /// Similar to [`mount`](Self::mount), but only transfers resources,
610 /// ignoring tools and prompts.
611 ///
612 /// # Example
613 ///
614 /// ```ignore
615 /// let data_server = Server::new("data", "1.0")
616 /// .resource(config_resource)
617 /// .resource(schema_resource)
618 /// .tool(query_tool) // Will NOT be mounted
619 /// .build();
620 ///
621 /// let main = Server::new("main", "1.0")
622 /// .mount_resources(data_server, Some("data")) // Only resources
623 /// .build();
624 /// ```
625 #[must_use]
626 pub fn mount_resources(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
627 let source_router = server.into_router();
628 let result = self.router.mount_resources(source_router, prefix);
629
630 // Log warnings if any
631 for warning in &result.warnings {
632 log::warn!(target: "fastmcp::mount", "{}", warning);
633 }
634
635 // Update capabilities if resources were mounted
636 if result.resources > 0 || result.resource_templates > 0 {
637 self.capabilities.resources = Some(ResourcesCapability::default());
638 }
639
640 self
641 }
642
643 /// Mounts only prompts from another server with an optional prefix.
644 ///
645 /// Similar to [`mount`](Self::mount), but only transfers prompts,
646 /// ignoring tools and resources.
647 ///
648 /// # Example
649 ///
650 /// ```ignore
651 /// let templates_server = Server::new("templates", "1.0")
652 /// .prompt(greeting_prompt)
653 /// .prompt(error_prompt)
654 /// .tool(format_tool) // Will NOT be mounted
655 /// .build();
656 ///
657 /// let main = Server::new("main", "1.0")
658 /// .mount_prompts(templates_server, Some("tmpl")) // Only prompts
659 /// .build();
660 /// ```
661 #[must_use]
662 pub fn mount_prompts(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
663 let source_router = server.into_router();
664 let result = self.router.mount_prompts(source_router, prefix);
665
666 // Log warnings if any
667 for warning in &result.warnings {
668 log::warn!(target: "fastmcp::mount", "{}", warning);
669 }
670
671 // Update capabilities if prompts were mounted
672 if result.prompts > 0 {
673 self.capabilities.prompts = Some(PromptsCapability::default());
674 }
675
676 self
677 }
678
679 /// Sets custom server instructions.
680 #[must_use]
681 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
682 self.instructions = Some(instructions.into());
683 self
684 }
685
686 /// Sets the log level.
687 ///
688 /// Default is read from `FASTMCP_LOG` environment variable, or `INFO` if not set.
689 #[must_use]
690 pub fn log_level(mut self, level: Level) -> Self {
691 self.logging.level = level;
692 self
693 }
694
695 /// Sets the log level from a filter.
696 #[must_use]
697 pub fn log_level_filter(mut self, filter: LevelFilter) -> Self {
698 self.logging.level = filter.to_level().unwrap_or(Level::Info);
699 self
700 }
701
702 /// Sets whether to show timestamps in logs.
703 ///
704 /// Default is `true`.
705 #[must_use]
706 pub fn log_timestamps(mut self, show: bool) -> Self {
707 self.logging.timestamps = show;
708 self
709 }
710
711 /// Sets whether to show target/module paths in logs.
712 ///
713 /// Default is `true`.
714 #[must_use]
715 pub fn log_targets(mut self, show: bool) -> Self {
716 self.logging.targets = show;
717 self
718 }
719
720 /// Sets the full logging configuration.
721 #[must_use]
722 pub fn logging(mut self, config: LoggingConfig) -> Self {
723 self.logging = config;
724 self
725 }
726
727 // ─────────────────────────────────────────────────
728 // Console Configuration
729 // ─────────────────────────────────────────────────
730
731 /// Sets the complete console configuration.
732 ///
733 /// This provides full control over all console output settings including
734 /// banner, traffic logging, periodic stats, and error formatting.
735 ///
736 /// # Example
737 ///
738 /// ```ignore
739 /// use fastmcp_console::config::{ConsoleConfig, BannerStyle};
740 ///
741 /// Server::new("demo", "1.0.0")
742 /// .with_console_config(
743 /// ConsoleConfig::new()
744 /// .with_banner(BannerStyle::Compact)
745 /// .plain_mode()
746 /// )
747 /// .build();
748 /// ```
749 #[must_use]
750 pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
751 self.console_config = config;
752 self
753 }
754
755 /// Sets the banner style.
756 ///
757 /// Controls how the startup banner is displayed.
758 /// Default is `BannerStyle::Full`.
759 #[must_use]
760 pub fn with_banner(mut self, style: BannerStyle) -> Self {
761 self.console_config = self.console_config.with_banner(style);
762 self
763 }
764
765 /// Disables the startup banner.
766 #[must_use]
767 pub fn without_banner(mut self) -> Self {
768 self.console_config = self.console_config.without_banner();
769 self
770 }
771
772 /// Enables request/response traffic logging.
773 ///
774 /// Controls the verbosity of traffic logging:
775 /// - `None`: No traffic logging (default)
776 /// - `Summary`: Method name and timing only
777 /// - `Headers`: Include metadata/headers
778 /// - `Full`: Full request/response bodies
779 #[must_use]
780 pub fn with_traffic_logging(mut self, verbosity: TrafficVerbosity) -> Self {
781 self.console_config = self.console_config.with_traffic(verbosity);
782 self
783 }
784
785 /// Enables periodic statistics display.
786 ///
787 /// When enabled, statistics will be printed to stderr at the specified
788 /// interval. Requires stats collection to be enabled (the default).
789 #[must_use]
790 pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
791 self.console_config = self.console_config.with_periodic_stats(interval_secs);
792 self
793 }
794
795 /// Forces plain text output (no colors/styling).
796 ///
797 /// Useful for CI environments, logging to files, or when running
798 /// as an MCP server where rich output might interfere with the
799 /// JSON-RPC protocol.
800 #[must_use]
801 pub fn plain_mode(mut self) -> Self {
802 self.console_config = self.console_config.plain_mode();
803 self
804 }
805
806 /// Forces color output even in non-TTY environments.
807 #[must_use]
808 pub fn force_color(mut self) -> Self {
809 self.console_config = self.console_config.force_color(true);
810 self
811 }
812
813 /// Returns a reference to the current console configuration.
814 #[must_use]
815 pub fn console_config(&self) -> &ConsoleConfig {
816 &self.console_config
817 }
818
819 // ─────────────────────────────────────────────────
820 // Lifecycle Hooks
821 // ─────────────────────────────────────────────────
822
823 /// Registers a startup hook that runs before the server starts accepting connections.
824 ///
825 /// The hook can perform initialization tasks like:
826 /// - Opening database connections
827 /// - Loading configuration files
828 /// - Initializing caches
829 ///
830 /// If the hook returns an error, the server will not start.
831 ///
832 /// # Example
833 ///
834 /// ```ignore
835 /// Server::new("demo", "1.0.0")
836 /// .on_startup(|| {
837 /// println!("Server starting up...");
838 /// Ok(())
839 /// })
840 /// .run_stdio();
841 /// ```
842 #[must_use]
843 pub fn on_startup<F, E>(mut self, hook: F) -> Self
844 where
845 F: FnOnce() -> Result<(), E> + Send + 'static,
846 E: std::error::Error + Send + Sync + 'static,
847 {
848 self.lifespan.on_startup = Some(Box::new(move || {
849 hook().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
850 }));
851 self
852 }
853
854 /// Registers a shutdown hook that runs when the server is shutting down.
855 ///
856 /// The hook can perform cleanup tasks like:
857 /// - Closing database connections
858 /// - Flushing caches
859 /// - Saving state
860 ///
861 /// Shutdown hooks are run on a best-effort basis. If the process is
862 /// forcefully terminated, hooks may not run.
863 ///
864 /// # Example
865 ///
866 /// ```ignore
867 /// Server::new("demo", "1.0.0")
868 /// .on_shutdown(|| {
869 /// println!("Server shutting down...");
870 /// })
871 /// .run_stdio();
872 /// ```
873 #[must_use]
874 pub fn on_shutdown<F>(mut self, hook: F) -> Self
875 where
876 F: FnOnce() + Send + 'static,
877 {
878 self.lifespan.on_shutdown = Some(Box::new(hook));
879 self
880 }
881
882 /// Sets a task manager for background tasks (Docket/SEP-1686).
883 ///
884 /// When a task manager is configured, the server will advertise
885 /// task capabilities and handle task-related methods.
886 ///
887 /// # Example
888 ///
889 /// ```ignore
890 /// use fastmcp_server::TaskManager;
891 ///
892 /// let task_manager = TaskManager::new();
893 /// Server::new("demo", "1.0.0")
894 /// .with_task_manager(task_manager.into_shared())
895 /// .run_stdio();
896 /// ```
897 #[must_use]
898 pub fn with_task_manager(mut self, task_manager: SharedTaskManager) -> Self {
899 self.task_manager = Some(task_manager);
900 let mut capability = TasksCapability::default();
901 if let Some(manager) = &self.task_manager {
902 capability.list_changed = manager.has_list_changed_notifications();
903 }
904 self.capabilities.tasks = Some(capability);
905 self
906 }
907
908 /// Builds the server.
909 #[must_use]
910 pub fn build(mut self) -> Server {
911 // Configure router with strict input validation setting
912 self.router
913 .set_strict_input_validation(self.strict_input_validation);
914
915 Server {
916 info: self.info,
917 capabilities: self.capabilities,
918 router: self.router,
919 instructions: self.instructions,
920 request_timeout_secs: self.request_timeout_secs,
921 stats: if self.stats_enabled {
922 Some(ServerStats::new())
923 } else {
924 None
925 },
926 mask_error_details: self.mask_error_details,
927 logging: self.logging,
928 console_config: self.console_config,
929 lifespan: Mutex::new(Some(self.lifespan)),
930 auth_provider: self.auth_provider,
931 middleware: Arc::new(self.middleware),
932 active_requests: Mutex::new(HashMap::new()),
933 task_manager: self.task_manager,
934 pending_requests: std::sync::Arc::new(crate::bidirectional::PendingRequests::new()),
935 }
936 }
937}