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, HttpServerConfig, LifespanHooks, LoggingConfig, PromptHandler,
18    ProxyCatalog, 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    /// HTTP server configuration (paths, max_connections, CORS).
53    http_config: HttpServerConfig,
54}
55
56impl ServerBuilder {
57    /// Creates a new server builder.
58    ///
59    /// Statistics collection is enabled by default. Use [`without_stats`](Self::without_stats)
60    /// to disable it for performance-critical scenarios.
61    ///
62    /// Console configuration defaults to environment-based settings. Use
63    /// [`with_console_config`](Self::with_console_config) for programmatic control.
64    #[must_use]
65    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
66        Self {
67            info: ServerInfo {
68                name: name.into(),
69                version: version.into(),
70            },
71            capabilities: ServerCapabilities {
72                logging: Some(LoggingCapability::default()),
73                ..ServerCapabilities::default()
74            },
75            router: Router::new(),
76            instructions: None,
77            request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
78            stats_enabled: true,
79            mask_error_details: false, // Disabled by default for development
80            logging: LoggingConfig::from_env(),
81            console_config: ConsoleConfig::from_env(),
82            lifespan: LifespanHooks::default(),
83            auth_provider: None,
84            middleware: Vec::new(),
85            task_manager: None,
86            on_duplicate: DuplicateBehavior::default(),
87            strict_input_validation: false,
88            http_config: HttpServerConfig::default(),
89        }
90    }
91
92    /// Sets the behavior when registering duplicate component names.
93    ///
94    /// Controls what happens when a tool, resource, or prompt is registered
95    /// with a name that already exists:
96    ///
97    /// - [`DuplicateBehavior::Error`]: Fail with an error
98    /// - [`DuplicateBehavior::Warn`]: Log warning, keep original (default)
99    /// - [`DuplicateBehavior::Replace`]: Replace with new component
100    /// - [`DuplicateBehavior::Ignore`]: Silently keep original
101    ///
102    /// # Example
103    ///
104    /// ```ignore
105    /// Server::new("demo", "1.0")
106    ///     .on_duplicate(DuplicateBehavior::Error)  // Strict mode
107    ///     .tool(handler1)
108    ///     .tool(handler2)  // Fails if name conflicts
109    ///     .build();
110    /// ```
111    #[must_use]
112    pub fn on_duplicate(mut self, behavior: DuplicateBehavior) -> Self {
113        self.on_duplicate = behavior;
114        self
115    }
116
117    /// Sets an authentication provider.
118    #[must_use]
119    pub fn auth_provider<P: AuthProvider + 'static>(mut self, provider: P) -> Self {
120        self.auth_provider = Some(Arc::new(provider));
121        self
122    }
123
124    /// Disables statistics collection.
125    ///
126    /// Use this for performance-critical scenarios where the overhead
127    /// of atomic operations for stats tracking is undesirable.
128    /// The overhead is minimal (typically nanoseconds per request),
129    /// so this is rarely needed.
130    #[must_use]
131    pub fn without_stats(mut self) -> Self {
132        self.stats_enabled = false;
133        self
134    }
135
136    /// Sets the request timeout in seconds.
137    ///
138    /// Set to 0 to disable timeout enforcement.
139    /// Default is 30 seconds.
140    #[must_use]
141    pub fn request_timeout(mut self, secs: u64) -> Self {
142        self.request_timeout_secs = secs;
143        self
144    }
145
146    /// Sets the pagination page size for list methods.
147    ///
148    /// When set, list methods will return up to `page_size` items and provide an
149    /// opaque `nextCursor` for retrieving the next page. When not set (default),
150    /// list methods return all items in a single response.
151    #[must_use]
152    pub fn list_page_size(mut self, page_size: usize) -> Self {
153        self.router.set_list_page_size(Some(page_size));
154        self
155    }
156
157    /// Enables or disables error detail masking.
158    ///
159    /// When enabled, internal error details are hidden from client responses:
160    /// - Stack traces removed
161    /// - File paths sanitized
162    /// - Internal state not exposed
163    /// - Generic "Internal server error" message returned
164    ///
165    /// Client errors (invalid request, method not found, etc.) are preserved
166    /// since they don't contain sensitive internal details.
167    ///
168    /// Default is `false` (disabled) for development convenience.
169    ///
170    /// # Example
171    ///
172    /// ```ignore
173    /// let server = Server::new("api", "1.0")
174    ///     .mask_error_details(true)  // Always mask in production
175    ///     .build();
176    /// ```
177    #[must_use]
178    pub fn mask_error_details(mut self, enabled: bool) -> Self {
179        self.mask_error_details = enabled;
180        self
181    }
182
183    /// Automatically masks error details based on environment.
184    ///
185    /// Masking is enabled when:
186    /// - `FASTMCP_ENV` is set to "production"
187    /// - `FASTMCP_MASK_ERRORS` is set to "true" or "1"
188    /// - The build is a release build (`cfg!(not(debug_assertions))`)
189    ///
190    /// Masking is explicitly disabled when:
191    /// - `FASTMCP_MASK_ERRORS` is set to "false" or "0"
192    ///
193    /// # Example
194    ///
195    /// ```ignore
196    /// let server = Server::new("api", "1.0")
197    ///     .auto_mask_errors()
198    ///     .build();
199    /// ```
200    #[must_use]
201    pub fn auto_mask_errors(mut self) -> Self {
202        // Check for explicit override first
203        if let Ok(val) = std::env::var("FASTMCP_MASK_ERRORS") {
204            match val.to_lowercase().as_str() {
205                "true" | "1" | "yes" => {
206                    self.mask_error_details = true;
207                    return self;
208                }
209                "false" | "0" | "no" => {
210                    self.mask_error_details = false;
211                    return self;
212                }
213                _ => {} // Fall through to other checks
214            }
215        }
216
217        // Check for production environment
218        if let Ok(env) = std::env::var("FASTMCP_ENV") {
219            if env.to_lowercase() == "production" {
220                self.mask_error_details = true;
221                return self;
222            }
223        }
224
225        // Default: mask in release builds, don't mask in debug builds
226        self.mask_error_details = cfg!(not(debug_assertions));
227        self
228    }
229
230    /// Returns whether error masking is enabled.
231    #[must_use]
232    pub fn is_error_masking_enabled(&self) -> bool {
233        self.mask_error_details
234    }
235
236    /// Enables or disables strict input validation.
237    ///
238    /// When enabled, tool input validation will reject any properties not
239    /// explicitly defined in the tool's input schema (enforces `additionalProperties: false`).
240    ///
241    /// When disabled (default), extra properties are allowed unless the schema
242    /// explicitly sets `additionalProperties: false`.
243    ///
244    /// # Example
245    ///
246    /// ```ignore
247    /// let server = Server::new("api", "1.0")
248    ///     .strict_input_validation(true)  // Reject unknown properties
249    ///     .build();
250    /// ```
251    #[must_use]
252    pub fn strict_input_validation(mut self, enabled: bool) -> Self {
253        self.strict_input_validation = enabled;
254        self
255    }
256
257    /// Returns whether strict input validation is enabled.
258    #[must_use]
259    pub fn is_strict_input_validation_enabled(&self) -> bool {
260        self.strict_input_validation
261    }
262
263    /// Sets the HTTP server configuration (paths, max connections, CORS).
264    ///
265    /// This configuration is used by [`Server::run_http`](crate::Server::run_http)
266    /// and related HTTP methods.
267    ///
268    /// # Example
269    ///
270    /// ```ignore
271    /// use fastmcp_server::HttpServerConfig;
272    ///
273    /// Server::new("demo", "1.0")
274    ///     .http_config(HttpServerConfig::new().mcp_path("/api/mcp").max_connections(128))
275    ///     .build();
276    /// ```
277    #[must_use]
278    pub fn http_config(mut self, config: HttpServerConfig) -> Self {
279        self.http_config = config;
280        self
281    }
282
283    /// Registers a middleware.
284    #[must_use]
285    pub fn middleware<M: crate::Middleware + 'static>(mut self, middleware: M) -> Self {
286        self.middleware.push(Box::new(middleware));
287        self
288    }
289
290    /// Registers a tool handler.
291    ///
292    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
293    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
294    /// an error will be logged and the tool will not be registered.
295    #[must_use]
296    pub fn tool<H: ToolHandler + 'static>(mut self, handler: H) -> Self {
297        if let Err(e) = self
298            .router
299            .add_tool_with_behavior(handler, self.on_duplicate)
300        {
301            log::error!(target: "fastmcp_rust::builder", "Failed to register tool: {}", e);
302        } else {
303            self.capabilities.tools = Some(ToolsCapability::default());
304        }
305        self
306    }
307
308    /// Registers a resource handler.
309    ///
310    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
311    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
312    /// an error will be logged and the resource will not be registered.
313    #[must_use]
314    pub fn resource<H: ResourceHandler + 'static>(mut self, handler: H) -> Self {
315        if let Err(e) = self
316            .router
317            .add_resource_with_behavior(handler, self.on_duplicate)
318        {
319            log::error!(target: "fastmcp_rust::builder", "Failed to register resource: {}", e);
320        } else {
321            self.capabilities.resources = Some(ResourcesCapability::default());
322        }
323        self
324    }
325
326    /// Registers a resource template.
327    #[must_use]
328    pub fn resource_template(mut self, template: ResourceTemplate) -> Self {
329        self.router.add_resource_template(template);
330        self.capabilities.resources = Some(ResourcesCapability::default());
331        self
332    }
333
334    /// Registers a prompt handler.
335    ///
336    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
337    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
338    /// an error will be logged and the prompt will not be registered.
339    #[must_use]
340    pub fn prompt<H: PromptHandler + 'static>(mut self, handler: H) -> Self {
341        if let Err(e) = self
342            .router
343            .add_prompt_with_behavior(handler, self.on_duplicate)
344        {
345            log::error!(target: "fastmcp_rust::builder", "Failed to register prompt: {}", e);
346        } else {
347            self.capabilities.prompts = Some(PromptsCapability::default());
348        }
349        self
350    }
351
352    /// Registers proxy handlers for a remote MCP server.
353    ///
354    /// Use [`ProxyCatalog::from_client`] or [`ProxyClient::catalog`] to fetch
355    /// definitions before calling this method.
356    #[must_use]
357    pub fn proxy(mut self, client: ProxyClient, catalog: ProxyCatalog) -> Self {
358        let has_tools = !catalog.tools.is_empty();
359        let has_resources = !catalog.resources.is_empty() || !catalog.resource_templates.is_empty();
360        let has_prompts = !catalog.prompts.is_empty();
361
362        for tool in catalog.tools {
363            self.router
364                .add_tool(ProxyToolHandler::new(tool, client.clone()));
365        }
366
367        for resource in catalog.resources {
368            self.router
369                .add_resource(ProxyResourceHandler::new(resource, client.clone()));
370        }
371
372        for template in catalog.resource_templates {
373            self.router
374                .add_resource(ProxyResourceHandler::from_template(
375                    template,
376                    client.clone(),
377                ));
378        }
379
380        for prompt in catalog.prompts {
381            self.router
382                .add_prompt(ProxyPromptHandler::new(prompt, client.clone()));
383        }
384
385        if has_tools {
386            self.capabilities.tools = Some(ToolsCapability::default());
387        }
388        if has_resources {
389            self.capabilities.resources = Some(ResourcesCapability::default());
390        }
391        if has_prompts {
392            self.capabilities.prompts = Some(PromptsCapability::default());
393        }
394
395        self
396    }
397
398    /// Creates a proxy to an external MCP server with automatic discovery.
399    ///
400    /// This is a convenience method that combines connection, discovery, and
401    /// handler registration. The client should already be initialized (connected
402    /// to the server).
403    ///
404    /// All tools, resources, and prompts from the external server are registered
405    /// as proxy handlers with the specified prefix.
406    ///
407    /// # Example
408    ///
409    /// ```ignore
410    /// use fastmcp_client::Client;
411    ///
412    /// // Create and initialize client
413    /// let mut client = Client::new(transport)?;
414    /// client.initialize()?;
415    ///
416    /// // Create main server with proxy to external
417    /// let main = Server::new("main", "1.0")
418    ///     .tool(local_tool)
419    ///     .as_proxy("ext", client)?    // ext/external_tool, etc.
420    ///     .build();
421    /// ```
422    ///
423    /// # Errors
424    ///
425    /// Returns an error if the catalog fetch fails.
426    pub fn as_proxy(
427        mut self,
428        prefix: &str,
429        client: fastmcp_client::Client,
430    ) -> Result<Self, fastmcp_core::McpError> {
431        // Create proxy client and fetch catalog
432        let proxy_client = ProxyClient::from_client(client);
433        let catalog = proxy_client.catalog()?;
434
435        // Capture counts before consuming
436        let tool_count = catalog.tools.len();
437        let resource_count = catalog.resources.len();
438        let template_count = catalog.resource_templates.len();
439        let prompt_count = catalog.prompts.len();
440
441        let has_tools = tool_count > 0;
442        let has_resources = resource_count > 0 || template_count > 0;
443        let has_prompts = prompt_count > 0;
444
445        // Register tools with prefix
446        for tool in catalog.tools {
447            log::debug!(
448                target: "fastmcp_rust::proxy",
449                "Registering proxied tool: {}/{}", prefix, tool.name
450            );
451            self.router.add_tool(ProxyToolHandler::with_prefix(
452                tool,
453                prefix,
454                proxy_client.clone(),
455            ));
456        }
457
458        // Register resources with prefix
459        for resource in catalog.resources {
460            log::debug!(
461                target: "fastmcp_rust::proxy",
462                "Registering proxied resource: {}/{}", prefix, resource.uri
463            );
464            self.router.add_resource(ProxyResourceHandler::with_prefix(
465                resource,
466                prefix,
467                proxy_client.clone(),
468            ));
469        }
470
471        // Register resource templates with prefix
472        for template in catalog.resource_templates {
473            log::debug!(
474                target: "fastmcp_rust::proxy",
475                "Registering proxied template: {}/{}", prefix, template.uri_template
476            );
477            self.router
478                .add_resource(ProxyResourceHandler::from_template_with_prefix(
479                    template,
480                    prefix,
481                    proxy_client.clone(),
482                ));
483        }
484
485        // Register prompts with prefix
486        for prompt in catalog.prompts {
487            log::debug!(
488                target: "fastmcp_rust::proxy",
489                "Registering proxied prompt: {}/{}", prefix, prompt.name
490            );
491            self.router.add_prompt(ProxyPromptHandler::with_prefix(
492                prompt,
493                prefix,
494                proxy_client.clone(),
495            ));
496        }
497
498        // Update capabilities
499        if has_tools {
500            self.capabilities.tools = Some(ToolsCapability::default());
501        }
502        if has_resources {
503            self.capabilities.resources = Some(ResourcesCapability::default());
504        }
505        if has_prompts {
506            self.capabilities.prompts = Some(PromptsCapability::default());
507        }
508
509        log::info!(
510            target: "fastmcp_rust::proxy",
511            "Proxied {} tools, {} resources, {} templates, {} prompts with prefix '{}'",
512            tool_count,
513            resource_count,
514            template_count,
515            prompt_count,
516            prefix
517        );
518
519        Ok(self)
520    }
521
522    /// Creates a proxy to an external MCP server without a prefix.
523    ///
524    /// Similar to [`as_proxy`](Self::as_proxy), but tools/resources/prompts
525    /// keep their original names. Use this when proxying a single external
526    /// server or when you don't need namespace separation.
527    ///
528    /// # Example
529    ///
530    /// ```ignore
531    /// let main = Server::new("main", "1.0")
532    ///     .as_proxy_raw(client)?  // External tools appear with original names
533    ///     .build();
534    /// ```
535    pub fn as_proxy_raw(
536        self,
537        client: fastmcp_client::Client,
538    ) -> Result<Self, fastmcp_core::McpError> {
539        let proxy_client = ProxyClient::from_client(client);
540        let catalog = proxy_client.catalog()?;
541        Ok(self.proxy(proxy_client, catalog))
542    }
543
544    // ─────────────────────────────────────────────────
545    // Server Composition (Mount)
546    // ─────────────────────────────────────────────────
547
548    /// Mounts another server's components into this server with an optional prefix.
549    ///
550    /// This consumes the source server and moves all its tools, resources, and prompts
551    /// into this server. Names/URIs are prefixed with `prefix/` if a prefix is provided.
552    ///
553    /// # Example
554    ///
555    /// ```ignore
556    /// let db_server = Server::new("db", "1.0")
557    ///     .tool(query_tool)
558    ///     .tool(insert_tool)
559    ///     .build();
560    ///
561    /// let api_server = Server::new("api", "1.0")
562    ///     .tool(endpoint_tool)
563    ///     .build();
564    ///
565    /// let main = Server::new("main", "1.0")
566    ///     .mount(db_server, Some("db"))      // db/query, db/insert
567    ///     .mount(api_server, Some("api"))    // api/endpoint
568    ///     .build();
569    /// ```
570    ///
571    /// # Prefix Rules
572    ///
573    /// - Prefixes must be alphanumeric plus underscores and hyphens
574    /// - Prefixes cannot contain slashes
575    /// - With prefix `"db"`, tool `"query"` becomes `"db/query"`
576    /// - Without prefix, names are preserved (may cause conflicts)
577    #[must_use]
578    pub fn mount(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
579        let has_tools = server.has_tools();
580        let has_resources = server.has_resources();
581        let has_prompts = server.has_prompts();
582
583        let source_router = server.into_router();
584        let result = self.router.mount(source_router, prefix);
585
586        // Log warnings if any
587        for warning in &result.warnings {
588            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
589        }
590
591        // Update capabilities based on what was mounted
592        if has_tools && result.tools > 0 {
593            self.capabilities.tools = Some(ToolsCapability::default());
594        }
595        if has_resources && (result.resources > 0 || result.resource_templates > 0) {
596            self.capabilities.resources = Some(ResourcesCapability::default());
597        }
598        if has_prompts && result.prompts > 0 {
599            self.capabilities.prompts = Some(PromptsCapability::default());
600        }
601
602        self
603    }
604
605    /// Mounts only tools from another server with an optional prefix.
606    ///
607    /// Similar to [`mount`](Self::mount), but only transfers tools, ignoring
608    /// resources and prompts.
609    ///
610    /// # Example
611    ///
612    /// ```ignore
613    /// let utils_server = Server::new("utils", "1.0")
614    ///     .tool(format_tool)
615    ///     .tool(parse_tool)
616    ///     .resource(config_resource)  // Will NOT be mounted
617    ///     .build();
618    ///
619    /// let main = Server::new("main", "1.0")
620    ///     .mount_tools(utils_server, Some("utils"))  // Only tools
621    ///     .build();
622    /// ```
623    #[must_use]
624    pub fn mount_tools(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
625        let source_router = server.into_router();
626        let result = self.router.mount_tools(source_router, prefix);
627
628        // Log warnings if any
629        for warning in &result.warnings {
630            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
631        }
632
633        // Update capabilities if tools were mounted
634        if result.tools > 0 {
635            self.capabilities.tools = Some(ToolsCapability::default());
636        }
637
638        self
639    }
640
641    /// Mounts only resources from another server with an optional prefix.
642    ///
643    /// Similar to [`mount`](Self::mount), but only transfers resources,
644    /// ignoring tools and prompts.
645    ///
646    /// # Example
647    ///
648    /// ```ignore
649    /// let data_server = Server::new("data", "1.0")
650    ///     .resource(config_resource)
651    ///     .resource(schema_resource)
652    ///     .tool(query_tool)  // Will NOT be mounted
653    ///     .build();
654    ///
655    /// let main = Server::new("main", "1.0")
656    ///     .mount_resources(data_server, Some("data"))  // Only resources
657    ///     .build();
658    /// ```
659    #[must_use]
660    pub fn mount_resources(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
661        let source_router = server.into_router();
662        let result = self.router.mount_resources(source_router, prefix);
663
664        // Log warnings if any
665        for warning in &result.warnings {
666            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
667        }
668
669        // Update capabilities if resources were mounted
670        if result.resources > 0 || result.resource_templates > 0 {
671            self.capabilities.resources = Some(ResourcesCapability::default());
672        }
673
674        self
675    }
676
677    /// Mounts only prompts from another server with an optional prefix.
678    ///
679    /// Similar to [`mount`](Self::mount), but only transfers prompts,
680    /// ignoring tools and resources.
681    ///
682    /// # Example
683    ///
684    /// ```ignore
685    /// let templates_server = Server::new("templates", "1.0")
686    ///     .prompt(greeting_prompt)
687    ///     .prompt(error_prompt)
688    ///     .tool(format_tool)  // Will NOT be mounted
689    ///     .build();
690    ///
691    /// let main = Server::new("main", "1.0")
692    ///     .mount_prompts(templates_server, Some("tmpl"))  // Only prompts
693    ///     .build();
694    /// ```
695    #[must_use]
696    pub fn mount_prompts(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
697        let source_router = server.into_router();
698        let result = self.router.mount_prompts(source_router, prefix);
699
700        // Log warnings if any
701        for warning in &result.warnings {
702            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
703        }
704
705        // Update capabilities if prompts were mounted
706        if result.prompts > 0 {
707            self.capabilities.prompts = Some(PromptsCapability::default());
708        }
709
710        self
711    }
712
713    /// Sets custom server instructions.
714    #[must_use]
715    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
716        self.instructions = Some(instructions.into());
717        self
718    }
719
720    /// Sets the log level.
721    ///
722    /// Default is read from `FASTMCP_LOG` environment variable, or `INFO` if not set.
723    #[must_use]
724    pub fn log_level(mut self, level: Level) -> Self {
725        self.logging.level = level;
726        self
727    }
728
729    /// Sets the log level from a filter.
730    #[must_use]
731    pub fn log_level_filter(mut self, filter: LevelFilter) -> Self {
732        self.logging.level = filter.to_level().unwrap_or(Level::Info);
733        self
734    }
735
736    /// Sets whether to show timestamps in logs.
737    ///
738    /// Default is `true`.
739    #[must_use]
740    pub fn log_timestamps(mut self, show: bool) -> Self {
741        self.logging.timestamps = show;
742        self
743    }
744
745    /// Sets whether to show target/module paths in logs.
746    ///
747    /// Default is `true`.
748    #[must_use]
749    pub fn log_targets(mut self, show: bool) -> Self {
750        self.logging.targets = show;
751        self
752    }
753
754    /// Sets the full logging configuration.
755    #[must_use]
756    pub fn logging(mut self, config: LoggingConfig) -> Self {
757        self.logging = config;
758        self
759    }
760
761    // ─────────────────────────────────────────────────
762    // Console Configuration
763    // ─────────────────────────────────────────────────
764
765    /// Sets the complete console configuration.
766    ///
767    /// This provides full control over all console output settings including
768    /// banner, traffic logging, periodic stats, and error formatting.
769    ///
770    /// # Example
771    ///
772    /// ```ignore
773    /// use fastmcp_console::config::{ConsoleConfig, BannerStyle};
774    ///
775    /// Server::new("demo", "1.0.0")
776    ///     .with_console_config(
777    ///         ConsoleConfig::new()
778    ///             .with_banner(BannerStyle::Compact)
779    ///             .plain_mode()
780    ///     )
781    ///     .build();
782    /// ```
783    #[must_use]
784    pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
785        self.console_config = config;
786        self
787    }
788
789    /// Sets the banner style.
790    ///
791    /// Controls how the startup banner is displayed.
792    /// Default is `BannerStyle::Full`.
793    #[must_use]
794    pub fn with_banner(mut self, style: BannerStyle) -> Self {
795        self.console_config = self.console_config.with_banner(style);
796        self
797    }
798
799    /// Disables the startup banner.
800    #[must_use]
801    pub fn without_banner(mut self) -> Self {
802        self.console_config = self.console_config.without_banner();
803        self
804    }
805
806    /// Enables request/response traffic logging.
807    ///
808    /// Controls the verbosity of traffic logging:
809    /// - `None`: No traffic logging (default)
810    /// - `Summary`: Method name and timing only
811    /// - `Headers`: Include metadata/headers
812    /// - `Full`: Full request/response bodies
813    #[must_use]
814    pub fn with_traffic_logging(mut self, verbosity: TrafficVerbosity) -> Self {
815        self.console_config = self.console_config.with_traffic(verbosity);
816        self
817    }
818
819    /// Enables periodic statistics display.
820    ///
821    /// When enabled, statistics will be printed to stderr at the specified
822    /// interval. Requires stats collection to be enabled (the default).
823    #[must_use]
824    pub fn with_periodic_stats(mut self, interval_secs: u64) -> Self {
825        self.console_config = self.console_config.with_periodic_stats(interval_secs);
826        self
827    }
828
829    /// Forces plain text output (no colors/styling).
830    ///
831    /// Useful for CI environments, logging to files, or when running
832    /// as an MCP server where rich output might interfere with the
833    /// JSON-RPC protocol.
834    #[must_use]
835    pub fn plain_mode(mut self) -> Self {
836        self.console_config = self.console_config.plain_mode();
837        self
838    }
839
840    /// Forces color output even in non-TTY environments.
841    #[must_use]
842    pub fn force_color(mut self) -> Self {
843        self.console_config = self.console_config.force_color(true);
844        self
845    }
846
847    /// Returns a reference to the current console configuration.
848    #[must_use]
849    pub fn console_config(&self) -> &ConsoleConfig {
850        &self.console_config
851    }
852
853    // ─────────────────────────────────────────────────
854    // Lifecycle Hooks
855    // ─────────────────────────────────────────────────
856
857    /// Registers a startup hook that runs before the server starts accepting connections.
858    ///
859    /// The hook can perform initialization tasks like:
860    /// - Opening database connections
861    /// - Loading configuration files
862    /// - Initializing caches
863    ///
864    /// If the hook returns an error, the server will not start.
865    ///
866    /// # Example
867    ///
868    /// ```ignore
869    /// Server::new("demo", "1.0.0")
870    ///     .on_startup(|| {
871    ///         println!("Server starting up...");
872    ///         Ok(())
873    ///     })
874    ///     .run_stdio();
875    /// ```
876    #[must_use]
877    pub fn on_startup<F, E>(mut self, hook: F) -> Self
878    where
879        F: FnOnce() -> Result<(), E> + Send + 'static,
880        E: std::error::Error + Send + Sync + 'static,
881    {
882        self.lifespan.on_startup = Some(Box::new(move || {
883            hook().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
884        }));
885        self
886    }
887
888    /// Registers a shutdown hook that runs when the server is shutting down.
889    ///
890    /// The hook can perform cleanup tasks like:
891    /// - Closing database connections
892    /// - Flushing caches
893    /// - Saving state
894    ///
895    /// Shutdown hooks are run on a best-effort basis. If the process is
896    /// forcefully terminated, hooks may not run.
897    ///
898    /// # Example
899    ///
900    /// ```ignore
901    /// Server::new("demo", "1.0.0")
902    ///     .on_shutdown(|| {
903    ///         println!("Server shutting down...");
904    ///     })
905    ///     .run_stdio();
906    /// ```
907    #[must_use]
908    pub fn on_shutdown<F>(mut self, hook: F) -> Self
909    where
910        F: FnOnce() + Send + 'static,
911    {
912        self.lifespan.on_shutdown = Some(Box::new(hook));
913        self
914    }
915
916    /// Sets a task manager for background tasks (Docket/SEP-1686).
917    ///
918    /// When a task manager is configured, the server will advertise
919    /// task capabilities and handle task-related methods.
920    ///
921    /// # Example
922    ///
923    /// ```ignore
924    /// use fastmcp_server::TaskManager;
925    ///
926    /// let task_manager = TaskManager::new();
927    /// Server::new("demo", "1.0.0")
928    ///     .with_task_manager(task_manager.into_shared())
929    ///     .run_stdio();
930    /// ```
931    #[must_use]
932    pub fn with_task_manager(mut self, task_manager: SharedTaskManager) -> Self {
933        self.task_manager = Some(task_manager);
934        let mut capability = TasksCapability::default();
935        if let Some(manager) = &self.task_manager {
936            capability.list_changed = manager.has_list_changed_notifications();
937        }
938        self.capabilities.tasks = Some(capability);
939        self
940    }
941
942    /// Returns the current request timeout.
943    #[cfg(test)]
944    fn request_timeout_secs(&self) -> u64 {
945        self.request_timeout_secs
946    }
947
948    /// Builds the server.
949    #[must_use]
950    pub fn build(mut self) -> Server {
951        // Configure router with strict input validation setting
952        self.router
953            .set_strict_input_validation(self.strict_input_validation);
954
955        Server {
956            info: self.info,
957            capabilities: self.capabilities,
958            router: self.router,
959            instructions: self.instructions,
960            request_timeout_secs: self.request_timeout_secs,
961            stats: if self.stats_enabled {
962                Some(ServerStats::new())
963            } else {
964                None
965            },
966            mask_error_details: self.mask_error_details,
967            logging: self.logging,
968            console_config: self.console_config,
969            lifespan: Mutex::new(Some(self.lifespan)),
970            auth_provider: self.auth_provider,
971            middleware: Arc::new(self.middleware),
972            active_requests: Mutex::new(HashMap::new()),
973            task_manager: self.task_manager,
974            pending_requests: std::sync::Arc::new(crate::bidirectional::PendingRequests::new()),
975            http_config: self.http_config,
976        }
977    }
978}
979
980#[cfg(test)]
981mod tests {
982    use super::*;
983    use fastmcp_core::{McpContext, McpResult};
984    use fastmcp_protocol::{Content, Prompt, Resource, ResourceContent, Tool};
985
986    // ── Stub handlers ────────────────────────────────────────────────
987
988    struct TestTool;
989    impl crate::ToolHandler for TestTool {
990        fn definition(&self) -> Tool {
991            Tool {
992                name: "test_tool".to_string(),
993                description: Some("a test tool".to_string()),
994                input_schema: serde_json::json!({"type": "object"}),
995                output_schema: None,
996                icon: None,
997                version: None,
998                tags: vec![],
999                annotations: None,
1000            }
1001        }
1002        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1003            Ok(vec![Content::text("ok")])
1004        }
1005    }
1006
1007    struct TestResource;
1008    impl crate::ResourceHandler for TestResource {
1009        fn definition(&self) -> Resource {
1010            Resource {
1011                uri: "file:///test".to_string(),
1012                name: "test_res".to_string(),
1013                description: None,
1014                mime_type: None,
1015                icon: None,
1016                version: None,
1017                tags: vec![],
1018            }
1019        }
1020        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1021            Ok(vec![ResourceContent {
1022                uri: "file:///test".to_string(),
1023                mime_type: None,
1024                text: Some("content".to_string()),
1025                blob: None,
1026            }])
1027        }
1028    }
1029
1030    struct TestPrompt;
1031    impl crate::PromptHandler for TestPrompt {
1032        fn definition(&self) -> Prompt {
1033            Prompt {
1034                name: "test_prompt".to_string(),
1035                description: None,
1036                arguments: vec![],
1037                icon: None,
1038                version: None,
1039                tags: vec![],
1040            }
1041        }
1042        fn get(
1043            &self,
1044            _ctx: &McpContext,
1045            _args: std::collections::HashMap<String, String>,
1046        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1047            Ok(vec![])
1048        }
1049    }
1050
1051    // ── Builder defaults ─────────────────────────────────────────────
1052
1053    #[test]
1054    fn builder_new_sets_info() {
1055        let builder = ServerBuilder::new("my-server", "2.0.0");
1056        let server = builder.build();
1057        assert_eq!(server.info().name, "my-server");
1058        assert_eq!(server.info().version, "2.0.0");
1059    }
1060
1061    #[test]
1062    fn builder_default_has_logging_capability() {
1063        let builder = ServerBuilder::new("srv", "1.0");
1064        let server = builder.build();
1065        assert!(server.capabilities().logging.is_some());
1066    }
1067
1068    #[test]
1069    fn builder_default_has_no_tool_resource_prompt_capabilities() {
1070        let builder = ServerBuilder::new("srv", "1.0");
1071        let server = builder.build();
1072        assert!(server.capabilities().tools.is_none());
1073        assert!(server.capabilities().resources.is_none());
1074        assert!(server.capabilities().prompts.is_none());
1075    }
1076
1077    #[test]
1078    fn builder_default_stats_enabled() {
1079        let server = ServerBuilder::new("srv", "1.0").build();
1080        assert!(server.stats().is_some());
1081    }
1082
1083    #[test]
1084    fn builder_default_request_timeout() {
1085        let builder = ServerBuilder::new("srv", "1.0");
1086        assert_eq!(builder.request_timeout_secs(), DEFAULT_REQUEST_TIMEOUT_SECS);
1087    }
1088
1089    #[test]
1090    fn builder_default_error_masking_disabled() {
1091        let builder = ServerBuilder::new("srv", "1.0");
1092        assert!(!builder.is_error_masking_enabled());
1093    }
1094
1095    #[test]
1096    fn builder_default_strict_validation_disabled() {
1097        let builder = ServerBuilder::new("srv", "1.0");
1098        assert!(!builder.is_strict_input_validation_enabled());
1099    }
1100
1101    // ── Fluent API setters ───────────────────────────────────────────
1102
1103    #[test]
1104    fn builder_request_timeout() {
1105        let builder = ServerBuilder::new("srv", "1.0").request_timeout(60);
1106        assert_eq!(builder.request_timeout_secs(), 60);
1107    }
1108
1109    #[test]
1110    fn builder_request_timeout_zero_disables() {
1111        let builder = ServerBuilder::new("srv", "1.0").request_timeout(0);
1112        assert_eq!(builder.request_timeout_secs(), 0);
1113    }
1114
1115    #[test]
1116    fn builder_without_stats() {
1117        let server = ServerBuilder::new("srv", "1.0").without_stats().build();
1118        assert!(server.stats().is_none());
1119    }
1120
1121    #[test]
1122    fn builder_mask_error_details() {
1123        let builder = ServerBuilder::new("srv", "1.0").mask_error_details(true);
1124        assert!(builder.is_error_masking_enabled());
1125    }
1126
1127    #[test]
1128    fn builder_strict_input_validation() {
1129        let builder = ServerBuilder::new("srv", "1.0").strict_input_validation(true);
1130        assert!(builder.is_strict_input_validation_enabled());
1131    }
1132
1133    #[test]
1134    fn builder_instructions() {
1135        let server = ServerBuilder::new("srv", "1.0")
1136            .instructions("Use this server wisely")
1137            .build();
1138        // instructions stored internally - verify build succeeds
1139        let _ = server;
1140    }
1141
1142    #[test]
1143    fn builder_log_level() {
1144        let _builder = ServerBuilder::new("srv", "1.0").log_level(Level::Debug);
1145    }
1146
1147    #[test]
1148    fn builder_log_level_filter() {
1149        let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Warn);
1150    }
1151
1152    #[test]
1153    fn builder_log_timestamps_and_targets() {
1154        let _builder = ServerBuilder::new("srv", "1.0")
1155            .log_timestamps(false)
1156            .log_targets(false);
1157    }
1158
1159    // ── Console configuration ────────────────────────────────────────
1160
1161    #[test]
1162    fn builder_without_banner() {
1163        let builder = ServerBuilder::new("srv", "1.0").without_banner();
1164        let config = builder.console_config();
1165        assert_eq!(config.banner_style, BannerStyle::None);
1166    }
1167
1168    #[test]
1169    fn builder_with_banner_compact() {
1170        let builder = ServerBuilder::new("srv", "1.0").with_banner(BannerStyle::Compact);
1171        let config = builder.console_config();
1172        assert_eq!(config.banner_style, BannerStyle::Compact);
1173    }
1174
1175    #[test]
1176    fn builder_plain_mode() {
1177        let builder = ServerBuilder::new("srv", "1.0").plain_mode();
1178        let _config = builder.console_config();
1179    }
1180
1181    // ── Handler registration ─────────────────────────────────────────
1182
1183    #[test]
1184    fn builder_tool_enables_capability() {
1185        let server = ServerBuilder::new("srv", "1.0").tool(TestTool).build();
1186        assert!(server.capabilities().tools.is_some());
1187        assert!(server.has_tools());
1188    }
1189
1190    #[test]
1191    fn builder_resource_enables_capability() {
1192        let server = ServerBuilder::new("srv", "1.0")
1193            .resource(TestResource)
1194            .build();
1195        assert!(server.capabilities().resources.is_some());
1196        assert!(server.has_resources());
1197    }
1198
1199    #[test]
1200    fn builder_prompt_enables_capability() {
1201        let server = ServerBuilder::new("srv", "1.0").prompt(TestPrompt).build();
1202        assert!(server.capabilities().prompts.is_some());
1203        assert!(server.has_prompts());
1204    }
1205
1206    #[test]
1207    fn builder_all_handlers() {
1208        let server = ServerBuilder::new("srv", "1.0")
1209            .tool(TestTool)
1210            .resource(TestResource)
1211            .prompt(TestPrompt)
1212            .build();
1213        assert!(server.has_tools());
1214        assert!(server.has_resources());
1215        assert!(server.has_prompts());
1216    }
1217
1218    #[test]
1219    fn builder_no_handlers_means_no_capabilities() {
1220        let server = ServerBuilder::new("srv", "1.0").build();
1221        assert!(!server.has_tools());
1222        assert!(!server.has_resources());
1223        assert!(!server.has_prompts());
1224    }
1225
1226    // ── Duplicate behavior ───────────────────────────────────────────
1227
1228    #[test]
1229    fn builder_on_duplicate_default_is_warn() {
1230        let _builder = ServerBuilder::new("srv", "1.0");
1231        // DuplicateBehavior::default() is Warn - builder should use that
1232    }
1233
1234    #[test]
1235    fn builder_on_duplicate_ignore() {
1236        let server = ServerBuilder::new("srv", "1.0")
1237            .on_duplicate(DuplicateBehavior::Ignore)
1238            .tool(TestTool)
1239            .build();
1240        assert!(server.has_tools());
1241    }
1242
1243    #[test]
1244    fn builder_on_duplicate_replace() {
1245        let server = ServerBuilder::new("srv", "1.0")
1246            .on_duplicate(DuplicateBehavior::Replace)
1247            .tool(TestTool)
1248            .build();
1249        assert!(server.has_tools());
1250    }
1251
1252    // ── Lifecycle hooks ──────────────────────────────────────────────
1253
1254    #[test]
1255    fn builder_on_startup_builds() {
1256        let server = ServerBuilder::new("srv", "1.0")
1257            .on_startup(|| -> Result<(), std::io::Error> { Ok(()) })
1258            .build();
1259        let _ = server;
1260    }
1261
1262    #[test]
1263    fn builder_on_shutdown_builds() {
1264        let server = ServerBuilder::new("srv", "1.0").on_shutdown(|| {}).build();
1265        let _ = server;
1266    }
1267
1268    // ── Console config on built server ───────────────────────────────
1269
1270    #[test]
1271    fn built_server_console_config_matches_builder() {
1272        let server = ServerBuilder::new("srv", "1.0").without_banner().build();
1273        assert_eq!(server.console_config().banner_style, BannerStyle::None);
1274    }
1275
1276    // ── Chaining ─────────────────────────────────────────────────────
1277
1278    #[test]
1279    fn builder_chaining_fluent_api() {
1280        let server = ServerBuilder::new("chain", "3.0")
1281            .request_timeout(120)
1282            .mask_error_details(true)
1283            .strict_input_validation(true)
1284            .without_banner()
1285            .plain_mode()
1286            .tool(TestTool)
1287            .resource(TestResource)
1288            .prompt(TestPrompt)
1289            .on_shutdown(|| {})
1290            .build();
1291
1292        assert_eq!(server.info().name, "chain");
1293        assert_eq!(server.info().version, "3.0");
1294        assert!(server.has_tools());
1295        assert!(server.has_resources());
1296        assert!(server.has_prompts());
1297    }
1298
1299    // ── Console configuration extended ─────────────────────────────
1300
1301    #[test]
1302    fn builder_with_console_config() {
1303        let config = ConsoleConfig::new().with_banner(BannerStyle::None);
1304        let builder = ServerBuilder::new("srv", "1.0").with_console_config(config);
1305        assert_eq!(builder.console_config().banner_style, BannerStyle::None);
1306    }
1307
1308    #[test]
1309    fn builder_with_traffic_logging() {
1310        let builder = ServerBuilder::new("srv", "1.0").with_traffic_logging(TrafficVerbosity::Full);
1311        let config = builder.console_config();
1312        assert_eq!(config.traffic_verbosity, TrafficVerbosity::Full);
1313    }
1314
1315    #[test]
1316    fn builder_with_periodic_stats() {
1317        let builder = ServerBuilder::new("srv", "1.0").with_periodic_stats(30);
1318        let config = builder.console_config();
1319        assert_eq!(config.stats_interval_secs, 30);
1320    }
1321
1322    #[test]
1323    fn builder_force_color() {
1324        let builder = ServerBuilder::new("srv", "1.0").force_color();
1325        let _config = builder.console_config();
1326        // Just verify the chain completes without panic
1327    }
1328
1329    // ── Logging config ─────────────────────────────────────────────
1330
1331    #[test]
1332    fn builder_logging_full_config() {
1333        let config = LoggingConfig {
1334            level: Level::Trace,
1335            timestamps: false,
1336            targets: false,
1337            file_line: true,
1338        };
1339        let _builder = ServerBuilder::new("srv", "1.0").logging(config);
1340    }
1341
1342    // ── List page size ─────────────────────────────────────────────
1343
1344    #[test]
1345    fn builder_list_page_size() {
1346        let server = ServerBuilder::new("srv", "1.0")
1347            .list_page_size(50)
1348            .tool(TestTool)
1349            .build();
1350        assert!(server.has_tools());
1351    }
1352
1353    // ── Resource template ──────────────────────────────────────────
1354
1355    #[test]
1356    fn builder_resource_template_enables_capability() {
1357        let template = ResourceTemplate {
1358            uri_template: "file://{path}".to_string(),
1359            name: "Template".to_string(),
1360            description: None,
1361            mime_type: None,
1362            icon: None,
1363            version: None,
1364            tags: vec![],
1365        };
1366        let server = ServerBuilder::new("srv", "1.0")
1367            .resource_template(template)
1368            .build();
1369        assert!(server.capabilities().resources.is_some());
1370    }
1371
1372    // ── Middleware ──────────────────────────────────────────────────
1373
1374    struct NoopMiddleware;
1375    impl crate::Middleware for NoopMiddleware {}
1376
1377    #[test]
1378    fn builder_middleware() {
1379        let server = ServerBuilder::new("srv", "1.0")
1380            .middleware(NoopMiddleware)
1381            .build();
1382        let _ = server;
1383    }
1384
1385    #[test]
1386    fn builder_multiple_middleware() {
1387        let server = ServerBuilder::new("srv", "1.0")
1388            .middleware(NoopMiddleware)
1389            .middleware(NoopMiddleware)
1390            .build();
1391        let _ = server;
1392    }
1393
1394    // ── Auth provider ──────────────────────────────────────────────
1395
1396    struct TestAuthProvider;
1397    impl crate::AuthProvider for TestAuthProvider {
1398        fn authenticate(
1399            &self,
1400            _ctx: &McpContext,
1401            _request: crate::auth::AuthRequest<'_>,
1402        ) -> McpResult<fastmcp_core::AuthContext> {
1403            Ok(fastmcp_core::AuthContext::with_subject("test-user"))
1404        }
1405    }
1406
1407    #[test]
1408    fn builder_auth_provider() {
1409        let server = ServerBuilder::new("srv", "1.0")
1410            .auth_provider(TestAuthProvider)
1411            .build();
1412        let _ = server;
1413    }
1414
1415    // ── auto_mask_errors ───────────────────────────────────────────
1416
1417    #[test]
1418    fn builder_auto_mask_errors() {
1419        // In debug builds (test mode), auto_mask_errors defaults to false
1420        let builder = ServerBuilder::new("srv", "1.0").auto_mask_errors();
1421        // In debug_assertions mode, masking should be disabled
1422        assert!(!builder.is_error_masking_enabled());
1423    }
1424
1425    // ── Duplicate behavior error ───────────────────────────────────
1426
1427    struct DupTool(&'static str);
1428    impl crate::ToolHandler for DupTool {
1429        fn definition(&self) -> Tool {
1430            Tool {
1431                name: self.0.to_string(),
1432                description: None,
1433                input_schema: serde_json::json!({"type": "object"}),
1434                output_schema: None,
1435                icon: None,
1436                version: None,
1437                tags: vec![],
1438                annotations: None,
1439            }
1440        }
1441        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
1442            Ok(vec![Content::text("ok")])
1443        }
1444    }
1445
1446    #[test]
1447    fn builder_on_duplicate_error_logs_but_continues() {
1448        // With DuplicateBehavior::Error, duplicate registration logs error
1449        // but builder doesn't panic
1450        let server = ServerBuilder::new("srv", "1.0")
1451            .on_duplicate(DuplicateBehavior::Error)
1452            .tool(DupTool("dup"))
1453            .tool(DupTool("dup")) // duplicate - will log error
1454            .build();
1455        assert!(server.has_tools());
1456    }
1457
1458    // ── Mount ──────────────────────────────────────────────────────
1459
1460    #[test]
1461    fn builder_mount_with_prefix() {
1462        let source = ServerBuilder::new("sub", "1.0")
1463            .tool(TestTool)
1464            .resource(TestResource)
1465            .prompt(TestPrompt)
1466            .build();
1467
1468        let main = ServerBuilder::new("main", "1.0")
1469            .mount(source, Some("sub"))
1470            .build();
1471
1472        assert!(main.has_tools());
1473        assert!(main.has_resources());
1474        assert!(main.has_prompts());
1475    }
1476
1477    #[test]
1478    fn builder_mount_without_prefix() {
1479        let source = ServerBuilder::new("sub", "1.0").tool(TestTool).build();
1480
1481        let main = ServerBuilder::new("main", "1.0")
1482            .mount(source, None)
1483            .build();
1484
1485        assert!(main.has_tools());
1486    }
1487
1488    #[test]
1489    fn builder_mount_tools_only() {
1490        let source = ServerBuilder::new("sub", "1.0")
1491            .tool(TestTool)
1492            .resource(TestResource)
1493            .prompt(TestPrompt)
1494            .build();
1495
1496        let main = ServerBuilder::new("main", "1.0")
1497            .mount_tools(source, Some("sub"))
1498            .build();
1499
1500        assert!(main.has_tools());
1501        // Resources and prompts should NOT be mounted
1502        assert!(!main.has_resources());
1503        assert!(!main.has_prompts());
1504    }
1505
1506    #[test]
1507    fn builder_mount_resources_only() {
1508        let source = ServerBuilder::new("sub", "1.0")
1509            .tool(TestTool)
1510            .resource(TestResource)
1511            .prompt(TestPrompt)
1512            .build();
1513
1514        let main = ServerBuilder::new("main", "1.0")
1515            .mount_resources(source, Some("data"))
1516            .build();
1517
1518        assert!(!main.has_tools());
1519        assert!(main.has_resources());
1520        assert!(!main.has_prompts());
1521    }
1522
1523    #[test]
1524    fn builder_mount_prompts_only() {
1525        let source = ServerBuilder::new("sub", "1.0")
1526            .tool(TestTool)
1527            .resource(TestResource)
1528            .prompt(TestPrompt)
1529            .build();
1530
1531        let main = ServerBuilder::new("main", "1.0")
1532            .mount_prompts(source, Some("tmpl"))
1533            .build();
1534
1535        assert!(!main.has_tools());
1536        assert!(!main.has_resources());
1537        assert!(main.has_prompts());
1538    }
1539
1540    #[test]
1541    fn builder_mount_empty_server() {
1542        let source = ServerBuilder::new("empty", "1.0").build();
1543
1544        let main = ServerBuilder::new("main", "1.0")
1545            .mount(source, Some("empty"))
1546            .build();
1547
1548        assert!(!main.has_tools());
1549        assert!(!main.has_resources());
1550        assert!(!main.has_prompts());
1551    }
1552
1553    // ── Proxy registration ─────────────────────────────────────────
1554
1555    #[test]
1556    fn builder_proxy_with_catalog() {
1557        use crate::proxy::{ProxyCatalog, ProxyClient};
1558
1559        struct DummyBackend;
1560        impl crate::proxy::ProxyBackend for DummyBackend {
1561            fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
1562                Ok(vec![])
1563            }
1564            fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
1565                Ok(vec![])
1566            }
1567            fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
1568                Ok(vec![])
1569            }
1570            fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
1571                Ok(vec![])
1572            }
1573            fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
1574                Ok(vec![])
1575            }
1576            fn call_tool_with_progress(
1577                &mut self,
1578                _: &str,
1579                _: serde_json::Value,
1580                _: crate::proxy::ProgressCallback<'_>,
1581            ) -> McpResult<Vec<Content>> {
1582                Ok(vec![])
1583            }
1584            fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
1585                Ok(vec![])
1586            }
1587            fn get_prompt(
1588                &mut self,
1589                _: &str,
1590                _: std::collections::HashMap<String, String>,
1591            ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1592                Ok(vec![])
1593            }
1594        }
1595
1596        let client = ProxyClient::from_backend(DummyBackend);
1597        let catalog = ProxyCatalog {
1598            tools: vec![Tool {
1599                name: "proxy-tool".to_string(),
1600                description: None,
1601                input_schema: serde_json::json!({}),
1602                output_schema: None,
1603                icon: None,
1604                version: None,
1605                tags: vec![],
1606                annotations: None,
1607            }],
1608            ..ProxyCatalog::default()
1609        };
1610
1611        let server = ServerBuilder::new("srv", "1.0")
1612            .proxy(client, catalog)
1613            .build();
1614        assert!(server.has_tools());
1615    }
1616
1617    // ── DEFAULT_REQUEST_TIMEOUT_SECS constant ──────────────────────
1618
1619    #[test]
1620    fn default_request_timeout_constant() {
1621        assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30);
1622    }
1623
1624    // ── mask_error_details toggling ────────────────────────────────
1625
1626    #[test]
1627    fn builder_mask_error_details_toggle() {
1628        let builder = ServerBuilder::new("srv", "1.0")
1629            .mask_error_details(true)
1630            .mask_error_details(false);
1631        assert!(!builder.is_error_masking_enabled());
1632    }
1633
1634    // ── strict_input_validation toggling ────────────────────────────
1635
1636    #[test]
1637    fn builder_strict_validation_toggle() {
1638        let builder = ServerBuilder::new("srv", "1.0")
1639            .strict_input_validation(true)
1640            .strict_input_validation(false);
1641        assert!(!builder.is_strict_input_validation_enabled());
1642    }
1643
1644    // ── Task manager ──────────────────────────────────────────────────
1645
1646    #[test]
1647    fn builder_with_task_manager_enables_capability() {
1648        use crate::tasks::TaskManager;
1649        let tm = TaskManager::new().into_shared();
1650        let server = ServerBuilder::new("srv", "1.0")
1651            .with_task_manager(tm)
1652            .build();
1653        assert!(server.capabilities().tasks.is_some());
1654    }
1655
1656    #[test]
1657    fn builder_with_task_manager_list_changed_true() {
1658        use crate::tasks::TaskManager;
1659        let tm = TaskManager::with_list_changed_notifications().into_shared();
1660        let server = ServerBuilder::new("srv", "1.0")
1661            .with_task_manager(tm)
1662            .build();
1663        let cap = server.capabilities().tasks.as_ref().unwrap();
1664        assert!(cap.list_changed);
1665    }
1666
1667    #[test]
1668    fn builder_with_task_manager_list_changed_false() {
1669        use crate::tasks::TaskManager;
1670        let tm = TaskManager::new().into_shared();
1671        let server = ServerBuilder::new("srv", "1.0")
1672            .with_task_manager(tm)
1673            .build();
1674        let cap = server.capabilities().tasks.as_ref().unwrap();
1675        assert!(!cap.list_changed);
1676    }
1677
1678    // ── Duplicate behavior for resources and prompts ─────────────────
1679
1680    struct DupResource(&'static str);
1681    impl crate::ResourceHandler for DupResource {
1682        fn definition(&self) -> Resource {
1683            Resource {
1684                uri: format!("file:///{}", self.0),
1685                name: self.0.to_string(),
1686                description: None,
1687                mime_type: None,
1688                icon: None,
1689                version: None,
1690                tags: vec![],
1691            }
1692        }
1693        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
1694            Ok(vec![])
1695        }
1696    }
1697
1698    struct DupPrompt(&'static str);
1699    impl crate::PromptHandler for DupPrompt {
1700        fn definition(&self) -> Prompt {
1701            Prompt {
1702                name: self.0.to_string(),
1703                description: None,
1704                arguments: vec![],
1705                icon: None,
1706                version: None,
1707                tags: vec![],
1708            }
1709        }
1710        fn get(
1711            &self,
1712            _ctx: &McpContext,
1713            _args: std::collections::HashMap<String, String>,
1714        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1715            Ok(vec![])
1716        }
1717    }
1718
1719    #[test]
1720    fn builder_on_duplicate_error_resource_logs_but_continues() {
1721        let server = ServerBuilder::new("srv", "1.0")
1722            .on_duplicate(DuplicateBehavior::Error)
1723            .resource(DupResource("dup"))
1724            .resource(DupResource("dup"))
1725            .build();
1726        assert!(server.has_resources());
1727    }
1728
1729    #[test]
1730    fn builder_on_duplicate_error_prompt_logs_but_continues() {
1731        let server = ServerBuilder::new("srv", "1.0")
1732            .on_duplicate(DuplicateBehavior::Error)
1733            .prompt(DupPrompt("dup"))
1734            .prompt(DupPrompt("dup"))
1735            .build();
1736        assert!(server.has_prompts());
1737    }
1738
1739    // ── Proxy with resources and prompts ─────────────────────────────
1740
1741    #[test]
1742    fn builder_proxy_with_resources_and_prompts() {
1743        use crate::proxy::{ProxyCatalog, ProxyClient};
1744
1745        struct DummyBackend2;
1746        impl crate::proxy::ProxyBackend for DummyBackend2 {
1747            fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
1748                Ok(vec![])
1749            }
1750            fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
1751                Ok(vec![])
1752            }
1753            fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
1754                Ok(vec![])
1755            }
1756            fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
1757                Ok(vec![])
1758            }
1759            fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
1760                Ok(vec![])
1761            }
1762            fn call_tool_with_progress(
1763                &mut self,
1764                _: &str,
1765                _: serde_json::Value,
1766                _: crate::proxy::ProgressCallback<'_>,
1767            ) -> McpResult<Vec<Content>> {
1768                Ok(vec![])
1769            }
1770            fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
1771                Ok(vec![])
1772            }
1773            fn get_prompt(
1774                &mut self,
1775                _: &str,
1776                _: std::collections::HashMap<String, String>,
1777            ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
1778                Ok(vec![])
1779            }
1780        }
1781
1782        let client = ProxyClient::from_backend(DummyBackend2);
1783        let catalog = ProxyCatalog {
1784            resources: vec![Resource {
1785                uri: "file:///proxy-res".to_string(),
1786                name: "proxy-res".to_string(),
1787                description: None,
1788                mime_type: None,
1789                icon: None,
1790                version: None,
1791                tags: vec![],
1792            }],
1793            prompts: vec![Prompt {
1794                name: "proxy-prompt".to_string(),
1795                description: None,
1796                arguments: vec![],
1797                icon: None,
1798                version: None,
1799                tags: vec![],
1800            }],
1801            resource_templates: vec![ResourceTemplate {
1802                uri_template: "db://{table}".to_string(),
1803                name: "db".to_string(),
1804                description: None,
1805                mime_type: None,
1806                icon: None,
1807                version: None,
1808                tags: vec![],
1809            }],
1810            ..ProxyCatalog::default()
1811        };
1812
1813        let server = ServerBuilder::new("srv", "1.0")
1814            .proxy(client, catalog)
1815            .build();
1816        assert!(server.has_resources());
1817        assert!(server.has_prompts());
1818        assert!(!server.has_tools());
1819    }
1820
1821    // ── Build propagates strict validation to router ─────────────────
1822
1823    #[test]
1824    fn build_propagates_strict_validation_to_router() {
1825        let server = ServerBuilder::new("srv", "1.0")
1826            .strict_input_validation(true)
1827            .build();
1828        let router = server.into_router();
1829        assert!(router.strict_input_validation());
1830    }
1831
1832    #[test]
1833    fn build_propagates_strict_validation_false_to_router() {
1834        let server = ServerBuilder::new("srv", "1.0")
1835            .strict_input_validation(false)
1836            .build();
1837        let router = server.into_router();
1838        assert!(!router.strict_input_validation());
1839    }
1840
1841    // ── log_level_filter with Off defaults to Info ───────────────────
1842
1843    #[test]
1844    fn builder_log_level_filter_off() {
1845        let _builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Off);
1846        // LevelFilter::Off.to_level() is None, so logging.level defaults to Info
1847    }
1848
1849    // ── mount does not update capabilities when nothing mounted ──────
1850
1851    #[test]
1852    fn builder_mount_no_op_leaves_capabilities_unchanged() {
1853        let source = ServerBuilder::new("sub", "1.0").build();
1854        let main = ServerBuilder::new("main", "1.0")
1855            .mount(source, Some("ns"))
1856            .build();
1857        assert!(!main.has_tools());
1858        assert!(!main.has_resources());
1859        assert!(!main.has_prompts());
1860    }
1861}