Skip to main content

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}