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    /// Sets the pagination page size for list methods.
144    ///
145    /// When set, list methods will return up to `page_size` items and provide an
146    /// opaque `nextCursor` for retrieving the next page. When not set (default),
147    /// list methods return all items in a single response.
148    #[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    /// Enables or disables error detail masking.
155    ///
156    /// When enabled, internal error details are hidden from client responses:
157    /// - Stack traces removed
158    /// - File paths sanitized
159    /// - Internal state not exposed
160    /// - Generic "Internal server error" message returned
161    ///
162    /// Client errors (invalid request, method not found, etc.) are preserved
163    /// since they don't contain sensitive internal details.
164    ///
165    /// Default is `false` (disabled) for development convenience.
166    ///
167    /// # Example
168    ///
169    /// ```ignore
170    /// let server = Server::new("api", "1.0")
171    ///     .mask_error_details(true)  // Always mask in production
172    ///     .build();
173    /// ```
174    #[must_use]
175    pub fn mask_error_details(mut self, enabled: bool) -> Self {
176        self.mask_error_details = enabled;
177        self
178    }
179
180    /// Automatically masks error details based on environment.
181    ///
182    /// Masking is enabled when:
183    /// - `FASTMCP_ENV` is set to "production"
184    /// - `FASTMCP_MASK_ERRORS` is set to "true" or "1"
185    /// - The build is a release build (`cfg!(not(debug_assertions))`)
186    ///
187    /// Masking is explicitly disabled when:
188    /// - `FASTMCP_MASK_ERRORS` is set to "false" or "0"
189    ///
190    /// # Example
191    ///
192    /// ```ignore
193    /// let server = Server::new("api", "1.0")
194    ///     .auto_mask_errors()
195    ///     .build();
196    /// ```
197    #[must_use]
198    pub fn auto_mask_errors(mut self) -> Self {
199        // Check for explicit override first
200        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                _ => {} // Fall through to other checks
211            }
212        }
213
214        // Check for production environment
215        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        // Default: mask in release builds, don't mask in debug builds
223        self.mask_error_details = cfg!(not(debug_assertions));
224        self
225    }
226
227    /// Returns whether error masking is enabled.
228    #[must_use]
229    pub fn is_error_masking_enabled(&self) -> bool {
230        self.mask_error_details
231    }
232
233    /// Enables or disables strict input validation.
234    ///
235    /// When enabled, tool input validation will reject any properties not
236    /// explicitly defined in the tool's input schema (enforces `additionalProperties: false`).
237    ///
238    /// When disabled (default), extra properties are allowed unless the schema
239    /// explicitly sets `additionalProperties: false`.
240    ///
241    /// # Example
242    ///
243    /// ```ignore
244    /// let server = Server::new("api", "1.0")
245    ///     .strict_input_validation(true)  // Reject unknown properties
246    ///     .build();
247    /// ```
248    #[must_use]
249    pub fn strict_input_validation(mut self, enabled: bool) -> Self {
250        self.strict_input_validation = enabled;
251        self
252    }
253
254    /// Returns whether strict input validation is enabled.
255    #[must_use]
256    pub fn is_strict_input_validation_enabled(&self) -> bool {
257        self.strict_input_validation
258    }
259
260    /// Registers a middleware.
261    #[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    /// Registers a tool handler.
268    ///
269    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
270    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
271    /// an error will be logged and the tool will not be registered.
272    #[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    /// Registers a resource handler.
286    ///
287    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
288    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
289    /// an error will be logged and the resource will not be registered.
290    #[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    /// Registers a resource template.
304    #[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    /// Registers a prompt handler.
312    ///
313    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
314    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
315    /// an error will be logged and the prompt will not be registered.
316    #[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    /// Registers proxy handlers for a remote MCP server.
330    ///
331    /// Use [`ProxyCatalog::from_client`] or [`ProxyClient::catalog`] to fetch
332    /// definitions before calling this method.
333    #[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    /// Creates a proxy to an external MCP server with automatic discovery.
376    ///
377    /// This is a convenience method that combines connection, discovery, and
378    /// handler registration. The client should already be initialized (connected
379    /// to the server).
380    ///
381    /// All tools, resources, and prompts from the external server are registered
382    /// as proxy handlers with the specified prefix.
383    ///
384    /// # Example
385    ///
386    /// ```ignore
387    /// use fastmcp_client::Client;
388    ///
389    /// // Create and initialize client
390    /// let mut client = Client::new(transport)?;
391    /// client.initialize()?;
392    ///
393    /// // Create main server with proxy to external
394    /// let main = Server::new("main", "1.0")
395    ///     .tool(local_tool)
396    ///     .as_proxy("ext", client)?    // ext/external_tool, etc.
397    ///     .build();
398    /// ```
399    ///
400    /// # Errors
401    ///
402    /// Returns an error if the catalog fetch fails.
403    pub fn as_proxy(
404        mut self,
405        prefix: &str,
406        client: fastmcp_client::Client,
407    ) -> Result<Self, fastmcp_core::McpError> {
408        // Create proxy client and fetch catalog
409        let proxy_client = ProxyClient::from_client(client);
410        let catalog = proxy_client.catalog()?;
411
412        // Capture counts before consuming
413        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        // Register tools with prefix
423        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        // Register resources with prefix
436        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        // Register resource templates with prefix
449        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        // Register prompts with prefix
463        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        // Update capabilities
476        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    /// Creates a proxy to an external MCP server without a prefix.
500    ///
501    /// Similar to [`as_proxy`](Self::as_proxy), but tools/resources/prompts
502    /// keep their original names. Use this when proxying a single external
503    /// server or when you don't need namespace separation.
504    ///
505    /// # Example
506    ///
507    /// ```ignore
508    /// let main = Server::new("main", "1.0")
509    ///     .as_proxy_raw(client)?  // External tools appear with original names
510    ///     .build();
511    /// ```
512    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    // ─────────────────────────────────────────────────
522    // Server Composition (Mount)
523    // ─────────────────────────────────────────────────
524
525    /// Mounts another server's components into this server with an optional prefix.
526    ///
527    /// This consumes the source server and moves all its tools, resources, and prompts
528    /// into this server. Names/URIs are prefixed with `prefix/` if a prefix is provided.
529    ///
530    /// # Example
531    ///
532    /// ```ignore
533    /// let db_server = Server::new("db", "1.0")
534    ///     .tool(query_tool)
535    ///     .tool(insert_tool)
536    ///     .build();
537    ///
538    /// let api_server = Server::new("api", "1.0")
539    ///     .tool(endpoint_tool)
540    ///     .build();
541    ///
542    /// let main = Server::new("main", "1.0")
543    ///     .mount(db_server, Some("db"))      // db/query, db/insert
544    ///     .mount(api_server, Some("api"))    // api/endpoint
545    ///     .build();
546    /// ```
547    ///
548    /// # Prefix Rules
549    ///
550    /// - Prefixes must be alphanumeric plus underscores and hyphens
551    /// - Prefixes cannot contain slashes
552    /// - With prefix `"db"`, tool `"query"` becomes `"db/query"`
553    /// - Without prefix, names are preserved (may cause conflicts)
554    #[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        // Log warnings if any
564        for warning in &result.warnings {
565            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
566        }
567
568        // Update capabilities based on what was mounted
569        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    /// Mounts only tools from another server with an optional prefix.
583    ///
584    /// Similar to [`mount`](Self::mount), but only transfers tools, ignoring
585    /// resources and prompts.
586    ///
587    /// # Example
588    ///
589    /// ```ignore
590    /// let utils_server = Server::new("utils", "1.0")
591    ///     .tool(format_tool)
592    ///     .tool(parse_tool)
593    ///     .resource(config_resource)  // Will NOT be mounted
594    ///     .build();
595    ///
596    /// let main = Server::new("main", "1.0")
597    ///     .mount_tools(utils_server, Some("utils"))  // Only tools
598    ///     .build();
599    /// ```
600    #[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        // Log warnings if any
606        for warning in &result.warnings {
607            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
608        }
609
610        // Update capabilities if tools were mounted
611        if result.tools > 0 {
612            self.capabilities.tools = Some(ToolsCapability::default());
613        }
614
615        self
616    }
617
618    /// Mounts only resources from another server with an optional prefix.
619    ///
620    /// Similar to [`mount`](Self::mount), but only transfers resources,
621    /// ignoring tools and prompts.
622    ///
623    /// # Example
624    ///
625    /// ```ignore
626    /// let data_server = Server::new("data", "1.0")
627    ///     .resource(config_resource)
628    ///     .resource(schema_resource)
629    ///     .tool(query_tool)  // Will NOT be mounted
630    ///     .build();
631    ///
632    /// let main = Server::new("main", "1.0")
633    ///     .mount_resources(data_server, Some("data"))  // Only resources
634    ///     .build();
635    /// ```
636    #[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        // Log warnings if any
642        for warning in &result.warnings {
643            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
644        }
645
646        // Update capabilities if resources were mounted
647        if result.resources > 0 || result.resource_templates > 0 {
648            self.capabilities.resources = Some(ResourcesCapability::default());
649        }
650
651        self
652    }
653
654    /// Mounts only prompts from another server with an optional prefix.
655    ///
656    /// Similar to [`mount`](Self::mount), but only transfers prompts,
657    /// ignoring tools and resources.
658    ///
659    /// # Example
660    ///
661    /// ```ignore
662    /// let templates_server = Server::new("templates", "1.0")
663    ///     .prompt(greeting_prompt)
664    ///     .prompt(error_prompt)
665    ///     .tool(format_tool)  // Will NOT be mounted
666    ///     .build();
667    ///
668    /// let main = Server::new("main", "1.0")
669    ///     .mount_prompts(templates_server, Some("tmpl"))  // Only prompts
670    ///     .build();
671    /// ```
672    #[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        // Log warnings if any
678        for warning in &result.warnings {
679            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
680        }
681
682        // Update capabilities if prompts were mounted
683        if result.prompts > 0 {
684            self.capabilities.prompts = Some(PromptsCapability::default());
685        }
686
687        self
688    }
689
690    /// Sets custom server instructions.
691    #[must_use]
692    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
693        self.instructions = Some(instructions.into());
694        self
695    }
696
697    /// Sets the log level.
698    ///
699    /// Default is read from `FASTMCP_LOG` environment variable, or `INFO` if not set.
700    #[must_use]
701    pub fn log_level(mut self, level: Level) -> Self {
702        self.logging.level = level;
703        self
704    }
705
706    /// Sets the log level from a filter.
707    #[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    /// Sets whether to show timestamps in logs.
714    ///
715    /// Default is `true`.
716    #[must_use]
717    pub fn log_timestamps(mut self, show: bool) -> Self {
718        self.logging.timestamps = show;
719        self
720    }
721
722    /// Sets whether to show target/module paths in logs.
723    ///
724    /// Default is `true`.
725    #[must_use]
726    pub fn log_targets(mut self, show: bool) -> Self {
727        self.logging.targets = show;
728        self
729    }
730
731    /// Sets the full logging configuration.
732    #[must_use]
733    pub fn logging(mut self, config: LoggingConfig) -> Self {
734        self.logging = config;
735        self
736    }
737
738    // ─────────────────────────────────────────────────
739    // Console Configuration
740    // ─────────────────────────────────────────────────
741
742    /// Sets the complete console configuration.
743    ///
744    /// This provides full control over all console output settings including
745    /// banner, traffic logging, periodic stats, and error formatting.
746    ///
747    /// # Example
748    ///
749    /// ```ignore
750    /// use fastmcp_console::config::{ConsoleConfig, BannerStyle};
751    ///
752    /// Server::new("demo", "1.0.0")
753    ///     .with_console_config(
754    ///         ConsoleConfig::new()
755    ///             .with_banner(BannerStyle::Compact)
756    ///             .plain_mode()
757    ///     )
758    ///     .build();
759    /// ```
760    #[must_use]
761    pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
762        self.console_config = config;
763        self
764    }
765
766    /// Sets the banner style.
767    ///
768    /// Controls how the startup banner is displayed.
769    /// Default is `BannerStyle::Full`.
770    #[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    /// Disables the startup banner.
777    #[must_use]
778    pub fn without_banner(mut self) -> Self {
779        self.console_config = self.console_config.without_banner();
780        self
781    }
782
783    /// Enables request/response traffic logging.
784    ///
785    /// Controls the verbosity of traffic logging:
786    /// - `None`: No traffic logging (default)
787    /// - `Summary`: Method name and timing only
788    /// - `Headers`: Include metadata/headers
789    /// - `Full`: Full request/response bodies
790    #[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    /// Enables periodic statistics display.
797    ///
798    /// When enabled, statistics will be printed to stderr at the specified
799    /// interval. Requires stats collection to be enabled (the default).
800    #[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    /// Forces plain text output (no colors/styling).
807    ///
808    /// Useful for CI environments, logging to files, or when running
809    /// as an MCP server where rich output might interfere with the
810    /// JSON-RPC protocol.
811    #[must_use]
812    pub fn plain_mode(mut self) -> Self {
813        self.console_config = self.console_config.plain_mode();
814        self
815    }
816
817    /// Forces color output even in non-TTY environments.
818    #[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    /// Returns a reference to the current console configuration.
825    #[must_use]
826    pub fn console_config(&self) -> &ConsoleConfig {
827        &self.console_config
828    }
829
830    // ─────────────────────────────────────────────────
831    // Lifecycle Hooks
832    // ─────────────────────────────────────────────────
833
834    /// Registers a startup hook that runs before the server starts accepting connections.
835    ///
836    /// The hook can perform initialization tasks like:
837    /// - Opening database connections
838    /// - Loading configuration files
839    /// - Initializing caches
840    ///
841    /// If the hook returns an error, the server will not start.
842    ///
843    /// # Example
844    ///
845    /// ```ignore
846    /// Server::new("demo", "1.0.0")
847    ///     .on_startup(|| {
848    ///         println!("Server starting up...");
849    ///         Ok(())
850    ///     })
851    ///     .run_stdio();
852    /// ```
853    #[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    /// Registers a shutdown hook that runs when the server is shutting down.
866    ///
867    /// The hook can perform cleanup tasks like:
868    /// - Closing database connections
869    /// - Flushing caches
870    /// - Saving state
871    ///
872    /// Shutdown hooks are run on a best-effort basis. If the process is
873    /// forcefully terminated, hooks may not run.
874    ///
875    /// # Example
876    ///
877    /// ```ignore
878    /// Server::new("demo", "1.0.0")
879    ///     .on_shutdown(|| {
880    ///         println!("Server shutting down...");
881    ///     })
882    ///     .run_stdio();
883    /// ```
884    #[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    /// Sets a task manager for background tasks (Docket/SEP-1686).
894    ///
895    /// When a task manager is configured, the server will advertise
896    /// task capabilities and handle task-related methods.
897    ///
898    /// # Example
899    ///
900    /// ```ignore
901    /// use fastmcp_server::TaskManager;
902    ///
903    /// let task_manager = TaskManager::new();
904    /// Server::new("demo", "1.0.0")
905    ///     .with_task_manager(task_manager.into_shared())
906    ///     .run_stdio();
907    /// ```
908    #[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    /// Returns the current request timeout.
920    #[cfg(test)]
921    fn request_timeout_secs(&self) -> u64 {
922        self.request_timeout_secs
923    }
924
925    /// Builds the server.
926    #[must_use]
927    pub fn build(mut self) -> Server {
928        // Configure router with strict input validation setting
929        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    // ── Stub handlers ────────────────────────────────────────────────
963
964    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    // ── Builder defaults ─────────────────────────────────────────────
1028
1029    #[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    // ── Fluent API setters ───────────────────────────────────────────
1078
1079    #[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        // instructions stored internally - verify build succeeds
1115        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    // ── Console configuration ────────────────────────────────────────
1136
1137    #[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    // ── Handler registration ─────────────────────────────────────────
1158
1159    #[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    // ── Duplicate behavior ───────────────────────────────────────────
1203
1204    #[test]
1205    fn builder_on_duplicate_default_is_warn() {
1206        let _builder = ServerBuilder::new("srv", "1.0");
1207        // DuplicateBehavior::default() is Warn - builder should use that
1208    }
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    // ── Lifecycle hooks ──────────────────────────────────────────────
1229
1230    #[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    // ── Console config on built server ───────────────────────────────
1245
1246    #[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    // ── Chaining ─────────────────────────────────────────────────────
1253
1254    #[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    // ── Console configuration extended ─────────────────────────────
1276
1277    #[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        // Just verify the chain completes without panic
1303    }
1304
1305    // ── Logging config ─────────────────────────────────────────────
1306
1307    #[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    // ── List page size ─────────────────────────────────────────────
1319
1320    #[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    // ── Resource template ──────────────────────────────────────────
1330
1331    #[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    // ── Middleware ──────────────────────────────────────────────────
1349
1350    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    // ── Auth provider ──────────────────────────────────────────────
1371
1372    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    // ── auto_mask_errors ───────────────────────────────────────────
1392
1393    #[test]
1394    fn builder_auto_mask_errors() {
1395        // In debug builds (test mode), auto_mask_errors defaults to false
1396        let builder = ServerBuilder::new("srv", "1.0").auto_mask_errors();
1397        // In debug_assertions mode, masking should be disabled
1398        assert!(!builder.is_error_masking_enabled());
1399    }
1400
1401    // ── Duplicate behavior error ───────────────────────────────────
1402
1403    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        // With DuplicateBehavior::Error, duplicate registration logs error
1425        // but builder doesn't panic
1426        let server = ServerBuilder::new("srv", "1.0")
1427            .on_duplicate(DuplicateBehavior::Error)
1428            .tool(DupTool("dup"))
1429            .tool(DupTool("dup")) // duplicate - will log error
1430            .build();
1431        assert!(server.has_tools());
1432    }
1433
1434    // ── Mount ──────────────────────────────────────────────────────
1435
1436    #[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        // Resources and prompts should NOT be mounted
1478        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    // ── Proxy registration ─────────────────────────────────────────
1530
1531    #[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    // ── DEFAULT_REQUEST_TIMEOUT_SECS constant ──────────────────────
1594
1595    #[test]
1596    fn default_request_timeout_constant() {
1597        assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30);
1598    }
1599
1600    // ── mask_error_details toggling ────────────────────────────────
1601
1602    #[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    // ── strict_input_validation toggling ────────────────────────────
1611
1612    #[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    // ── Task manager ──────────────────────────────────────────────────
1621
1622    #[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    // ── Duplicate behavior for resources and prompts ─────────────────
1655
1656    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    // ── Proxy with resources and prompts ─────────────────────────────
1716
1717    #[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    // ── Build propagates strict validation to router ─────────────────
1798
1799    #[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    // ── log_level_filter with Off defaults to Info ───────────────────
1818
1819    #[test]
1820    fn builder_log_level_filter_off() {
1821        let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Off);
1822        // LevelFilter::Off.to_level() is None, so logging.level defaults to Info
1823    }
1824
1825    // ── mount does not update capabilities when nothing mounted ──────
1826
1827    #[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}