Skip to main content

fastmcp_server/
builder.rs

1//! Server builder for configuring MCP servers.
2
3use std::collections::HashMap;
4use std::ffi::OsStr;
5use std::fmt;
6use std::sync::{Arc, Mutex};
7
8use fastmcp_console::config::{BannerStyle, ConsoleConfig, TrafficVerbosity};
9use fastmcp_console::stats::ServerStats;
10use fastmcp_core::McpResult;
11use fastmcp_protocol::extensions::ExtensionSettingsCompatibilityResolver;
12#[cfg(feature = "apps")]
13use fastmcp_protocol::extensions::{
14    official_mcp_apps_extension_id, validate_official_mcp_apps_descriptor,
15    validate_official_mcp_apps_server_settings,
16};
17#[cfg(feature = "proxy")]
18use fastmcp_protocol::protocol_policy::ProtocolEra;
19use fastmcp_protocol::protocol_policy::ProtocolPolicy;
20use fastmcp_protocol::{
21    LoggingCapability, PromptsCapability, ResourceTemplate, ResourcesCapability,
22    ServerCapabilities, ServerExtensionDiscovery, ServerInfo, ToolsCapability,
23};
24use log::{Level, LevelFilter};
25
26#[cfg(feature = "tasks")]
27use crate::FinalTaskRuntime;
28use crate::handler::CompletionHandler;
29#[cfg(feature = "proxy")]
30use crate::handler::{
31    FinalProxyPromptHandler, FinalProxyResourceHandler, FinalProxyResourceTemplateHandler,
32};
33use crate::oauth::OAuthHttpRoutes;
34#[cfg(feature = "apps")]
35use crate::providers::McpAppsUiResource;
36#[cfg(all(test, feature = "proxy"))]
37use crate::proxy::ProxyFinalCatalog;
38#[cfg(all(feature = "proxy", feature = "tasks"))]
39use crate::proxy::ProxyFinalTaskRelay;
40#[cfg(feature = "proxy")]
41use crate::proxy::{
42    ProxyCompletionHandler, ProxyPromptCatalog, ProxyPromptHandler, ProxyResourceCatalog,
43    ProxyResourceHandler, ProxyResourceTemplateCatalog, ProxyToolCatalog, ProxyToolHandler,
44    ProxyTypedCatalog,
45};
46#[cfg(feature = "tasks")]
47use crate::tasks::FinalTaskRuntimeConfig;
48#[cfg(all(test, feature = "tasks"))]
49use crate::tasks::SharedTaskManager;
50use crate::{
51    AuthProvider, DuplicateBehavior, ExtensionHandlerRegistry, FinalSubscriptionRegistry,
52    HttpServerConfig, LifespanHooks, LoggingConfig, PromptHandler, ResourceHandler, Router, Server,
53    ServerExtensionConfigurationError, ServerExtensionRuntime, ToolHandler,
54};
55#[cfg(feature = "proxy")]
56use crate::{ProxyCatalog, ProxyClient};
57
58/// Default request timeout in seconds.
59const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
60
61/// Reserved launch setting written by `fastmcp run` for FastMCP server targets.
62const FASTMCP_PROTOCOL_POLICY_ENV: &str = "FASTMCP_PROTOCOL_POLICY";
63
64/// The selected launch policy is malformed, not valid Unicode, or unavailable
65/// in the compiled server feature set.
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ServerLaunchPolicyError {
68    /// The reserved launch setting was present but was not valid Unicode.
69    NonUnicode,
70    /// The reserved launch setting was not one of the exact supported values.
71    InvalidValue,
72    /// The selected policy requires a server feature that is not compiled in.
73    FeatureUnavailable,
74}
75
76impl fmt::Display for ServerLaunchPolicyError {
77    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            Self::NonUnicode => write!(
80                formatter,
81                "{FASTMCP_PROTOCOL_POLICY_ENV} must be valid Unicode"
82            ),
83            Self::InvalidValue => write!(
84                formatter,
85                "{FASTMCP_PROTOCOL_POLICY_ENV} must be auto, modern-only, or legacy-only"
86            ),
87            Self::FeatureUnavailable => write!(
88                formatter,
89                "{FASTMCP_PROTOCOL_POLICY_ENV}=auto or legacy-only requires the legacy-2024-11-05 feature"
90            ),
91        }
92    }
93}
94
95impl std::error::Error for ServerLaunchPolicyError {}
96
97fn protocol_policy_from_server_launch_value(
98    value: Option<&OsStr>,
99) -> Result<Option<ProtocolPolicy>, ServerLaunchPolicyError> {
100    match value {
101        None => Ok(None),
102        Some(value) => match value.to_str() {
103            Some("auto") => Ok(Some(ProtocolPolicy::Auto)),
104            Some("modern-only") => Ok(Some(ProtocolPolicy::ModernOnly)),
105            Some("legacy-only") => Ok(Some(ProtocolPolicy::LegacyOnly)),
106            Some(_) => Err(ServerLaunchPolicyError::InvalidValue),
107            None => Err(ServerLaunchPolicyError::NonUnicode),
108        },
109    }
110}
111
112fn protocol_policy_from_server_launch_environment()
113-> Result<Option<ProtocolPolicy>, ServerLaunchPolicyError> {
114    protocol_policy_from_server_launch_value(
115        std::env::var_os(FASTMCP_PROTOCOL_POLICY_ENV).as_deref(),
116    )
117}
118
119const fn legacy_protocol_is_available() -> bool {
120    cfg!(feature = "legacy-2024-11-05")
121}
122
123fn resolve_protocol_policy(
124    launch_protocol_policy: Option<ProtocolPolicy>,
125    legacy_protocol_available: bool,
126) -> Result<ProtocolPolicy, ServerLaunchPolicyError> {
127    let protocol_policy = launch_protocol_policy.unwrap_or(if legacy_protocol_available {
128        ProtocolPolicy::Auto
129    } else {
130        ProtocolPolicy::ModernOnly
131    });
132
133    if !legacy_protocol_available && !matches!(protocol_policy, ProtocolPolicy::ModernOnly) {
134        return Err(ServerLaunchPolicyError::FeatureUnavailable);
135    }
136
137    Ok(protocol_policy)
138}
139
140/// Builder for configuring an MCP server.
141pub struct ServerBuilder {
142    info: ServerInfo,
143    capabilities: ServerCapabilities,
144    router: Router,
145    instructions: Option<String>,
146    /// Optional modern discovery title. Exact-2024 initialize stays name/version.
147    title: Option<String>,
148    /// Optional modern discovery description.
149    description: Option<String>,
150    /// Optional modern discovery website identity.
151    website_url: Option<String>,
152    /// Optional modern discovery icons.
153    icons: Vec<fastmcp_protocol::common_types::RawIcon>,
154    /// Request timeout in seconds (0 = no timeout).
155    request_timeout_secs: u64,
156    /// Whether to enable statistics collection.
157    stats_enabled: bool,
158    /// Whether to mask internal error details in responses.
159    mask_error_details: bool,
160    /// Logging configuration.
161    logging: LoggingConfig,
162    /// Console configuration for rich output.
163    console_config: ConsoleConfig,
164    /// Lifecycle hooks for startup/shutdown.
165    lifespan: LifespanHooks,
166    /// Optional authentication provider.
167    auth_provider: Option<Arc<dyn AuthProvider>>,
168    /// Registered middleware.
169    middleware: Vec<Box<dyn crate::Middleware>>,
170    /// Test-only legacy task manager. Production builds have no task-manager
171    /// field or builder edge.
172    #[cfg(all(test, feature = "tasks"))]
173    task_manager: Option<SharedTaskManager>,
174    /// Behavior when registering duplicate component names.
175    on_duplicate: DuplicateBehavior,
176    /// Whether to use strict input validation (reject extra properties).
177    strict_input_validation: bool,
178    /// Per-connection ceiling for concurrent server-to-client requests.
179    max_bidirectional_requests_per_connection: usize,
180    /// Immutable protocol-era admission policy for live stdio/runtime connections.
181    protocol_policy: ProtocolPolicy,
182    /// Reserved policy selected before construction by a launch setting or a
183    /// sealed embedding component.
184    launch_protocol_policy: Option<ProtocolPolicy>,
185    /// Immutable configuration for the live dual-era HTTP endpoint.
186    http_config: HttpServerConfig,
187    /// Optional immutable OAuth-only public HTTP routes.
188    oauth_http_routes: Option<OAuthHttpRoutes>,
189    /// Installed extension handlers and current server discovery settings.
190    extension_runtime: Option<ServerExtensionRuntime>,
191    /// Application-owned state for the configured final Tasks extension.
192    #[cfg(feature = "tasks")]
193    final_task_runtime: Option<FinalTaskRuntime>,
194    /// One route-bound upstream final Tasks relay. The official Task methods
195    /// cannot disambiguate two independent upstream task-ID namespaces.
196    #[cfg(all(feature = "proxy", feature = "tasks"))]
197    final_task_relay: Option<Arc<ProxyFinalTaskRelay>>,
198}
199
200impl ServerBuilder {
201    /// Creates a new server builder.
202    ///
203    /// Statistics collection is enabled by default. Use [`without_stats`](Self::without_stats)
204    /// to disable it for performance-critical scenarios.
205    ///
206    /// Console configuration defaults to environment-based settings. Use
207    /// [`with_console_config`](Self::with_console_config) for programmatic control.
208    #[must_use]
209    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
210        Self::try_new(name, version).unwrap_or_else(|error| {
211            panic!("ServerBuilder::new rejected launch configuration: {error}")
212        })
213    }
214
215    /// Creates a new server builder after validating the reserved launch policy.
216    ///
217    /// This is the typed construction boundary for applications that need to
218    /// report a malformed or unavailable launch policy instead of panicking.
219    pub fn try_new(
220        name: impl Into<String>,
221        version: impl Into<String>,
222    ) -> Result<Self, ServerLaunchPolicyError> {
223        Self::from_launch_protocol_policy(
224            name,
225            version,
226            protocol_policy_from_server_launch_environment(),
227        )
228    }
229
230    /// Creates a builder whose protocol policy is fixed by the embedding
231    /// component rather than the process launch environment.
232    ///
233    /// The selected policy is validated against the compiled feature set, but
234    /// this constructor deliberately does not read `FASTMCP_PROTOCOL_POLICY`.
235    /// It also reserves the selected policy, so a later
236    /// [`protocol_policy`](Self::protocol_policy) call validates its argument
237    /// without changing the fixed selection. This is intended for sealed
238    /// component facades that expose only one protocol era.
239    pub fn try_new_with_fixed_protocol_policy(
240        name: impl Into<String>,
241        version: impl Into<String>,
242        policy: ProtocolPolicy,
243    ) -> Result<Self, ServerLaunchPolicyError> {
244        let policy = resolve_protocol_policy(Some(policy), legacy_protocol_is_available())?;
245        Ok(Self::with_protocol_policy(
246            name,
247            version,
248            policy,
249            Some(policy),
250        ))
251    }
252
253    pub(crate) fn from_launch_protocol_policy(
254        name: impl Into<String>,
255        version: impl Into<String>,
256        launch_protocol_policy: Result<Option<ProtocolPolicy>, ServerLaunchPolicyError>,
257    ) -> Result<Self, ServerLaunchPolicyError> {
258        let launch_protocol_policy = launch_protocol_policy?;
259        let protocol_policy =
260            resolve_protocol_policy(launch_protocol_policy, legacy_protocol_is_available())?;
261        Ok(Self::with_protocol_policy(
262            name,
263            version,
264            protocol_policy,
265            launch_protocol_policy,
266        ))
267    }
268
269    fn with_protocol_policy(
270        name: impl Into<String>,
271        version: impl Into<String>,
272        protocol_policy: ProtocolPolicy,
273        launch_protocol_policy: Option<ProtocolPolicy>,
274    ) -> Self {
275        let console_config = ConsoleConfig::from_env();
276        let logging = LoggingConfig::from(&console_config);
277        Self {
278            info: ServerInfo {
279                name: name.into(),
280                version: version.into(),
281            },
282            capabilities: ServerCapabilities {
283                logging: Some(LoggingCapability::default()),
284                ..ServerCapabilities::default()
285            },
286            router: Router::new(),
287            instructions: None,
288            title: None,
289            description: None,
290            website_url: None,
291            icons: Vec::new(),
292            request_timeout_secs: DEFAULT_REQUEST_TIMEOUT_SECS,
293            stats_enabled: true,
294            mask_error_details: false, // Disabled by default for development
295            logging,
296            console_config,
297            lifespan: LifespanHooks::default(),
298            auth_provider: None,
299            middleware: Vec::new(),
300            #[cfg(all(test, feature = "tasks"))]
301            task_manager: None,
302            on_duplicate: DuplicateBehavior::default(),
303            strict_input_validation: false,
304            max_bidirectional_requests_per_connection:
305                crate::bidirectional::DEFAULT_MAX_IN_FLIGHT_REQUESTS,
306            protocol_policy,
307            launch_protocol_policy,
308            http_config: HttpServerConfig::default(),
309            oauth_http_routes: None,
310            extension_runtime: None,
311            #[cfg(feature = "tasks")]
312            final_task_runtime: None,
313            #[cfg(all(feature = "proxy", feature = "tasks"))]
314            final_task_relay: None,
315        }
316    }
317
318    /// Sets the behavior when registering duplicate component names.
319    ///
320    /// Controls what happens when a tool, resource, resource template, prompt,
321    /// or mounted component is registered with an identifier that already
322    /// exists:
323    ///
324    /// - [`DuplicateBehavior::Error`]: Reject the conflicting registration,
325    ///   log the error, and continue constructing the builder
326    /// - [`DuplicateBehavior::Warn`]: Log warning, keep original (default)
327    /// - [`DuplicateBehavior::Replace`]: Replace with new component
328    /// - [`DuplicateBehavior::Ignore`]: Silently keep original
329    ///
330    /// # Example
331    ///
332    /// ```ignore
333    /// Server::new("demo", "1.0")
334    ///     .on_duplicate(DuplicateBehavior::Error)  // Strict mode
335    ///     .tool(handler1)
336    ///     .tool(handler2)  // Rejected and logged if the name conflicts
337    ///     .build();
338    /// ```
339    #[must_use]
340    pub fn on_duplicate(mut self, behavior: DuplicateBehavior) -> Self {
341        self.on_duplicate = behavior;
342        self
343    }
344
345    /// Sets an authentication provider.
346    #[must_use]
347    pub fn auth_provider<P: AuthProvider + 'static>(mut self, provider: P) -> Self {
348        self.auth_provider = Some(Arc::new(provider));
349        self
350    }
351
352    /// Disables statistics collection.
353    ///
354    /// Use this for performance-critical scenarios where the overhead
355    /// of atomic operations for stats tracking is undesirable.
356    /// The overhead is minimal (typically nanoseconds per request),
357    /// so this is rarely needed.
358    #[must_use]
359    pub fn without_stats(mut self) -> Self {
360        self.stats_enabled = false;
361        self
362    }
363
364    /// Sets the request timeout in seconds.
365    ///
366    /// Set to 0 to omit the server-owned ceiling. Ambient/request and handler
367    /// deadlines are still composed into admission checks, cooperative
368    /// checkpoints, and late-result rejection; this setting cannot relax them.
369    /// A deadline does not preempt blocking synchronous code or imply that
370    /// descendant work has been cancelled and drained. Default is 30 seconds.
371    #[must_use]
372    pub fn request_timeout(mut self, secs: u64) -> Self {
373        self.request_timeout_secs = secs;
374        self
375    }
376
377    /// Sets the maximum number of in-flight server-to-client requests for one
378    /// transport connection.
379    ///
380    /// # Errors
381    ///
382    /// Returns `InvalidParams` when `max` is zero or exceeds the hard safety
383    /// limit enforced by the bidirectional request tracker.
384    pub fn max_bidirectional_requests_per_connection(mut self, max: usize) -> McpResult<Self> {
385        crate::bidirectional::PendingRequests::validate_max_in_flight(max)?;
386        self.max_bidirectional_requests_per_connection = max;
387        Ok(self)
388    }
389
390    /// Sets the pagination page size for list methods.
391    ///
392    /// When set, list methods will return up to `page_size` items and provide an
393    /// opaque `nextCursor` for retrieving the next page. When not set (default),
394    /// list methods return all items in a single response.
395    #[must_use]
396    pub fn list_page_size(mut self, page_size: usize) -> Self {
397        self.router.set_list_page_size(Some(page_size));
398        self
399    }
400
401    /// Enables or disables error detail masking.
402    ///
403    /// When enabled, internal error details are hidden from client responses:
404    /// - Stack traces removed
405    /// - File paths sanitized
406    /// - Internal state not exposed
407    /// - Generic "Internal server error" message returned
408    ///
409    /// Client errors (invalid request, method not found, etc.) are preserved
410    /// since they don't contain sensitive internal details.
411    ///
412    /// Default is `false` (disabled) for development convenience.
413    ///
414    /// # Example
415    ///
416    /// ```ignore
417    /// let server = Server::new("api", "1.0")
418    ///     .mask_error_details(true)  // Always mask in production
419    ///     .build();
420    /// ```
421    #[must_use]
422    pub fn mask_error_details(mut self, enabled: bool) -> Self {
423        self.mask_error_details = enabled;
424        self
425    }
426
427    /// Automatically masks error details based on environment.
428    ///
429    /// Masking is enabled when:
430    /// - `FASTMCP_ENV` is set to "production"
431    /// - `FASTMCP_MASK_ERRORS` is set to "true" or "1"
432    /// - The build is a release build (`cfg!(not(debug_assertions))`)
433    ///
434    /// Masking is explicitly disabled when:
435    /// - `FASTMCP_MASK_ERRORS` is set to "false" or "0"
436    ///
437    /// # Example
438    ///
439    /// ```ignore
440    /// let server = Server::new("api", "1.0")
441    ///     .auto_mask_errors()
442    ///     .build();
443    /// ```
444    #[must_use]
445    pub fn auto_mask_errors(mut self) -> Self {
446        // Check for explicit override first
447        if let Ok(val) = std::env::var("FASTMCP_MASK_ERRORS") {
448            match val.to_lowercase().as_str() {
449                "true" | "1" | "yes" => {
450                    self.mask_error_details = true;
451                    return self;
452                }
453                "false" | "0" | "no" => {
454                    self.mask_error_details = false;
455                    return self;
456                }
457                _ => {} // Fall through to other checks
458            }
459        }
460
461        // Check for production environment
462        if let Ok(env) = std::env::var("FASTMCP_ENV") {
463            if env.to_lowercase() == "production" {
464                self.mask_error_details = true;
465                return self;
466            }
467        }
468
469        // Default: mask in release builds, don't mask in debug builds
470        self.mask_error_details = cfg!(not(debug_assertions));
471        self
472    }
473
474    /// Returns whether error masking is enabled.
475    #[must_use]
476    pub fn is_error_masking_enabled(&self) -> bool {
477        self.mask_error_details
478    }
479
480    /// Enables or disables strict input validation.
481    ///
482    /// When enabled, tool input validation will reject any properties not
483    /// explicitly defined in the tool's input schema (enforces `additionalProperties: false`).
484    ///
485    /// When disabled (default), extra properties are allowed unless the schema
486    /// explicitly sets `additionalProperties: false`.
487    ///
488    /// # Example
489    ///
490    /// ```ignore
491    /// let server = Server::new("api", "1.0")
492    ///     .strict_input_validation(true)  // Reject unknown properties
493    ///     .build();
494    /// ```
495    #[must_use]
496    pub fn strict_input_validation(mut self, enabled: bool) -> Self {
497        self.strict_input_validation = enabled;
498        self
499    }
500
501    /// Returns whether strict input validation is enabled.
502    #[must_use]
503    pub fn is_strict_input_validation_enabled(&self) -> bool {
504        self.strict_input_validation
505    }
506
507    /// Selects the immutable MCP protocol-era policy for live stdio and runtime connections.
508    ///
509    /// With the exact legacy adapter enabled, the default [`ProtocolPolicy::Auto`] classifies the
510    /// first accepted opening frame and then pins that connection to its selected era. Without
511    /// that adapter, construction defaults to [`ProtocolPolicy::ModernOnly`]. `ModernOnly` and
512    /// `LegacyOnly` reject an opening frame from the other exact supported era before it can enter
513    /// request dispatch. In a no-legacy production build, `Auto` and `LegacyOnly` return
514    /// [`ServerLaunchPolicyError::FeatureUnavailable`] before either can be stored.
515    pub fn protocol_policy(
516        mut self,
517        policy: ProtocolPolicy,
518    ) -> Result<Self, ServerLaunchPolicyError> {
519        self.try_set_protocol_policy(policy)?;
520        Ok(self)
521    }
522
523    /// Attempts to select the immutable MCP protocol-era policy without
524    /// consuming the builder.
525    ///
526    /// A policy unavailable in the compiled feature set is rejected before
527    /// this builder is changed. A reserved launch or component policy still
528    /// takes precedence over an explicit builder selection.
529    pub fn try_set_protocol_policy(
530        &mut self,
531        policy: ProtocolPolicy,
532    ) -> Result<(), ServerLaunchPolicyError> {
533        resolve_protocol_policy(Some(policy), legacy_protocol_is_available())?;
534        if self.launch_protocol_policy.is_none() {
535            self.protocol_policy = policy;
536        }
537        Ok(())
538    }
539
540    /// Installs the server's modern-only extension handlers and discovery settings.
541    ///
542    /// Descriptor registration remains mutable only until [`Self::build`]. The
543    /// builder validates the advertised identifiers immediately, then freezes
544    /// the handler and descriptor registries together while building the
545    /// immutable [`Server`]. Exact MCP 2024-11-05 remains outside this path.
546    pub fn extension_registry<R>(
547        mut self,
548        handlers: ExtensionHandlerRegistry,
549        server_discovery: ServerExtensionDiscovery,
550        resolver: R,
551    ) -> Result<Self, ServerExtensionConfigurationError>
552    where
553        R: ExtensionSettingsCompatibilityResolver + Send + 'static,
554    {
555        if self.extension_runtime.is_some() {
556            return Err(ServerExtensionConfigurationError::AlreadyInstalled);
557        }
558        #[cfg(feature = "apps")]
559        if let Some(settings) = server_discovery
560            .extensions
561            .get(&official_mcp_apps_extension_id())
562        {
563            validate_official_mcp_apps_server_settings(settings)
564                .map_err(ServerExtensionConfigurationError::Registry)?;
565        }
566        #[cfg(feature = "tasks")]
567        let mut extension_runtime =
568            ServerExtensionRuntime::new(handlers, server_discovery, resolver)?;
569        #[cfg(not(feature = "tasks"))]
570        let extension_runtime = ServerExtensionRuntime::new(handlers, server_discovery, resolver)?;
571        #[cfg(feature = "tasks")]
572        if let Some(task_runtime) = self.final_task_runtime.as_ref() {
573            extension_runtime.install_final_tasks(task_runtime)?;
574        }
575        #[cfg(all(feature = "proxy", feature = "tasks"))]
576        if self.final_task_runtime.is_none() {
577            if let Some(task_relay) = self.final_task_relay.as_ref() {
578                extension_runtime.install_proxy_final_tasks(Arc::clone(task_relay))?;
579            }
580        }
581        self.extension_runtime = Some(extension_runtime);
582        Ok(self)
583    }
584
585    /// Installs the official MCP Apps descriptor and empty server discovery marker.
586    ///
587    /// Apps owns no client-to-server JSON-RPC method on this surface. This
588    /// opt-in therefore configures bilateral capability negotiation and
589    /// `server/discover` metadata only. It composes in either order with
590    /// [`Self::final_tasks`], preserving an Apps inactive disposition when a
591    /// client does not advertise the Apps HTML MIME type.
592    #[cfg(feature = "apps")]
593    pub fn mcp_apps(mut self) -> Result<Self, ServerExtensionConfigurationError> {
594        if let Some(extension_runtime) = self.extension_runtime.as_mut() {
595            extension_runtime.install_official_mcp_apps()?;
596        } else {
597            let mut extension_runtime = ServerExtensionRuntime::with_official_mcp_apps()?;
598            #[cfg(feature = "tasks")]
599            if let Some(task_runtime) = self.final_task_runtime.as_ref() {
600                extension_runtime.install_final_tasks(task_runtime)?;
601            }
602            #[cfg(all(feature = "proxy", feature = "tasks"))]
603            if self.final_task_runtime.is_none() {
604                if let Some(task_relay) = self.final_task_relay.as_ref() {
605                    extension_runtime.install_proxy_final_tasks(Arc::clone(task_relay))?;
606                }
607            }
608            self.extension_runtime = Some(extension_runtime);
609        }
610        Ok(self)
611    }
612
613    /// Returns whether this destination owns an active, exact official Apps
614    /// capability configuration. Merely advertising the official identifier
615    /// is insufficient: the frozen descriptor, local enablement, and exact
616    /// empty server settings must all be present.
617    #[cfg(feature = "apps")]
618    fn has_active_official_mcp_apps(&self) -> bool {
619        let apps_id = official_mcp_apps_extension_id();
620        self.extension_runtime.as_ref().is_some_and(|runtime| {
621            runtime.local_enablement.is_enabled(&apps_id)
622                && runtime
623                    .handlers
624                    .descriptor_registry()
625                    .descriptor(&apps_id)
626                    .is_some_and(|descriptor| {
627                        validate_official_mcp_apps_descriptor(descriptor).is_ok()
628                    })
629                && runtime
630                    .server_discovery
631                    .extensions
632                    .get(&apps_id)
633                    .is_some_and(|settings| {
634                        validate_official_mcp_apps_server_settings(settings).is_ok()
635                    })
636        })
637    }
638
639    /// Registers one final-only `ui://` HTML document for a negotiated MCP Apps View.
640    ///
641    /// This resource is deliberately absent from exact MCP 2024-11-05
642    /// discovery and reads. Call [`Self::mcp_apps`] first so the server also
643    /// advertises the matching modern bilateral extension capability.
644    #[cfg(feature = "apps")]
645    pub fn mcp_apps_ui_resource(mut self, resource: McpAppsUiResource) -> McpResult<Self> {
646        if !self.has_active_official_mcp_apps() {
647            return Err(fastmcp_core::McpError::invalid_request(
648                "MCP Apps UI resources require ServerBuilder::mcp_apps first",
649            ));
650        }
651        self.router
652            .add_mcp_apps_ui_resource_with_behavior(resource, self.on_duplicate)?;
653        self.advertise_legacy_resource_subscriptions();
654        Ok(self)
655    }
656
657    /// Registers a tool with final-only MCP Apps metadata.
658    ///
659    /// Register the linked [`McpAppsUiResource`] first. The router validates
660    /// the tool's typed `_meta.ui.resourceUri` against that exact final Apps
661    /// HTML resource before either tool catalog is changed. Unlike
662    /// [`Self::tool`], this Apps-specific entry point returns the registration
663    /// failure directly.
664    #[cfg(feature = "apps")]
665    pub fn mcp_apps_tool<H: ToolHandler + 'static>(mut self, handler: H) -> McpResult<Self> {
666        if !self.has_active_official_mcp_apps() {
667            return Err(fastmcp_core::McpError::invalid_request(
668                "MCP Apps tools require ServerBuilder::mcp_apps first",
669            ));
670        }
671        self.router
672            .add_mcp_apps_tool_with_behavior(handler, self.on_duplicate)?;
673        self.advertise_legacy_tools_list_changed();
674        Ok(self)
675    }
676
677    /// Installs the official final Tasks extension around application-owned state.
678    ///
679    /// The supplied runtime owns neither an executor nor a task region. Its
680    /// store and notification emitter remain application-owned, while a
681    /// caller-owned structured supervisor advances worker state through the
682    /// [`FinalTaskRuntime`] retained on the built [`Server`]. This builder
683    /// installs the official descriptor, its three client-to-server request
684    /// handlers, and its `notifications/tasks` delivery path together.
685    #[cfg(feature = "tasks")]
686    pub fn final_tasks(
687        mut self,
688        task_runtime: FinalTaskRuntime,
689    ) -> Result<Self, ServerExtensionConfigurationError> {
690        if self.final_task_runtime.is_some() || {
691            #[cfg(feature = "proxy")]
692            {
693                self.final_task_relay.is_some()
694            }
695            #[cfg(not(feature = "proxy"))]
696            {
697                false
698            }
699        } {
700            return Err(ServerExtensionConfigurationError::FinalTasksAlreadyInstalled);
701        }
702        if let Some(extension_runtime) = self.extension_runtime.as_mut() {
703            extension_runtime.install_final_tasks(&task_runtime)?;
704        }
705        self.final_task_runtime = Some(task_runtime);
706        Ok(self)
707    }
708
709    /// Installs a process-local official Tasks runtime when the builder has
710    /// no Tasks owner yet.
711    ///
712    /// A caller-supplied [`Self::final_tasks`] runtime, a proxy Tasks relay,
713    /// or the quarantined [`Self::with_task_manager`] path all suppress this
714    /// default. If an existing extension registry already owns the official
715    /// Tasks methods, the default is left uninstalled rather than panicking.
716    #[cfg(feature = "tasks")]
717    fn install_default_in_memory_final_tasks(&mut self) {
718        if self.final_task_runtime.is_some() {
719            return;
720        }
721        #[cfg(all(test, feature = "tasks"))]
722        if self.task_manager.is_some() {
723            return;
724        }
725        #[cfg(feature = "proxy")]
726        if self.final_task_relay.is_some() {
727            return;
728        }
729        let runtime = FinalTaskRuntime::in_memory(
730            FinalTaskRuntimeConfig::new(60_000, Some(5_000))
731                .expect("default in-memory Tasks timing policy is valid"),
732            Arc::new(|_| {}),
733        );
734        if let Some(extension_runtime) = self.extension_runtime.as_mut() {
735            if extension_runtime.install_final_tasks(&runtime).is_err() {
736                return;
737            }
738        }
739        self.final_task_runtime = Some(runtime);
740    }
741
742    /// Sets configuration for the live dual-era HTTP endpoint.
743    ///
744    /// # Example
745    ///
746    /// ```ignore
747    /// use fastmcp_server::HttpServerConfig;
748    ///
749    /// Server::new("demo", "1.0")
750    ///     .http_config(HttpServerConfig::new().mcp_path("/api/mcp").max_connections(128))
751    ///     .build();
752    /// ```
753    #[must_use]
754    pub fn http_config(mut self, config: HttpServerConfig) -> Self {
755        self.http_config = config;
756        self
757    }
758
759    /// Installs immutable OAuth authorization, token, and revocation routes
760    /// into the native HTTP listener.
761    ///
762    /// The routes retain an explicit public HTTPS endpoint base and are
763    /// admitted before MCP request conversion. OIDC/JWKS/ID-token routes are
764    /// deliberately not installed here.
765    #[must_use]
766    pub fn oauth_http_routes(mut self, routes: OAuthHttpRoutes) -> Self {
767        self.oauth_http_routes = Some(routes);
768        self
769    }
770
771    /// Builds a live modern Streamable HTTP endpoint.
772    #[cfg(not(any(feature = "legacy-2024-11-05", test)))]
773    pub fn build_http_endpoint(
774        self,
775    ) -> Result<crate::ServerHttpEndpoint, crate::ServerHttpEndpointError> {
776        self.try_build()
777            .map_err(|error| {
778                crate::ServerHttpEndpointError::InvalidConfiguration(error.to_string())
779            })?
780            .into_http_endpoint()
781    }
782
783    /// Builds a live dual-era HTTP endpoint with an exact legacy SSE origin.
784    ///
785    /// The modern route remains at [`HttpServerConfig::mcp_path`], while the
786    /// exact MCP 2024-11-05 SSE route advertises `legacy_origin` plus the
787    /// configured legacy message path.
788    #[cfg(any(feature = "legacy-2024-11-05", test))]
789    pub fn build_http_endpoint(
790        self,
791        legacy_origin: impl Into<String>,
792    ) -> Result<crate::ServerHttpEndpoint, crate::ServerHttpEndpointError> {
793        self.try_build()
794            .map_err(|error| {
795                crate::ServerHttpEndpointError::InvalidConfiguration(error.to_string())
796            })?
797            .into_http_endpoint(legacy_origin)
798    }
799
800    /// Registers a middleware.
801    #[must_use]
802    pub fn middleware<M: crate::Middleware + 'static>(mut self, middleware: M) -> Self {
803        self.middleware.push(Box::new(middleware));
804        self
805    }
806
807    /// Registers a tool handler.
808    ///
809    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
810    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
811    /// an error will be logged and the tool will not be registered.
812    #[must_use]
813    pub fn tool<H: ToolHandler + 'static>(mut self, handler: H) -> Self {
814        if let Err(e) = self
815            .router
816            .add_tool_with_behavior(handler, self.on_duplicate)
817        {
818            log::error!(
819                target: "fastmcp_rust::builder",
820                "Failed to register tool; code={:?}",
821                e.code
822            );
823        } else {
824            self.advertise_legacy_tools_list_changed();
825        }
826        self
827    }
828
829    /// Registers an intentionally exact MCP 2024-11-05-only tool handler.
830    ///
831    /// This does not depend on builder call order or the connection protocol
832    /// policy. The tool is available through exact legacy list/call routes and
833    /// omitted from all MCP 2026-07-28 catalogs and dispatch. Use [`Self::tool`]
834    /// for ordinary dual-era registration; it never falls back to this path
835    /// when final schema admission fails.
836    #[must_use]
837    pub fn legacy_tool<H: ToolHandler + 'static>(mut self, handler: H) -> Self {
838        if let Err(error) = self
839            .router
840            .add_legacy_tool_with_behavior(handler, self.on_duplicate)
841        {
842            log::error!(
843                target: "fastmcp_rust::builder",
844                "Failed to register exact-2024-only tool; code={:?}",
845                error.code
846            );
847        } else {
848            self.advertise_legacy_tools_list_changed();
849        }
850        self
851    }
852
853    /// Registers a resource handler.
854    ///
855    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
856    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
857    /// an error will be logged and the resource will not be registered.
858    #[must_use]
859    pub fn resource<H: ResourceHandler + 'static>(mut self, handler: H) -> Self {
860        if let Err(e) = self
861            .router
862            .add_resource_with_behavior(handler, self.on_duplicate)
863        {
864            log::error!(
865                target: "fastmcp_rust::builder",
866                "Failed to register resource; code={:?}",
867                e.code
868            );
869        } else {
870            self.advertise_legacy_resource_subscriptions();
871        }
872        self
873    }
874
875    /// Advertises the `resources.subscribe` capability so clients may
876    /// subscribe to registered resource URIs.
877    ///
878    /// Registering a resource or template already advertises subscribe because
879    /// session and exact-2024 dispatch serve `resources/subscribe` for those
880    /// URIs. This remains for servers that want the capability visible before
881    /// any catalog entry is installed.
882    #[must_use]
883    pub fn resource_subscriptions(mut self) -> Self {
884        self.advertise_legacy_resource_subscriptions();
885        self
886    }
887
888    fn advertise_legacy_resource_subscriptions(&mut self) {
889        let resources = self
890            .capabilities
891            .resources
892            .get_or_insert_with(ResourcesCapability::default);
893        resources.subscribe = true;
894        resources.list_changed = true;
895    }
896
897    fn advertise_legacy_tools_list_changed(&mut self) {
898        self.capabilities
899            .tools
900            .get_or_insert_with(ToolsCapability::default)
901            .list_changed = true;
902    }
903
904    fn advertise_legacy_prompts_list_changed(&mut self) {
905        self.capabilities
906            .prompts
907            .get_or_insert_with(PromptsCapability::default)
908            .list_changed = true;
909    }
910
911    fn advertise_completions(&mut self) {
912        self.capabilities.completions = Some(fastmcp_protocol::CompletionsCapability::default());
913    }
914
915    /// Registers an intentionally exact MCP 2024-11-05-only resource.
916    #[must_use]
917    pub fn legacy_resource<H: ResourceHandler + 'static>(mut self, handler: H) -> Self {
918        if let Err(error) = self
919            .router
920            .add_legacy_resource_with_behavior(handler, self.on_duplicate)
921        {
922            log::error!(
923                target: "fastmcp_rust::builder",
924                "Failed to register exact-2024-only resource; code={:?}",
925                error.code
926            );
927        } else {
928            self.advertise_legacy_resource_subscriptions();
929        }
930        self
931    }
932
933    /// Registers a resource template.
934    ///
935    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
936    /// With [`DuplicateBehavior::Error`], a conflicting template is rejected
937    /// and logged while builder construction continues.
938    #[must_use]
939    pub fn resource_template(mut self, template: ResourceTemplate) -> Self {
940        if let Err(error) = self
941            .router
942            .add_resource_template_with_behavior(template, self.on_duplicate)
943        {
944            log::error!(
945                target: "fastmcp_rust::builder",
946                "Failed to register resource template; code={:?}",
947                error.code
948            );
949        } else {
950            self.advertise_legacy_resource_subscriptions();
951        }
952        self
953    }
954
955    /// Registers an intentionally exact MCP 2024-11-05-only resource template.
956    #[must_use]
957    pub fn legacy_resource_template(mut self, template: ResourceTemplate) -> Self {
958        if let Err(error) = self
959            .router
960            .add_legacy_resource_template_with_behavior(template, self.on_duplicate)
961        {
962            log::error!(
963                target: "fastmcp_rust::builder",
964                "Failed to register exact-2024-only resource template; code={:?}",
965                error.code
966            );
967        } else {
968            self.advertise_legacy_resource_subscriptions();
969        }
970        self
971    }
972
973    /// Registers a prompt handler.
974    ///
975    /// Duplicate handling is controlled by [`on_duplicate`](Self::on_duplicate).
976    /// If [`DuplicateBehavior::Error`] is set and a duplicate is found,
977    /// an error will be logged and the prompt will not be registered.
978    #[must_use]
979    pub fn prompt<H: PromptHandler + 'static>(mut self, handler: H) -> Self {
980        if let Err(e) = self
981            .router
982            .add_prompt_with_behavior(handler, self.on_duplicate)
983        {
984            log::error!(
985                target: "fastmcp_rust::builder",
986                "Failed to register prompt; code={:?}",
987                e.code
988            );
989        } else {
990            self.advertise_legacy_prompts_list_changed();
991        }
992        self
993    }
994
995    /// Registers an intentionally exact MCP 2024-11-05-only prompt.
996    #[must_use]
997    pub fn legacy_prompt<H: PromptHandler + 'static>(mut self, handler: H) -> Self {
998        if let Err(error) = self
999            .router
1000            .add_legacy_prompt_with_behavior(handler, self.on_duplicate)
1001        {
1002            log::error!(
1003                target: "fastmcp_rust::builder",
1004                "Failed to register exact-2024-only prompt; code={:?}",
1005                error.code
1006            );
1007        } else {
1008            self.advertise_legacy_prompts_list_changed();
1009        }
1010        self
1011    }
1012
1013    /// Registers the server-wide `completion/complete` handler.
1014    ///
1015    /// The handler receives disjoint exact-legacy and final request parameter
1016    /// types. Building with a handler installs the real router dispatch target,
1017    /// which is the sole condition that enables final discovery's
1018    /// `capabilities.completions` claim.
1019    #[must_use]
1020    pub fn completion_handler<H: CompletionHandler + 'static>(mut self, handler: H) -> Self {
1021        self.router.add_completion_handler(handler);
1022        self.advertise_completions();
1023        self
1024    }
1025
1026    /// Registers a completion handler for exact MCP 2024-11-05 dispatch only.
1027    ///
1028    /// Initialize advertises `capabilities.completions` so a 2024-11-05 client
1029    /// can discover `completion/complete`. Final `server/discover` still omits
1030    /// completions unless a modern handler is also installed.
1031    #[must_use]
1032    pub fn legacy_completion_handler<H: CompletionHandler + 'static>(mut self, handler: H) -> Self {
1033        self.router.add_legacy_completion_handler(handler);
1034        self.advertise_completions();
1035        self
1036    }
1037
1038    /// Registers a final completion provider for one exact prompt name.
1039    ///
1040    /// Final `completion/complete` dispatch validates the referenced prompt
1041    /// and argument before selecting this provider. Exact MCP 2024-11-05
1042    /// completion remains on [`Self::completion_handler`] or
1043    /// [`Self::legacy_completion_handler`].
1044    #[must_use]
1045    pub fn prompt_completion_handler<H: CompletionHandler + 'static>(
1046        mut self,
1047        prompt_name: impl Into<String>,
1048        handler: H,
1049    ) -> Self {
1050        self.router
1051            .add_prompt_completion_handler(prompt_name, handler);
1052        self
1053    }
1054
1055    /// Registers a final completion provider for one exact resource-template URI.
1056    ///
1057    /// Final dispatch admits the registered resource template and requested
1058    /// template variable before selecting this provider.
1059    #[must_use]
1060    pub fn resource_template_completion_handler<H: CompletionHandler + 'static>(
1061        mut self,
1062        uri_template: impl Into<String>,
1063        handler: H,
1064    ) -> Self {
1065        self.router
1066            .add_resource_template_completion_handler(uri_template, handler);
1067        self
1068    }
1069
1070    /// Registers an exact MCP 2024-11-05 completion provider for one resource
1071    /// template URI.
1072    ///
1073    /// Exact-2024 dispatch selects this provider before the server-wide
1074    /// [`Self::legacy_completion_handler`] fallback. Initialize advertises
1075    /// `capabilities.completions` so a 2024-11-05 client can discover
1076    /// `completion/complete`.
1077    #[must_use]
1078    pub fn legacy_resource_template_completion_handler<H: CompletionHandler + 'static>(
1079        mut self,
1080        uri_template: impl Into<String>,
1081        handler: H,
1082    ) -> Self {
1083        self.router
1084            .add_legacy_resource_template_completion_handler(uri_template, handler);
1085        self.advertise_completions();
1086        self
1087    }
1088
1089    /// Returns whether this proxy registration will own the exact prompt
1090    /// target after the configured duplicate policy is applied.
1091    ///
1092    /// Router registration intentionally returns `Ok(())` when `Warn` or
1093    /// `Ignore` retains an existing target. Completion providers must not use
1094    /// that successful no-op as authorization to bind an upstream to the
1095    /// retained local target.
1096    #[cfg(feature = "proxy")]
1097    fn proxy_prompt_wins_admission(&self, name: &str) -> bool {
1098        self.router.get_prompt(name).is_none() || self.on_duplicate == DuplicateBehavior::Replace
1099    }
1100
1101    /// Returns whether this proxy registration will own the exact resource
1102    /// template target after the configured duplicate policy is applied.
1103    ///
1104    /// See [`Self::proxy_prompt_wins_admission`] for why `Ok(())` alone is
1105    /// insufficient for proxy completion binding.
1106    #[cfg(feature = "proxy")]
1107    fn proxy_resource_template_wins_admission(&self, uri_template: &str) -> bool {
1108        self.router.get_resource_template(uri_template).is_none()
1109            || self.on_duplicate == DuplicateBehavior::Replace
1110    }
1111
1112    /// Skips one colliding catalog member under [`DuplicateBehavior::Error`].
1113    /// Other admission failures still fail `proxy` / `as_proxy_typed` so a
1114    /// schema or metadata defect is not logged away as a duplicate skip.
1115    #[cfg(feature = "proxy")]
1116    fn absorb_prefixed_proxy_duplicate(
1117        error: fastmcp_core::McpError,
1118    ) -> Result<(), fastmcp_core::McpError> {
1119        if crate::router::is_duplicate_registration_error(&error) {
1120            Ok(())
1121        } else {
1122            Err(error)
1123        }
1124    }
1125
1126    /// Installs one route-bound modern Tasks relay into every server surface
1127    /// that owns Tasks discovery and dispatch. Exact-2024 never supplies a
1128    /// relay, so callers pass `None` for its selected upstream era.
1129    #[cfg(all(feature = "proxy", feature = "tasks"))]
1130    fn install_proxy_final_tasks_relay(
1131        &mut self,
1132        task_relay: Option<Arc<ProxyFinalTaskRelay>>,
1133    ) -> McpResult<()> {
1134        let Some(task_relay) = task_relay else {
1135            return Ok(());
1136        };
1137        if self.final_task_runtime.is_some() || self.final_task_relay.is_some() {
1138            return Err(fastmcp_core::McpError::invalid_request(
1139                "a server may install only one local or route-bound final Tasks service",
1140            ));
1141        }
1142        if let Some(extension_runtime) = self.extension_runtime.as_mut() {
1143            extension_runtime
1144                .install_proxy_final_tasks(Arc::clone(&task_relay))
1145                .map_err(|error| fastmcp_core::McpError::invalid_request(error.to_string()))?;
1146        }
1147        self.router
1148            .set_final_task_relay(Some(Arc::clone(&task_relay)));
1149        self.final_task_relay = Some(task_relay);
1150        Ok(())
1151    }
1152
1153    /// Registers proxy handlers for a remote MCP server.
1154    ///
1155    /// Use [`ProxyClient::catalog`] to fetch definitions before calling this
1156    /// method. A caller-supplied catalog must agree with an already-selected
1157    /// transport binding or backend-observed era; it cannot bind an unbound
1158    /// proxy route itself.
1159    ///
1160    /// # Errors
1161    ///
1162    /// Returns an error when the catalog is malformed, contradicts the route,
1163    /// or attempts to establish an era without upstream evidence.
1164    #[cfg(feature = "proxy")]
1165    pub fn proxy(mut self, client: ProxyClient, catalog: ProxyCatalog) -> McpResult<Self> {
1166        client.admit_catalog(&catalog)?;
1167        let catalog_era = catalog.era()?;
1168        let completion_supported = match client.supports_completion() {
1169            Ok(supported) => supported,
1170            Err(error) => {
1171                log::error!(
1172                    target: "fastmcp_rust::builder",
1173                    "Failed to determine proxied completion support; code={:?}",
1174                    error.code
1175                );
1176                false
1177            }
1178        };
1179        // An admitted final-only catalog owns the same public core capability
1180        // claims as its legacy counterpart. Registration below remains era
1181        // separated; this only makes discovery reflect the handlers that were
1182        // actually installed.
1183        let has_tools = !catalog.tools.is_empty() || !catalog.final_tools.is_empty();
1184        let has_resources = !catalog.resources.is_empty()
1185            || !catalog.resource_templates.is_empty()
1186            || !catalog.final_resources.is_empty()
1187            || !catalog.final_resource_templates.is_empty();
1188        let has_prompts = !catalog.prompts.is_empty() || !catalog.final_prompts.is_empty();
1189        #[cfg(feature = "tasks")]
1190        let task_relay = if catalog_era == ProtocolEra::Modern2026 {
1191            client.final_tasks_relay()?
1192        } else {
1193            None
1194        };
1195        #[cfg(feature = "tasks")]
1196        self.install_proxy_final_tasks_relay(task_relay.clone())?;
1197        #[cfg(not(feature = "tasks"))]
1198        let final_handlers = catalog.final_tool_handlers(client.clone())?;
1199        #[cfg(feature = "tasks")]
1200        let final_handlers = catalog
1201            .final_tools
1202            .iter()
1203            .cloned()
1204            .map(|tool| match task_relay.as_ref() {
1205                Some(task_relay) => ProxyToolHandler::from_final_with_task_relay(
1206                    tool,
1207                    client.clone(),
1208                    Arc::clone(task_relay),
1209                ),
1210                None => ProxyToolHandler::from_final(tool, client.clone()),
1211            })
1212            .collect::<McpResult<Vec<_>>>()?;
1213
1214        // Legacy catalog entries register exact-2024-only: dual-era proxy
1215        // composition must not promote a legacy upstream's components into
1216        // the final catalog beside a modern upstream's own registrations.
1217        for tool in catalog.tools {
1218            if let Err(error) = self.router.add_legacy_tool_with_behavior(
1219                ProxyToolHandler::new(tool, client.clone()),
1220                self.on_duplicate,
1221            ) {
1222                log::error!(
1223                    target: "fastmcp_rust::builder",
1224                    "Failed to register proxied tool; code={:?}",
1225                    error.code
1226                );
1227                Self::absorb_prefixed_proxy_duplicate(error)?;
1228            }
1229        }
1230
1231        for handler in final_handlers {
1232            match self
1233                .router
1234                .add_final_tool_with_behavior(handler, self.on_duplicate)
1235            {
1236                Ok(()) => {}
1237                Err(error) => {
1238                    log::error!(
1239                        target: "fastmcp_rust::builder",
1240                        "Failed to register exact-final proxied tool; code={:?}",
1241                        error.code
1242                    );
1243                    Self::absorb_prefixed_proxy_duplicate(error)?;
1244                }
1245            }
1246        }
1247
1248        for resource in catalog.resources {
1249            if let Err(error) = self.router.add_legacy_resource_with_behavior(
1250                ProxyResourceHandler::new(resource, client.clone()),
1251                self.on_duplicate,
1252            ) {
1253                log::error!(
1254                    target: "fastmcp_rust::builder",
1255                    "Failed to register proxied resource; code={:?}",
1256                    error.code
1257                );
1258                Self::absorb_prefixed_proxy_duplicate(error)?;
1259            }
1260        }
1261
1262        for template in catalog.resource_templates {
1263            let downstream_uri = template.uri_template.clone();
1264            let completion_target_admitted =
1265                self.proxy_resource_template_wins_admission(&downstream_uri);
1266            match self.router.add_legacy_resource_with_behavior(
1267                ProxyResourceHandler::from_template(template, client.clone()),
1268                self.on_duplicate,
1269            ) {
1270                Ok(()) if completion_target_admitted => {
1271                    if completion_supported && catalog_era == ProtocolEra::Legacy2024 {
1272                        self.router.add_legacy_resource_template_completion_handler(
1273                            downstream_uri.clone(),
1274                            ProxyCompletionHandler::for_resource_template(
1275                                client.clone(),
1276                                downstream_uri.clone(),
1277                                downstream_uri,
1278                            ),
1279                        );
1280                        self.advertise_completions();
1281                    }
1282                }
1283                Ok(()) => {}
1284                Err(error) => {
1285                    log::error!(
1286                        target: "fastmcp_rust::builder",
1287                        "Failed to register proxied resource template; code={:?}",
1288                        error.code
1289                    );
1290                    Self::absorb_prefixed_proxy_duplicate(error)?;
1291                }
1292            }
1293        }
1294
1295        for prompt in catalog.prompts {
1296            let downstream_name = prompt.name.clone();
1297            let completion_target_admitted = self.proxy_prompt_wins_admission(&downstream_name);
1298            match self.router.add_legacy_prompt_with_behavior(
1299                ProxyPromptHandler::new(prompt, client.clone()),
1300                self.on_duplicate,
1301            ) {
1302                Ok(()) if completion_target_admitted => {
1303                    if completion_supported && catalog_era == ProtocolEra::Legacy2024 {
1304                        self.router.add_legacy_prompt_completion_handler(
1305                            downstream_name.clone(),
1306                            ProxyCompletionHandler::for_prompt(
1307                                client.clone(),
1308                                downstream_name.clone(),
1309                                downstream_name,
1310                            ),
1311                        );
1312                        self.advertise_completions();
1313                    }
1314                }
1315                Ok(()) => {}
1316                Err(error) => {
1317                    log::error!(
1318                        target: "fastmcp_rust::builder",
1319                        "Failed to register proxied prompt; code={:?}",
1320                        error.code
1321                    );
1322                    Self::absorb_prefixed_proxy_duplicate(error)?;
1323                }
1324            }
1325        }
1326
1327        for resource in catalog.final_resources {
1328            if let Err(error) = self.router.add_final_resource_with_behavior(
1329                FinalProxyResourceHandler::new(resource, client.clone()),
1330                self.on_duplicate,
1331            ) {
1332                log::error!(
1333                    target: "fastmcp_rust::builder",
1334                    "Failed to register exact-final proxied resource; code={:?}",
1335                    error.code
1336                );
1337                Self::absorb_prefixed_proxy_duplicate(error)?;
1338            }
1339        }
1340
1341        for template in catalog.final_resource_templates {
1342            let downstream_uri = template.uri_template.clone();
1343            let completion_target_admitted =
1344                self.proxy_resource_template_wins_admission(&downstream_uri);
1345            match self.router.add_final_resource_with_behavior(
1346                FinalProxyResourceTemplateHandler::new(template, client.clone()),
1347                self.on_duplicate,
1348            ) {
1349                Ok(())
1350                    if completion_target_admitted
1351                        && completion_supported
1352                        && catalog_era == ProtocolEra::Modern2026 =>
1353                {
1354                    self.router.add_resource_template_completion_handler(
1355                        downstream_uri.clone(),
1356                        ProxyCompletionHandler::for_resource_template(
1357                            client.clone(),
1358                            downstream_uri.clone(),
1359                            downstream_uri,
1360                        ),
1361                    );
1362                    self.advertise_completions();
1363                }
1364                Ok(()) => {}
1365                Err(error) => {
1366                    log::error!(
1367                        target: "fastmcp_rust::builder",
1368                        "Failed to register exact-final proxied resource template; code={:?}",
1369                        error.code
1370                    );
1371                    Self::absorb_prefixed_proxy_duplicate(error)?;
1372                }
1373            }
1374        }
1375
1376        for prompt in catalog.final_prompts {
1377            let downstream_name = prompt.name.clone();
1378            let completion_target_admitted = self.proxy_prompt_wins_admission(&downstream_name);
1379            match self.router.add_final_prompt_with_behavior(
1380                FinalProxyPromptHandler::new(prompt, client.clone()),
1381                self.on_duplicate,
1382            ) {
1383                Ok(())
1384                    if completion_target_admitted
1385                        && completion_supported
1386                        && catalog_era == ProtocolEra::Modern2026 =>
1387                {
1388                    self.router.add_prompt_completion_handler(
1389                        downstream_name.clone(),
1390                        ProxyCompletionHandler::for_prompt(
1391                            client.clone(),
1392                            downstream_name.clone(),
1393                            downstream_name,
1394                        ),
1395                    );
1396                    self.advertise_completions();
1397                }
1398                Ok(()) => {}
1399                Err(error) => {
1400                    log::error!(
1401                        target: "fastmcp_rust::builder",
1402                        "Failed to register exact-final proxied prompt; code={:?}",
1403                        error.code
1404                    );
1405                    Self::absorb_prefixed_proxy_duplicate(error)?;
1406                }
1407            }
1408        }
1409
1410        if has_tools {
1411            self.advertise_legacy_tools_list_changed();
1412        }
1413        if has_resources {
1414            self.advertise_legacy_resource_subscriptions();
1415        }
1416        if has_prompts {
1417            self.advertise_legacy_prompts_list_changed();
1418        }
1419
1420        Ok(self)
1421    }
1422
1423    /// Creates a proxy to an external MCP server with automatic discovery.
1424    ///
1425    /// This is a convenience method that combines connection, discovery, and
1426    /// handler registration. The client should already be initialized (connected
1427    /// to the server).
1428    ///
1429    /// All tools, resources, and prompts from the external server are registered
1430    /// as proxy handlers with the specified prefix.
1431    ///
1432    /// # Example
1433    ///
1434    /// ```ignore
1435    /// use fastmcp_client::Client;
1436    ///
1437    /// // Create and initialize client
1438    /// let mut client = Client::new(transport)?;
1439    /// client.initialize()?;
1440    ///
1441    /// // Create main server with proxy to external
1442    /// let main = Server::new("main", "1.0")
1443    ///     .tool(local_tool)
1444    ///     .as_proxy("ext", client)?    // ext/external_tool, etc.
1445    ///     .build();
1446    /// ```
1447    ///
1448    /// # Errors
1449    ///
1450    /// Returns an error if the catalog fetch fails. Duplicate-policy
1451    /// rejections of individual proxied components are logged and skipped so
1452    /// the rest of the catalog still installs.
1453    #[cfg(feature = "proxy")]
1454    pub fn as_proxy(
1455        self,
1456        prefix: &str,
1457        client: fastmcp_client::Client,
1458    ) -> Result<Self, fastmcp_core::McpError> {
1459        let proxy_client = ProxyClient::from_client(client)?;
1460        let catalog = proxy_client.catalog_typed()?;
1461        self.register_prefixed_typed_proxy_catalog(prefix, proxy_client, catalog)
1462    }
1463
1464    /// Registers one already-negotiated typed upstream catalog under a caller
1465    /// prefix. Tools and prompts take `{prefix}/{name}`; exact-final resource
1466    /// URIs stay unprefixed so they remain absolute.
1467    ///
1468    /// This is the HTTP-capable counterpart to [`as_proxy`](Self::as_proxy):
1469    /// a live `ProxyClient` from `connect_http_with_protocol_plan` already
1470    /// carries a ModernHttp/LegacyHttpSse binding, so it must not be rebuilt
1471    /// through `from_client` (which labels the adapter as stdio).
1472    ///
1473    /// # Errors
1474    ///
1475    /// Returns an error if the typed catalog is mixed-era or contradicts the
1476    /// selected route. Duplicate-policy rejections of individual proxied
1477    /// components are logged and skipped so the rest of the catalog still
1478    /// installs.
1479    #[cfg(feature = "proxy")]
1480    pub fn as_proxy_typed(
1481        self,
1482        prefix: &str,
1483        proxy_client: ProxyClient,
1484        catalog: ProxyTypedCatalog,
1485    ) -> Result<Self, fastmcp_core::McpError> {
1486        self.register_prefixed_typed_proxy_catalog(prefix, proxy_client, catalog)
1487    }
1488
1489    /// Copies nonempty upstream initialize/discover instructions onto this
1490    /// gateway when the builder has not already set its own string.
1491    #[cfg(feature = "proxy")]
1492    fn adopt_upstream_proxy_instructions(
1493        &mut self,
1494        proxy_client: &ProxyClient,
1495    ) -> Result<(), fastmcp_core::McpError> {
1496        if self
1497            .instructions
1498            .as_ref()
1499            .is_some_and(|instructions| !instructions.is_empty())
1500        {
1501            return Ok(());
1502        }
1503        if let Some(instructions) = proxy_client.upstream_instructions()?
1504            && !instructions.is_empty()
1505        {
1506            self.instructions = Some(instructions);
1507        }
1508        Ok(())
1509    }
1510
1511    /// Copies upstream modern Implementation extras onto this gateway when the
1512    /// builder has not already set title, description, website, or icons.
1513    ///
1514    /// The gateway keeps its own `name`/`version`. Exact-2024 initialize stays
1515    /// name/version-only.
1516    #[cfg(feature = "proxy")]
1517    fn adopt_upstream_proxy_implementation(
1518        &mut self,
1519        proxy_client: &ProxyClient,
1520    ) -> Result<(), fastmcp_core::McpError> {
1521        let gateway_has_extras = self.title.as_ref().is_some_and(|title| !title.is_empty())
1522            || self
1523                .description
1524                .as_ref()
1525                .is_some_and(|description| !description.is_empty())
1526            || self
1527                .website_url
1528                .as_ref()
1529                .is_some_and(|website| !website.is_empty())
1530            || !self.icons.is_empty();
1531        if gateway_has_extras {
1532            return Ok(());
1533        }
1534        let Some(implementation) = proxy_client.upstream_implementation()? else {
1535            return Ok(());
1536        };
1537        if implementation.title.is_none()
1538            && implementation.description.is_none()
1539            && implementation.website_url.is_none()
1540            && implementation.icons.is_empty()
1541        {
1542            return Ok(());
1543        }
1544        self.title = implementation.title;
1545        self.description = implementation.description;
1546        self.website_url = implementation
1547            .website_url
1548            .map(|uri| uri.as_str().to_owned());
1549        self.icons = implementation.icons;
1550        Ok(())
1551    }
1552
1553    #[cfg(feature = "proxy")]
1554    fn register_prefixed_typed_proxy_catalog(
1555        mut self,
1556        prefix: &str,
1557        proxy_client: ProxyClient,
1558        catalog: ProxyTypedCatalog,
1559    ) -> Result<Self, fastmcp_core::McpError> {
1560        proxy_client.admit_typed_catalog(&catalog)?;
1561        self.adopt_upstream_proxy_instructions(&proxy_client)?;
1562        self.adopt_upstream_proxy_implementation(&proxy_client)?;
1563        let completion_supported = proxy_client.supports_completion()?;
1564        #[cfg(feature = "tasks")]
1565        let catalog_era = catalog.era()?;
1566        #[cfg(feature = "tasks")]
1567        let task_relay = if catalog_era == ProtocolEra::Modern2026 {
1568            proxy_client.final_tasks_relay()?
1569        } else {
1570            None
1571        };
1572        #[cfg(feature = "tasks")]
1573        self.install_proxy_final_tasks_relay(task_relay.clone())?;
1574        let (tool_count, resource_count, template_count, prompt_count) = match catalog {
1575            ProxyTypedCatalog {
1576                tools: ProxyToolCatalog::Legacy(tools),
1577                resources: ProxyResourceCatalog::Legacy(resources),
1578                resource_templates: ProxyResourceTemplateCatalog::Legacy(resource_templates),
1579                prompts: ProxyPromptCatalog::Legacy(prompts),
1580            } => {
1581                let counts = (
1582                    tools.len(),
1583                    resources.len(),
1584                    resource_templates.len(),
1585                    prompts.len(),
1586                );
1587                // Prefixed legacy keys (`{prefix}/{uri}`) are not absolute
1588                // final URIs. Register them exact-2024-only so they are not
1589                // projected into the modern catalog.
1590                for tool in tools {
1591                    if let Err(error) = self.router.add_legacy_tool_with_behavior(
1592                        ProxyToolHandler::with_prefix(tool, prefix, proxy_client.clone()),
1593                        self.on_duplicate,
1594                    ) {
1595                        log::error!(
1596                            target: "fastmcp_rust::builder",
1597                            "Failed to register prefixed proxied tool; code={:?}",
1598                            error.code
1599                        );
1600                        Self::absorb_prefixed_proxy_duplicate(error)?;
1601                    }
1602                }
1603                for resource in resources {
1604                    if let Err(error) = self.router.add_legacy_resource_with_behavior(
1605                        ProxyResourceHandler::with_prefix(resource, prefix, proxy_client.clone()),
1606                        self.on_duplicate,
1607                    ) {
1608                        log::error!(
1609                            target: "fastmcp_rust::builder",
1610                            "Failed to register prefixed proxied resource; code={:?}",
1611                            error.code
1612                        );
1613                        Self::absorb_prefixed_proxy_duplicate(error)?;
1614                    }
1615                }
1616                for template in resource_templates {
1617                    let upstream_uri = template.uri_template.clone();
1618                    let downstream_uri = format!("{prefix}/{upstream_uri}");
1619                    let completion_target_admitted =
1620                        self.proxy_resource_template_wins_admission(&downstream_uri);
1621                    match self.router.add_legacy_resource_with_behavior(
1622                        ProxyResourceHandler::from_template_with_prefix(
1623                            template,
1624                            prefix,
1625                            proxy_client.clone(),
1626                        ),
1627                        self.on_duplicate,
1628                    ) {
1629                        Ok(()) if completion_target_admitted && completion_supported => {
1630                            self.router.add_legacy_resource_template_completion_handler(
1631                                downstream_uri.clone(),
1632                                ProxyCompletionHandler::for_resource_template(
1633                                    proxy_client.clone(),
1634                                    downstream_uri,
1635                                    upstream_uri,
1636                                ),
1637                            );
1638                            self.advertise_completions();
1639                        }
1640                        Ok(()) => {}
1641                        Err(error) => {
1642                            log::error!(
1643                                target: "fastmcp_rust::builder",
1644                                "Failed to register prefixed proxied resource template; code={:?}",
1645                                error.code
1646                            );
1647                            Self::absorb_prefixed_proxy_duplicate(error)?;
1648                        }
1649                    }
1650                }
1651                for prompt in prompts {
1652                    let upstream_name = prompt.name.clone();
1653                    let downstream_name = format!("{prefix}/{upstream_name}");
1654                    let completion_target_admitted =
1655                        self.proxy_prompt_wins_admission(&downstream_name);
1656                    match self.router.add_legacy_prompt_with_behavior(
1657                        ProxyPromptHandler::with_prefix(prompt, prefix, proxy_client.clone()),
1658                        self.on_duplicate,
1659                    ) {
1660                        Ok(()) if completion_target_admitted && completion_supported => {
1661                            self.router.add_legacy_prompt_completion_handler(
1662                                downstream_name.clone(),
1663                                ProxyCompletionHandler::for_prompt(
1664                                    proxy_client.clone(),
1665                                    downstream_name,
1666                                    upstream_name,
1667                                ),
1668                            );
1669                            self.advertise_completions();
1670                        }
1671                        Ok(()) => {}
1672                        Err(error) => {
1673                            log::error!(
1674                                target: "fastmcp_rust::builder",
1675                                "Failed to register prefixed proxied prompt; code={:?}",
1676                                error.code
1677                            );
1678                            Self::absorb_prefixed_proxy_duplicate(error)?;
1679                        }
1680                    }
1681                }
1682                counts
1683            }
1684            ProxyTypedCatalog {
1685                tools: ProxyToolCatalog::Final(tools),
1686                resources: ProxyResourceCatalog::Final(resources),
1687                resource_templates: ProxyResourceTemplateCatalog::Final(resource_templates),
1688                prompts: ProxyPromptCatalog::Final(prompts),
1689            } => {
1690                // Tools and prompts take the caller prefix. Exact-final resource
1691                // URIs stay unprefixed so they remain absolute; prefixing them
1692                // as `{prefix}/{uri}` would drop them from the modern catalog.
1693                let counts = (
1694                    tools.len(),
1695                    resources.len(),
1696                    resource_templates.len(),
1697                    prompts.len(),
1698                );
1699                for tool in tools {
1700                    #[cfg(feature = "tasks")]
1701                    let handler = match task_relay.as_ref() {
1702                        Some(task_relay) => ProxyToolHandler::with_prefix_final_with_task_relay(
1703                            tool,
1704                            prefix,
1705                            proxy_client.clone(),
1706                            Arc::clone(task_relay),
1707                        )?,
1708                        None => {
1709                            ProxyToolHandler::with_prefix_final(tool, prefix, proxy_client.clone())?
1710                        }
1711                    };
1712                    #[cfg(not(feature = "tasks"))]
1713                    let handler =
1714                        ProxyToolHandler::with_prefix_final(tool, prefix, proxy_client.clone())?;
1715                    if let Err(error) = self
1716                        .router
1717                        .add_final_tool_with_behavior(handler, self.on_duplicate)
1718                    {
1719                        log::error!(
1720                            target: "fastmcp_rust::builder",
1721                            "Failed to register prefixed exact-final proxied tool; code={:?}",
1722                            error.code
1723                        );
1724                        Self::absorb_prefixed_proxy_duplicate(error)?;
1725                    }
1726                }
1727                for resource in resources {
1728                    if let Err(error) = self.router.add_final_resource_with_behavior(
1729                        FinalProxyResourceHandler::new(resource, proxy_client.clone()),
1730                        self.on_duplicate,
1731                    ) {
1732                        log::error!(
1733                            target: "fastmcp_rust::builder",
1734                            "Failed to register prefixed exact-final proxied resource; code={:?}",
1735                            error.code
1736                        );
1737                        Self::absorb_prefixed_proxy_duplicate(error)?;
1738                    }
1739                }
1740                for template in resource_templates {
1741                    let downstream_uri = template.uri_template.clone();
1742                    let completion_target_admitted =
1743                        self.proxy_resource_template_wins_admission(&downstream_uri);
1744                    match self.router.add_final_resource_with_behavior(
1745                        FinalProxyResourceTemplateHandler::new(template, proxy_client.clone()),
1746                        self.on_duplicate,
1747                    ) {
1748                        Ok(()) if completion_target_admitted && completion_supported => {
1749                            self.router.add_resource_template_completion_handler(
1750                                downstream_uri.clone(),
1751                                ProxyCompletionHandler::for_resource_template(
1752                                    proxy_client.clone(),
1753                                    downstream_uri.clone(),
1754                                    downstream_uri,
1755                                ),
1756                            );
1757                            self.advertise_completions();
1758                        }
1759                        Ok(()) => {}
1760                        Err(error) => {
1761                            log::error!(
1762                                target: "fastmcp_rust::builder",
1763                                "Failed to register prefixed exact-final proxied resource template; code={:?}",
1764                                error.code
1765                            );
1766                            Self::absorb_prefixed_proxy_duplicate(error)?;
1767                        }
1768                    }
1769                }
1770                for prompt in prompts {
1771                    let upstream_name = prompt.name.clone();
1772                    let downstream_name = format!("{prefix}/{upstream_name}");
1773                    let completion_target_admitted =
1774                        self.proxy_prompt_wins_admission(&downstream_name);
1775                    match self.router.add_final_prompt_with_behavior(
1776                        FinalProxyPromptHandler::with_prefix(prompt, prefix, proxy_client.clone()),
1777                        self.on_duplicate,
1778                    ) {
1779                        Ok(()) if completion_target_admitted && completion_supported => {
1780                            self.router.add_prompt_completion_handler(
1781                                downstream_name.clone(),
1782                                ProxyCompletionHandler::for_prompt(
1783                                    proxy_client.clone(),
1784                                    downstream_name,
1785                                    upstream_name,
1786                                ),
1787                            );
1788                            self.advertise_completions();
1789                        }
1790                        Ok(()) => {}
1791                        Err(error) => {
1792                            log::error!(
1793                                target: "fastmcp_rust::builder",
1794                                "Failed to register prefixed exact-final proxied prompt; code={:?}",
1795                                error.code
1796                            );
1797                            Self::absorb_prefixed_proxy_duplicate(error)?;
1798                        }
1799                    }
1800                }
1801                counts
1802            }
1803            _ => {
1804                return Err(fastmcp_core::McpError::invalid_request(
1805                    "proxy typed catalog mixes legacy and final component vectors",
1806                ));
1807            }
1808        };
1809
1810        let has_tools = tool_count > 0;
1811        let has_resources = resource_count > 0 || template_count > 0;
1812        let has_prompts = prompt_count > 0;
1813
1814        // Update capabilities
1815        if has_tools {
1816            self.advertise_legacy_tools_list_changed();
1817        }
1818        if has_resources {
1819            self.advertise_legacy_resource_subscriptions();
1820        }
1821        if has_prompts {
1822            self.advertise_legacy_prompts_list_changed();
1823        }
1824
1825        log::info!(
1826            target: "fastmcp_rust::proxy",
1827            "Proxied {} tools, {} resources, {} templates, and {} prompts with a configured prefix",
1828            tool_count,
1829            resource_count,
1830            template_count,
1831            prompt_count
1832        );
1833
1834        Ok(self)
1835    }
1836
1837    /// Creates a proxy to an external MCP server without a prefix.
1838    ///
1839    /// Similar to [`as_proxy`](Self::as_proxy), but tools/resources/prompts
1840    /// keep their original names. Use this when proxying a single external
1841    /// server or when you don't need namespace separation.
1842    ///
1843    /// # Example
1844    ///
1845    /// ```ignore
1846    /// let main = Server::new("main", "1.0")
1847    ///     .as_proxy_raw(client)?  // External tools appear with original names
1848    ///     .build();
1849    /// ```
1850    ///
1851    /// # Errors
1852    ///
1853    /// Returns an error if catalog discovery fails or the configured duplicate
1854    /// policy rejects any unprefixed proxy component.
1855    #[cfg(feature = "proxy")]
1856    pub fn as_proxy_raw(
1857        self,
1858        client: fastmcp_client::Client,
1859    ) -> Result<Self, fastmcp_core::McpError> {
1860        self.as_proxy_raw_with_proxy_client(ProxyClient::from_client(client)?)
1861    }
1862
1863    /// Registers one already-negotiated, typed upstream catalog without any
1864    /// legacy projection. Final components are visible only to final routes;
1865    /// legacy components remain visible only to exact MCP 2024-11-05 routes.
1866    ///
1867    /// A caller-provided catalog is accepted only when the client already has
1868    /// immutable transport binding or backend-observed era evidence. Call
1869    /// [`ProxyClient::catalog_typed`] first for an unbound custom backend.
1870    ///
1871    /// # Errors
1872    ///
1873    /// Returns an error when the typed catalog is mixed-era, contradicts the
1874    /// selected route, or tries to bind an otherwise unbound route.
1875    #[cfg(feature = "proxy")]
1876    pub fn proxy_typed(
1877        self,
1878        proxy_client: ProxyClient,
1879        catalog: ProxyTypedCatalog,
1880    ) -> Result<Self, fastmcp_core::McpError> {
1881        self.register_raw_typed_proxy_catalog(proxy_client, catalog)
1882    }
1883
1884    #[cfg(feature = "proxy")]
1885    fn as_proxy_raw_with_proxy_client(
1886        self,
1887        proxy_client: ProxyClient,
1888    ) -> Result<Self, fastmcp_core::McpError> {
1889        let catalog = proxy_client.catalog_typed()?;
1890        self.register_raw_typed_proxy_catalog(proxy_client, catalog)
1891    }
1892
1893    /// Registers one already-negotiated typed upstream catalog without
1894    /// projecting final resource/template/prompt definitions through their
1895    /// narrower legacy models.
1896    #[cfg(feature = "proxy")]
1897    fn register_raw_typed_proxy_catalog(
1898        mut self,
1899        proxy_client: ProxyClient,
1900        catalog: ProxyTypedCatalog,
1901    ) -> Result<Self, fastmcp_core::McpError> {
1902        // `catalog_typed` seals backend-observed evidence itself. A
1903        // caller-provided typed catalog must instead prove it agrees with an
1904        // already-selected transport or an earlier observed catalog; it may
1905        // not establish an era for an unbound proxy route.
1906        proxy_client.admit_typed_catalog(&catalog)?;
1907        self.adopt_upstream_proxy_instructions(&proxy_client)?;
1908        self.adopt_upstream_proxy_implementation(&proxy_client)?;
1909        let catalog_era = catalog.era()?;
1910        let completion_supported = proxy_client.supports_completion()?;
1911        #[cfg(feature = "tasks")]
1912        let task_relay = if catalog_era == ProtocolEra::Modern2026 {
1913            proxy_client.final_tasks_relay()?
1914        } else {
1915            None
1916        };
1917        #[cfg(feature = "tasks")]
1918        self.install_proxy_final_tasks_relay(task_relay.clone())?;
1919        match (
1920            catalog.tools,
1921            catalog.resources,
1922            catalog.resource_templates,
1923            catalog.prompts,
1924        ) {
1925            (
1926                ProxyToolCatalog::Legacy(tools),
1927                ProxyResourceCatalog::Legacy(resources),
1928                ProxyResourceTemplateCatalog::Legacy(resource_templates),
1929                ProxyPromptCatalog::Legacy(prompts),
1930            ) => {
1931                let has_tools = !tools.is_empty();
1932                let has_resources = !resources.is_empty() || !resource_templates.is_empty();
1933                let has_prompts = !prompts.is_empty();
1934                // A legacy upstream catalog registers exact-2024-only: dual-era
1935                // proxy composition must not promote its entries into the
1936                // final catalog beside a modern upstream's own registrations.
1937                for tool in tools {
1938                    self.router.add_legacy_tool_with_behavior(
1939                        ProxyToolHandler::new(tool, proxy_client.clone()),
1940                        self.on_duplicate,
1941                    )?;
1942                }
1943                for resource in resources {
1944                    self.router.add_legacy_resource_with_behavior(
1945                        ProxyResourceHandler::new(resource, proxy_client.clone()),
1946                        self.on_duplicate,
1947                    )?;
1948                }
1949                for template in resource_templates {
1950                    let downstream_uri = template.uri_template.clone();
1951                    let completion_target_admitted =
1952                        self.proxy_resource_template_wins_admission(&downstream_uri);
1953                    self.router.add_legacy_resource_with_behavior(
1954                        ProxyResourceHandler::from_template(template, proxy_client.clone()),
1955                        self.on_duplicate,
1956                    )?;
1957                    if completion_target_admitted && completion_supported {
1958                        self.router.add_legacy_resource_template_completion_handler(
1959                            downstream_uri.clone(),
1960                            ProxyCompletionHandler::for_resource_template(
1961                                proxy_client.clone(),
1962                                downstream_uri.clone(),
1963                                downstream_uri,
1964                            ),
1965                        );
1966                        self.advertise_completions();
1967                    }
1968                }
1969                for prompt in prompts {
1970                    let downstream_name = prompt.name.clone();
1971                    let completion_target_admitted =
1972                        self.proxy_prompt_wins_admission(&downstream_name);
1973                    self.router.add_legacy_prompt_with_behavior(
1974                        ProxyPromptHandler::new(prompt, proxy_client.clone()),
1975                        self.on_duplicate,
1976                    )?;
1977                    if completion_target_admitted && completion_supported {
1978                        self.router.add_legacy_prompt_completion_handler(
1979                            downstream_name.clone(),
1980                            ProxyCompletionHandler::for_prompt(
1981                                proxy_client.clone(),
1982                                downstream_name.clone(),
1983                                downstream_name,
1984                            ),
1985                        );
1986                        self.advertise_completions();
1987                    }
1988                }
1989                if has_tools {
1990                    self.advertise_legacy_tools_list_changed();
1991                }
1992                if has_resources {
1993                    self.advertise_legacy_resource_subscriptions();
1994                }
1995                if has_prompts {
1996                    self.advertise_legacy_prompts_list_changed();
1997                }
1998            }
1999            (
2000                ProxyToolCatalog::Final(tools),
2001                ProxyResourceCatalog::Final(resources),
2002                ProxyResourceTemplateCatalog::Final(resource_templates),
2003                ProxyPromptCatalog::Final(prompts),
2004            ) => {
2005                let has_tools = !tools.is_empty();
2006                let has_resources = !resources.is_empty() || !resource_templates.is_empty();
2007                let has_prompts = !prompts.is_empty();
2008                for tool in tools {
2009                    // Complete-only registration: a live echo/tool catalog must
2010                    // not require the official Tasks client extension merely
2011                    // because the upstream server advertises Tasks. The route
2012                    // still installs `final_task_relay` for `tasks/*` controls.
2013                    let handler = ProxyToolHandler::from_final(tool, proxy_client.clone())?;
2014                    self.router
2015                        .add_final_tool_with_behavior(handler, self.on_duplicate)?;
2016                }
2017                for resource in resources {
2018                    self.router.add_final_resource_with_behavior(
2019                        FinalProxyResourceHandler::new(resource, proxy_client.clone()),
2020                        self.on_duplicate,
2021                    )?;
2022                }
2023                for template in resource_templates {
2024                    let downstream_uri = template.uri_template.clone();
2025                    let completion_target_admitted =
2026                        self.proxy_resource_template_wins_admission(&downstream_uri);
2027                    self.router.add_final_resource_with_behavior(
2028                        FinalProxyResourceTemplateHandler::new(template, proxy_client.clone()),
2029                        self.on_duplicate,
2030                    )?;
2031                    if completion_target_admitted && completion_supported {
2032                        self.router.add_resource_template_completion_handler(
2033                            downstream_uri.clone(),
2034                            ProxyCompletionHandler::for_resource_template(
2035                                proxy_client.clone(),
2036                                downstream_uri.clone(),
2037                                downstream_uri,
2038                            ),
2039                        );
2040                        self.advertise_completions();
2041                    }
2042                }
2043                for prompt in prompts {
2044                    let downstream_name = prompt.name.clone();
2045                    let completion_target_admitted =
2046                        self.proxy_prompt_wins_admission(&downstream_name);
2047                    self.router.add_final_prompt_with_behavior(
2048                        FinalProxyPromptHandler::new(prompt, proxy_client.clone()),
2049                        self.on_duplicate,
2050                    )?;
2051                    if completion_target_admitted && completion_supported {
2052                        self.router.add_prompt_completion_handler(
2053                            downstream_name.clone(),
2054                            ProxyCompletionHandler::for_prompt(
2055                                proxy_client.clone(),
2056                                downstream_name.clone(),
2057                                downstream_name,
2058                            ),
2059                        );
2060                        self.advertise_completions();
2061                    }
2062                }
2063                if has_tools {
2064                    self.advertise_legacy_tools_list_changed();
2065                }
2066                if has_resources {
2067                    self.advertise_legacy_resource_subscriptions();
2068                }
2069                if has_prompts {
2070                    self.advertise_legacy_prompts_list_changed();
2071                }
2072            }
2073            _ => {
2074                return Err(fastmcp_core::McpError::invalid_request(
2075                    "proxy typed catalog mixes legacy and final component vectors",
2076                ));
2077            }
2078        }
2079        Ok(self)
2080    }
2081
2082    // ─────────────────────────────────────────────────
2083    // Server Composition (Mount)
2084    // ─────────────────────────────────────────────────
2085
2086    /// Mounts another server's components into this server with an optional prefix.
2087    ///
2088    /// This consumes the source server and moves all its tools, resources, and prompts
2089    /// into this server. Names/URIs are prefixed with `prefix/` if a prefix is provided.
2090    ///
2091    /// # Example
2092    ///
2093    /// ```ignore
2094    /// let db_server = Server::new("db", "1.0")
2095    ///     .tool(query_tool)
2096    ///     .tool(insert_tool)
2097    ///     .build();
2098    ///
2099    /// let api_server = Server::new("api", "1.0")
2100    ///     .tool(endpoint_tool)
2101    ///     .build();
2102    ///
2103    /// let main = Server::new("main", "1.0")
2104    ///     .mount(db_server, Some("db"))      // db/query, db/insert
2105    ///     .mount(api_server, Some("api"))    // api/endpoint
2106    ///     .build();
2107    /// ```
2108    ///
2109    /// # Prefix Rules
2110    ///
2111    /// - Prefixes must be alphanumeric plus underscores and hyphens
2112    /// - Prefixes cannot contain slashes
2113    /// - With prefix `"db"`, tool `"query"` becomes `"db/query"`
2114    /// - Without prefix, names are preserved (may cause conflicts)
2115    ///
2116    /// Duplicate handling follows [`on_duplicate`](Self::on_duplicate). With
2117    /// [`DuplicateBehavior::Error`], any conflict rejects the complete mount;
2118    /// the failure is logged and fluent builder construction continues.
2119    #[must_use]
2120    pub fn mount(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2121        #[cfg(feature = "apps")]
2122        if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2123            log::error!(
2124                target: "fastmcp_rust::mount",
2125                "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2126            );
2127            return self;
2128        }
2129
2130        let has_tools = server.has_tools();
2131        let has_resources = server.has_resources();
2132        let has_prompts = server.has_prompts();
2133
2134        let source_router = server.into_router();
2135        let result = self
2136            .router
2137            .mount_with_behavior(source_router, prefix, self.on_duplicate);
2138
2139        // Log warnings if any
2140        for warning in &result.warnings {
2141            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2142        }
2143        for error in &result.errors {
2144            log::error!(target: "fastmcp_rust::mount", "{}", error);
2145        }
2146
2147        // Update capabilities based on what was mounted
2148        if has_tools && result.tools > 0 {
2149            self.advertise_legacy_tools_list_changed();
2150        }
2151        if has_resources && (result.resources > 0 || result.resource_templates > 0) {
2152            self.advertise_legacy_resource_subscriptions();
2153        }
2154        if has_prompts && result.prompts > 0 {
2155            self.advertise_legacy_prompts_list_changed();
2156        }
2157        if self.router.has_completion_handler() {
2158            self.advertise_completions();
2159        }
2160
2161        self
2162    }
2163
2164    /// Mounts tools and prompts with an optional name prefix, and keeps
2165    /// resource and template URIs exact.
2166    ///
2167    /// Use this when the destination must remain a modern catalog: a nonempty
2168    /// `{prefix}/{uri}` key is not an absolute final URI.
2169    #[must_use]
2170    pub fn mount_preserving_resource_uris(
2171        mut self,
2172        server: crate::Server,
2173        prefix: Option<&str>,
2174    ) -> Self {
2175        #[cfg(feature = "apps")]
2176        if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2177            log::error!(
2178                target: "fastmcp_rust::mount",
2179                "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2180            );
2181            return self;
2182        }
2183
2184        let has_tools = server.has_tools();
2185        let has_resources = server.has_resources();
2186        let has_prompts = server.has_prompts();
2187
2188        let source_router = server.into_router();
2189        let result =
2190            self.router
2191                .mount_namespaced_with_behavior(source_router, prefix, self.on_duplicate);
2192
2193        for warning in &result.warnings {
2194            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2195        }
2196        for error in &result.errors {
2197            log::error!(target: "fastmcp_rust::mount", "{}", error);
2198        }
2199
2200        if has_tools && result.tools > 0 {
2201            self.advertise_legacy_tools_list_changed();
2202        }
2203        if has_resources && (result.resources > 0 || result.resource_templates > 0) {
2204            self.advertise_legacy_resource_subscriptions();
2205        }
2206        if has_prompts && result.prompts > 0 {
2207            self.advertise_legacy_prompts_list_changed();
2208        }
2209        if self.router.has_completion_handler() {
2210            self.advertise_completions();
2211        }
2212
2213        self
2214    }
2215
2216    /// Mounts only tools from another server with an optional prefix.
2217    ///
2218    /// Similar to [`mount`](Self::mount), but only transfers tools, ignoring
2219    /// resources and prompts.
2220    ///
2221    /// # Example
2222    ///
2223    /// ```ignore
2224    /// let utils_server = Server::new("utils", "1.0")
2225    ///     .tool(format_tool)
2226    ///     .tool(parse_tool)
2227    ///     .resource(config_resource)  // Will NOT be mounted
2228    ///     .build();
2229    ///
2230    /// let main = Server::new("main", "1.0")
2231    ///     .mount_tools(utils_server, Some("utils"))  // Only tools
2232    ///     .build();
2233    /// ```
2234    ///
2235    /// Duplicate handling follows [`on_duplicate`](Self::on_duplicate).
2236    #[must_use]
2237    pub fn mount_tools(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2238        #[cfg(feature = "apps")]
2239        if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2240            log::error!(
2241                target: "fastmcp_rust::mount",
2242                "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2243            );
2244            return self;
2245        }
2246
2247        let source_router = server.into_router();
2248        let result =
2249            self.router
2250                .mount_tools_with_behavior(source_router, prefix, self.on_duplicate);
2251
2252        // Log warnings if any
2253        for warning in &result.warnings {
2254            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2255        }
2256        for error in &result.errors {
2257            log::error!(target: "fastmcp_rust::mount", "{}", error);
2258        }
2259
2260        // Update capabilities if tools were mounted
2261        if result.tools > 0 {
2262            self.advertise_legacy_tools_list_changed();
2263        }
2264
2265        self
2266    }
2267
2268    /// Mounts only resources from another server with an optional prefix.
2269    ///
2270    /// Similar to [`mount`](Self::mount), but only transfers resources,
2271    /// ignoring tools and prompts.
2272    ///
2273    /// # Example
2274    ///
2275    /// ```ignore
2276    /// let data_server = Server::new("data", "1.0")
2277    ///     .resource(config_resource)
2278    ///     .resource(schema_resource)
2279    ///     .tool(query_tool)  // Will NOT be mounted
2280    ///     .build();
2281    ///
2282    /// let main = Server::new("main", "1.0")
2283    ///     .mount_resources(data_server, Some("data"))  // Only resources
2284    ///     .build();
2285    /// ```
2286    ///
2287    /// Duplicate handling follows [`on_duplicate`](Self::on_duplicate) for
2288    /// both static resources and resource templates.
2289    #[must_use]
2290    pub fn mount_resources(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2291        #[cfg(feature = "apps")]
2292        if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2293            log::error!(
2294                target: "fastmcp_rust::mount",
2295                "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2296            );
2297            return self;
2298        }
2299
2300        let source_router = server.into_router();
2301        let result =
2302            self.router
2303                .mount_resources_with_behavior(source_router, prefix, self.on_duplicate);
2304
2305        // Log warnings if any
2306        for warning in &result.warnings {
2307            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2308        }
2309        for error in &result.errors {
2310            log::error!(target: "fastmcp_rust::mount", "{}", error);
2311        }
2312
2313        // Update capabilities if resources were mounted
2314        if result.resources > 0 || result.resource_templates > 0 {
2315            self.advertise_legacy_resource_subscriptions();
2316        }
2317        if self.router.has_completion_handler() {
2318            self.advertise_completions();
2319        }
2320
2321        self
2322    }
2323
2324    /// Mounts only prompts from another server with an optional prefix.
2325    ///
2326    /// Similar to [`mount`](Self::mount), but only transfers prompts,
2327    /// ignoring tools and resources.
2328    ///
2329    /// # Example
2330    ///
2331    /// ```ignore
2332    /// let templates_server = Server::new("templates", "1.0")
2333    ///     .prompt(greeting_prompt)
2334    ///     .prompt(error_prompt)
2335    ///     .tool(format_tool)  // Will NOT be mounted
2336    ///     .build();
2337    ///
2338    /// let main = Server::new("main", "1.0")
2339    ///     .mount_prompts(templates_server, Some("tmpl"))  // Only prompts
2340    ///     .build();
2341    /// ```
2342    ///
2343    /// Duplicate handling follows [`on_duplicate`](Self::on_duplicate).
2344    #[must_use]
2345    pub fn mount_prompts(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2346        let source_router = server.into_router();
2347        let result =
2348            self.router
2349                .mount_prompts_with_behavior(source_router, prefix, self.on_duplicate);
2350
2351        // Log warnings if any
2352        for warning in &result.warnings {
2353            log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2354        }
2355        for error in &result.errors {
2356            log::error!(target: "fastmcp_rust::mount", "{}", error);
2357        }
2358
2359        // Update capabilities if prompts were mounted
2360        if result.prompts > 0 {
2361            self.advertise_legacy_prompts_list_changed();
2362        }
2363        if self.router.has_completion_handler() {
2364            self.advertise_completions();
2365        }
2366
2367        self
2368    }
2369
2370    /// Sets custom server instructions.
2371    #[must_use]
2372    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
2373        self.instructions = Some(instructions.into());
2374        self
2375    }
2376
2377    /// Sets the modern discovery title. Exact-2024 initialize stays name/version.
2378    #[must_use]
2379    pub fn title(mut self, title: impl Into<String>) -> Self {
2380        self.title = Some(title.into());
2381        self
2382    }
2383
2384    /// Sets the modern discovery description.
2385    #[must_use]
2386    pub fn description(mut self, description: impl Into<String>) -> Self {
2387        self.description = Some(description.into());
2388        self
2389    }
2390
2391    /// Sets the modern discovery website URL.
2392    #[must_use]
2393    pub fn website_url(mut self, website_url: impl Into<String>) -> Self {
2394        self.website_url = Some(website_url.into());
2395        self
2396    }
2397
2398    /// Sets the modern discovery icon set.
2399    #[must_use]
2400    pub fn icons(mut self, icons: Vec<fastmcp_protocol::common_types::RawIcon>) -> Self {
2401        self.icons = icons;
2402        self
2403    }
2404
2405    /// Sets the log level.
2406    ///
2407    /// Default is read from `FASTMCP_LOG` environment variable, or `INFO` if not set.
2408    #[must_use]
2409    pub fn log_level(mut self, level: Level) -> Self {
2410        let filter = level.to_level_filter();
2411        self.logging.level = filter;
2412        self.console_config.log_level = filter;
2413        self
2414    }
2415
2416    /// Sets the log level from a filter, including [`LevelFilter::Off`].
2417    #[must_use]
2418    pub fn log_level_filter(mut self, filter: LevelFilter) -> Self {
2419        self.logging.level = filter;
2420        self.console_config.log_level = filter;
2421        self
2422    }
2423
2424    /// Sets whether to show timestamps in logs.
2425    ///
2426    /// Default is `true`.
2427    #[must_use]
2428    pub fn log_timestamps(mut self, show: bool) -> Self {
2429        self.logging.timestamps = show;
2430        self.console_config.log_timestamps = show;
2431        self
2432    }
2433
2434    /// Sets whether to show target/module paths in logs.
2435    ///
2436    /// Default is `true`.
2437    #[must_use]
2438    pub fn log_targets(mut self, show: bool) -> Self {
2439        self.logging.targets = show;
2440        self.console_config.log_targets = show;
2441        self
2442    }
2443
2444    /// Sets whether to show source file and line in logs.
2445    #[must_use]
2446    pub fn log_file_line(mut self, show: bool) -> Self {
2447        self.logging.file_line = show;
2448        self.console_config.log_file_line = show;
2449        self
2450    }
2451
2452    /// Sets the full logging configuration.
2453    #[must_use]
2454    pub fn logging(mut self, config: LoggingConfig) -> Self {
2455        self.console_config.log_level = config.level;
2456        self.console_config.log_timestamps = config.timestamps;
2457        self.console_config.log_targets = config.targets;
2458        self.console_config.log_file_line = config.file_line;
2459        self.logging = config;
2460        self
2461    }
2462
2463    // ─────────────────────────────────────────────────
2464    // Console Configuration
2465    // ─────────────────────────────────────────────────
2466
2467    /// Sets the complete console configuration.
2468    ///
2469    /// This controls server-owned console output, including the banner,
2470    /// traffic logging, logger formatting, and rich/plain display mode.
2471    ///
2472    /// # Example
2473    ///
2474    /// ```ignore
2475    /// use fastmcp_console::config::{ConsoleConfig, BannerStyle};
2476    ///
2477    /// Server::new("demo", "1.0.0")
2478    ///     .with_console_config(
2479    ///         ConsoleConfig::new()
2480    ///             .with_banner(BannerStyle::Compact)
2481    ///             .plain_mode()
2482    ///     )
2483    ///     .build();
2484    /// ```
2485    #[must_use]
2486    pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
2487        self.logging = LoggingConfig::from(&config);
2488        self.console_config = config;
2489        self
2490    }
2491
2492    /// Sets the banner style.
2493    ///
2494    /// Controls how the startup banner is displayed.
2495    /// Default is `BannerStyle::Full`.
2496    #[must_use]
2497    pub fn with_banner(mut self, style: BannerStyle) -> Self {
2498        self.console_config = self.console_config.with_banner(style);
2499        self
2500    }
2501
2502    /// Disables the startup banner.
2503    #[must_use]
2504    pub fn without_banner(mut self) -> Self {
2505        self.console_config = self.console_config.without_banner();
2506        self
2507    }
2508
2509    /// Enables request/response traffic logging.
2510    ///
2511    /// Controls the verbosity of traffic logging:
2512    /// - `None`: No traffic logging (default)
2513    /// - `Summary`: Method name and timing only
2514    /// - `Full`: Full request/response bodies
2515    #[must_use]
2516    pub fn with_traffic_logging(mut self, verbosity: TrafficVerbosity) -> Self {
2517        self.console_config = self.console_config.with_traffic(verbosity);
2518        self
2519    }
2520
2521    /// Forces plain text output (no colors/styling).
2522    ///
2523    /// Useful for CI environments, logging to files, or when running
2524    /// as an MCP server where rich output might interfere with the
2525    /// JSON-RPC protocol.
2526    #[must_use]
2527    pub fn plain_mode(mut self) -> Self {
2528        self.console_config = self.console_config.plain_mode();
2529        self
2530    }
2531
2532    /// Forces color output even in non-TTY environments.
2533    #[must_use]
2534    pub fn force_color(mut self) -> Self {
2535        self.console_config = self.console_config.force_color(true);
2536        self
2537    }
2538
2539    /// Returns a reference to the current console configuration.
2540    #[must_use]
2541    pub fn console_config(&self) -> &ConsoleConfig {
2542        &self.console_config
2543    }
2544
2545    // ─────────────────────────────────────────────────
2546    // Lifecycle Hooks
2547    // ─────────────────────────────────────────────────
2548
2549    /// Registers a startup hook that runs before the server starts accepting connections.
2550    ///
2551    /// The hook can perform initialization tasks like:
2552    /// - Opening database connections
2553    /// - Loading configuration files
2554    /// - Initializing caches
2555    ///
2556    /// If the hook returns an error, the server will not start.
2557    ///
2558    /// # Example
2559    ///
2560    /// ```ignore
2561    /// Server::new("demo", "1.0.0")
2562    ///     .on_startup(|| {
2563    ///         println!("Server starting up...");
2564    ///         Ok(())
2565    ///     })
2566    ///     .run_stdio();
2567    /// ```
2568    #[must_use]
2569    pub fn on_startup<F, E>(mut self, hook: F) -> Self
2570    where
2571        F: FnOnce() -> Result<(), E> + Send + 'static,
2572        E: std::error::Error + Send + Sync + 'static,
2573    {
2574        self.lifespan.on_startup = Some(Box::new(move || {
2575            hook().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
2576        }));
2577        self
2578    }
2579
2580    /// Registers a shutdown hook that runs when the server is shutting down.
2581    ///
2582    /// The hook can perform cleanup tasks like:
2583    /// - Closing database connections
2584    /// - Flushing caches
2585    /// - Saving state
2586    ///
2587    /// Shutdown hooks are run on a best-effort basis. If the process is
2588    /// forcefully terminated, hooks may not run.
2589    ///
2590    /// # Example
2591    ///
2592    /// ```ignore
2593    /// Server::new("demo", "1.0.0")
2594    ///     .on_shutdown(|| {
2595    ///         println!("Server shutting down...");
2596    ///     })
2597    ///     .run_stdio();
2598    /// ```
2599    #[must_use]
2600    pub fn on_shutdown<F>(mut self, hook: F) -> Self
2601    where
2602        F: FnOnce() + Send + 'static,
2603    {
2604        self.lifespan.on_shutdown = Some(Box::new(hook));
2605        self
2606    }
2607
2608    /// Retains the legacy task manager for unit-test archaeology only.
2609    /// Production builds expose no task-manager builder edge.
2610    #[cfg(all(test, feature = "tasks"))]
2611    #[must_use]
2612    pub(crate) fn with_task_manager(mut self, task_manager: SharedTaskManager) -> Self {
2613        self.task_manager = Some(task_manager);
2614        // Fail closed even if future builder composition adds another path
2615        // that mutates the capability object before this call.
2616        self.capabilities.tasks = None;
2617        self
2618    }
2619
2620    /// Returns the current request timeout.
2621    #[cfg(test)]
2622    fn request_timeout_secs(&self) -> u64 {
2623        self.request_timeout_secs
2624    }
2625
2626    /// Builds the server after a valid builder-level configuration.
2627    ///
2628    /// Both [`Self::try_new`] and [`Self::protocol_policy`] reject invalid or
2629    /// unavailable policy selections before a builder exists.
2630    ///
2631    /// When the `tasks` feature is enabled and the builder has not already
2632    /// installed a local or proxy Tasks owner, this installs a process-local
2633    /// in-memory official Tasks runtime so `tasks/get`, `tasks/update`, and
2634    /// `tasks/cancel` are served. Call [`Self::final_tasks`] to replace that
2635    /// default with an application-owned store. The historical
2636    /// [`Self::with_task_manager`] path stays quarantined and does not receive
2637    /// the official methods.
2638    #[must_use]
2639    pub fn build(mut self) -> Server {
2640        // Configure router with strict input validation setting
2641        self.router
2642            .set_strict_input_validation(self.strict_input_validation);
2643        let console = fastmcp_console::console::FastMcpConsole::with_enabled(
2644            self.console_config.should_use_rich(),
2645        );
2646        let final_subscriptions = Arc::new(FinalSubscriptionRegistry::default());
2647        #[cfg(feature = "tasks")]
2648        self.install_default_in_memory_final_tasks();
2649        #[cfg(feature = "tasks")]
2650        let final_task_runtime = self.final_task_runtime.clone();
2651        #[cfg(all(feature = "proxy", feature = "tasks"))]
2652        let final_task_relay = self.final_task_relay.clone();
2653        #[cfg(feature = "tasks")]
2654        if let Some(task_runtime) = final_task_runtime.as_ref() {
2655            let subscriptions = Arc::clone(&final_subscriptions);
2656            task_runtime.add_notification_emitter(Arc::new(move |notification| {
2657                if subscriptions.publish_task(notification).is_err() {
2658                    log::error!(
2659                        target: "fastmcp_rust::server",
2660                        "Failed to publish a typed final Task notification"
2661                    );
2662                }
2663            }));
2664        }
2665        #[cfg(feature = "tasks")]
2666        self.router
2667            .set_final_task_runtime(final_task_runtime.clone());
2668        #[cfg(all(feature = "proxy", feature = "tasks"))]
2669        self.router.set_final_task_relay(final_task_relay.clone());
2670        let extension_runtime = match self.extension_runtime {
2671            Some(mut runtime) => {
2672                runtime
2673                    .freeze()
2674                    .expect("validated server extension descriptors must freeze");
2675                Some(Arc::new(runtime))
2676            }
2677            #[cfg(all(feature = "proxy", feature = "tasks"))]
2678            None => match (final_task_runtime.as_ref(), final_task_relay.as_ref()) {
2679                (Some(task_runtime), None) => {
2680                    let mut runtime = ServerExtensionRuntime::with_final_tasks(task_runtime)
2681                        .expect("final Tasks must install into an empty extension registry");
2682                    runtime
2683                        .freeze()
2684                        .expect("final Tasks extension descriptors must freeze");
2685                    Some(Arc::new(runtime))
2686                }
2687                (None, Some(task_relay)) => {
2688                    let mut runtime =
2689                        ServerExtensionRuntime::with_proxy_final_tasks(Arc::clone(task_relay))
2690                            .expect(
2691                                "proxy final Tasks must install into an empty extension registry",
2692                            );
2693                    runtime
2694                        .freeze()
2695                        .expect("proxy final Tasks extension descriptors must freeze");
2696                    Some(Arc::new(runtime))
2697                }
2698                (None, None) => None,
2699                (Some(_), Some(_)) => unreachable!("builder rejects mixed final Tasks owners"),
2700            },
2701            #[cfg(all(feature = "tasks", not(feature = "proxy")))]
2702            None => match final_task_runtime.as_ref() {
2703                Some(task_runtime) => {
2704                    let mut runtime = ServerExtensionRuntime::with_final_tasks(task_runtime)
2705                        .expect("final Tasks must install into an empty extension registry");
2706                    runtime
2707                        .freeze()
2708                        .expect("final Tasks extension descriptors must freeze");
2709                    Some(Arc::new(runtime))
2710                }
2711                None => None,
2712            },
2713            #[cfg(not(feature = "tasks"))]
2714            None => None,
2715        };
2716
2717        Server {
2718            info: self.info,
2719            title: self.title,
2720            description: self.description,
2721            website_url: self.website_url,
2722            icons: self.icons,
2723            capabilities: self.capabilities,
2724            router: Arc::new(self.router),
2725            instructions: self.instructions,
2726            request_timeout_secs: self.request_timeout_secs,
2727            stats: if self.stats_enabled {
2728                Some(ServerStats::new())
2729            } else {
2730                None
2731            },
2732            mask_error_details: self.mask_error_details,
2733            logging: self.logging,
2734            console_config: self.console_config,
2735            console,
2736            lifespan: Mutex::new(Some(self.lifespan)),
2737            auth_provider: self.auth_provider,
2738            middleware: Arc::new(self.middleware),
2739            active_requests: Arc::new(Mutex::new(HashMap::new())),
2740            #[cfg(all(test, feature = "tasks"))]
2741            task_manager: self.task_manager,
2742            max_bidirectional_requests_per_connection: self
2743                .max_bidirectional_requests_per_connection,
2744            protocol_policy: self.protocol_policy,
2745            http_config: self.http_config,
2746            oauth_http_routes: self.oauth_http_routes,
2747            extension_runtime,
2748            #[cfg(feature = "tasks")]
2749            final_task_runtime: self.final_task_runtime,
2750            #[cfg(all(feature = "proxy", feature = "tasks"))]
2751            final_task_relay,
2752            final_subscriptions,
2753        }
2754    }
2755
2756    /// Builds a server through the historical fallible spelling.
2757    ///
2758    /// Invalid launch configuration is rejected by [`Self::try_new`] before
2759    /// the builder exists, so this always returns the result of [`Self::build`].
2760    pub fn try_build(self) -> Result<Server, ServerLaunchPolicyError> {
2761        Ok(self.build())
2762    }
2763}
2764
2765#[cfg(test)]
2766mod tests {
2767    use super::*;
2768    #[cfg(all(feature = "proxy", feature = "tasks"))]
2769    use crate::proxy::{ProxyFinalTaskListener, ProxyFinalTaskListenerEvent};
2770    #[cfg(feature = "proxy")]
2771    use asupersync::Cx;
2772    #[cfg(all(feature = "proxy", feature = "tasks"))]
2773    use fastmcp_client::FinalToolCallOutcome;
2774    #[cfg(all(feature = "proxy", feature = "tasks"))]
2775    use fastmcp_core::block_on;
2776    use fastmcp_core::{McpContext, McpResult};
2777    #[cfg(feature = "apps")]
2778    use fastmcp_protocol::FinalTool;
2779    #[cfg(feature = "apps")]
2780    use fastmcp_protocol::common_types::AbsoluteUri;
2781    #[cfg(feature = "proxy")]
2782    use fastmcp_protocol::common_types::{ContentBlock, Implementation};
2783    #[cfg(feature = "apps")]
2784    use fastmcp_protocol::extensions::ExtensionNegotiationError;
2785    use fastmcp_protocol::protocol_policy::ProtocolPolicy;
2786    #[cfg(feature = "proxy")]
2787    use fastmcp_protocol::protocol_policy::{ProtocolEra, StdioOpeningFrame};
2788    #[cfg(feature = "proxy")]
2789    use fastmcp_protocol::{
2790        CallToolResult, CompleteResult, CoreResult, FinalCallToolResult, FinalCoreResult,
2791        JsonRpcRequest, LegacyContent, LegacyCoreResult, ResultMeta,
2792    };
2793    use fastmcp_protocol::{Content, Prompt, Resource, ResourceContent, Tool};
2794    #[cfg(all(feature = "proxy", feature = "tasks"))]
2795    use fastmcp_protocol::{
2796        CoreResultDiscriminatorPolicy, CreateTaskResult, DecodedResult, EmptyTaskResult,
2797        ResultPeerEra, SubscriptionFilter, decode_peer_result,
2798    };
2799
2800    // ── Stub handlers ────────────────────────────────────────────────
2801
2802    struct TestTool;
2803    impl crate::ToolHandler for TestTool {
2804        fn definition(&self) -> Tool {
2805            Tool {
2806                name: "test_tool".to_string(),
2807                description: Some("a test tool".to_string()),
2808                input_schema: serde_json::json!({"type": "object"}),
2809                output_schema: None,
2810                icon: None,
2811                version: None,
2812                tags: vec![],
2813                annotations: None,
2814            }
2815        }
2816        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2817            Ok(vec![Content::text("ok")])
2818        }
2819    }
2820
2821    struct ExactLegacyOnlyTool;
2822    impl crate::ToolHandler for ExactLegacyOnlyTool {
2823        fn definition(&self) -> Tool {
2824            Tool {
2825                name: "exact_legacy_only".to_owned(),
2826                description: Some("exact 2024-only test tool".to_owned()),
2827                input_schema: serde_json::json!({"type": "object"}),
2828                output_schema: Some(serde_json::json!(false)),
2829                icon: None,
2830                version: None,
2831                tags: Vec::new(),
2832                annotations: None,
2833            }
2834        }
2835
2836        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2837            Ok(vec![Content::text("legacy")])
2838        }
2839    }
2840
2841    #[cfg(feature = "apps")]
2842    struct MountedAppsTool;
2843
2844    #[cfg(feature = "apps")]
2845    impl crate::ToolHandler for MountedAppsTool {
2846        fn definition(&self) -> Tool {
2847            Tool {
2848                name: "mounted_apps_tool".to_owned(),
2849                description: Some("final-only Apps mount fixture".to_owned()),
2850                input_schema: serde_json::json!({"type": "object"}),
2851                output_schema: None,
2852                icon: None,
2853                version: None,
2854                tags: Vec::new(),
2855                annotations: None,
2856            }
2857        }
2858
2859        fn final_definition(&self) -> Option<FinalTool> {
2860            let metadata = fastmcp_protocol::McpAppsToolMetadata::try_new(
2861                Some(
2862                    AbsoluteUri::parse("ui://mount/dashboard")
2863                        .expect("fixed Apps mount URI is valid"),
2864                ),
2865                None,
2866            )
2867            .expect("fixed Apps mount metadata is valid")
2868            .to_open_metadata()
2869            .expect("fixed Apps mount metadata serializes");
2870            Some(FinalTool {
2871                name: "mounted_apps_tool".to_owned(),
2872                title: None,
2873                description: Some("final-only Apps mount fixture".to_owned()),
2874                input_schema: serde_json::json!({"type": "object"}),
2875                output_schema: None,
2876                annotations: None,
2877                icons: None,
2878                meta: Some(metadata),
2879            })
2880        }
2881
2882        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2883            Ok(vec![Content::text("Apps mount fixture")])
2884        }
2885    }
2886
2887    #[cfg(feature = "apps")]
2888    fn apps_mount_child() -> crate::Server {
2889        let resource = McpAppsUiResource::try_new(
2890            AbsoluteUri::parse("ui://mount/dashboard").expect("fixed Apps mount URI is valid"),
2891            "mounted-apps-dashboard",
2892            "<main>Apps mount fixture</main>",
2893        )
2894        .expect("fixed Apps mount resource is valid");
2895        ServerBuilder::new("apps-child", "1.0")
2896            .mcp_apps()
2897            .expect("child Apps extension installs")
2898            .mcp_apps_ui_resource(resource)
2899            .expect("child Apps resource registers")
2900            .mcp_apps_tool(MountedAppsTool)
2901            .expect("child Apps tool registers")
2902            .build()
2903    }
2904
2905    #[cfg(feature = "apps")]
2906    fn apps_resource_bound_tool_mount_child() -> crate::Server {
2907        let resource = McpAppsUiResource::try_new(
2908            AbsoluteUri::parse("ui://mount/dashboard").expect("fixed Apps mount URI is valid"),
2909            "mounted-apps-dashboard",
2910            "<main>Apps mount fixture</main>",
2911        )
2912        .expect("fixed Apps mount resource is valid");
2913        ServerBuilder::new("apps-resource-child", "1.0")
2914            .mcp_apps()
2915            .expect("child Apps extension installs")
2916            .mcp_apps_ui_resource(resource)
2917            .expect("child Apps resource registers")
2918            .tool(TestTool)
2919            .build()
2920    }
2921
2922    struct TestResource;
2923    impl crate::ResourceHandler for TestResource {
2924        fn definition(&self) -> Resource {
2925            Resource {
2926                uri: "file:///test".to_string(),
2927                name: "test_res".to_string(),
2928                description: None,
2929                mime_type: None,
2930                icon: None,
2931                version: None,
2932                tags: vec![],
2933            }
2934        }
2935        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
2936            Ok(vec![ResourceContent {
2937                uri: "file:///test".to_string(),
2938                mime_type: None,
2939                text: Some("content".to_string()),
2940                blob: None,
2941            }])
2942        }
2943    }
2944
2945    struct TestPrompt;
2946    impl crate::PromptHandler for TestPrompt {
2947        fn definition(&self) -> Prompt {
2948            Prompt {
2949                name: "test_prompt".to_string(),
2950                description: None,
2951                arguments: vec![],
2952                icon: None,
2953                version: None,
2954                tags: vec![],
2955            }
2956        }
2957        fn get(
2958            &self,
2959            _ctx: &McpContext,
2960            _args: std::collections::HashMap<String, String>,
2961        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
2962            Ok(vec![])
2963        }
2964    }
2965
2966    struct TestCompletion;
2967
2968    impl crate::handler::CompletionHandler for TestCompletion {
2969        fn complete_legacy(
2970            &self,
2971            _ctx: &McpContext,
2972            _params: fastmcp_protocol::LegacyCompletionParams,
2973        ) -> McpResult<fastmcp_protocol::CompletionValues> {
2974            Ok(fastmcp_protocol::CompletionValues {
2975                values: vec!["staging".to_string()],
2976                total: Some(1),
2977                has_more: Some(false),
2978            })
2979        }
2980
2981        fn complete_final(
2982            &self,
2983            _ctx: &McpContext,
2984            _params: fastmcp_protocol::FinalCompletionParams,
2985        ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
2986            Ok(fastmcp_protocol::FinalCompletionValues {
2987                values: vec!["staging".to_string()],
2988                total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
2989                has_more: Some(false),
2990            })
2991        }
2992    }
2993
2994    struct CountingCompletion(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2995
2996    impl crate::handler::CompletionHandler for CountingCompletion {
2997        fn complete_legacy(
2998            &self,
2999            _ctx: &McpContext,
3000            _params: fastmcp_protocol::LegacyCompletionParams,
3001        ) -> McpResult<fastmcp_protocol::CompletionValues> {
3002            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3003            Ok(fastmcp_protocol::CompletionValues {
3004                values: vec!["staging".to_string()],
3005                total: Some(1),
3006                has_more: Some(false),
3007            })
3008        }
3009
3010        fn complete_final(
3011            &self,
3012            _ctx: &McpContext,
3013            _params: fastmcp_protocol::FinalCompletionParams,
3014        ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
3015            self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3016            Ok(fastmcp_protocol::FinalCompletionValues {
3017                values: vec!["staging".to_string()],
3018                total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
3019                has_more: Some(false),
3020            })
3021        }
3022    }
3023
3024    struct MarkedTool(&'static str);
3025
3026    impl crate::ToolHandler for MarkedTool {
3027        fn definition(&self) -> Tool {
3028            Tool {
3029                name: "duplicate_tool".to_string(),
3030                description: Some(self.0.to_string()),
3031                input_schema: serde_json::json!({"type": "object"}),
3032                output_schema: None,
3033                icon: None,
3034                version: None,
3035                tags: vec![self.0.to_string()],
3036                annotations: None,
3037            }
3038        }
3039
3040        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
3041            Ok(vec![Content::text(self.0)])
3042        }
3043    }
3044
3045    struct MarkedResource(&'static str);
3046
3047    impl crate::ResourceHandler for MarkedResource {
3048        fn definition(&self) -> Resource {
3049            Resource {
3050                uri: "duplicate://resource".to_string(),
3051                name: self.0.to_string(),
3052                description: Some(self.0.to_string()),
3053                mime_type: None,
3054                icon: None,
3055                version: None,
3056                tags: vec![self.0.to_string()],
3057            }
3058        }
3059
3060        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
3061            Ok(vec![])
3062        }
3063    }
3064
3065    struct MarkedPrompt(&'static str);
3066
3067    impl crate::PromptHandler for MarkedPrompt {
3068        fn definition(&self) -> Prompt {
3069            Prompt {
3070                name: "duplicate_prompt".to_string(),
3071                description: Some(self.0.to_string()),
3072                arguments: vec![],
3073                icon: None,
3074                version: None,
3075                tags: vec![self.0.to_string()],
3076            }
3077        }
3078
3079        fn get(
3080            &self,
3081            _ctx: &McpContext,
3082            _args: std::collections::HashMap<String, String>,
3083        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
3084            Ok(vec![])
3085        }
3086    }
3087
3088    fn marked_resource_template(marker: &str) -> ResourceTemplate {
3089        ResourceTemplate {
3090            uri_template: "mcp://duplicate-item/{item}".to_string(),
3091            name: marker.to_string(),
3092            description: Some(marker.to_string()),
3093            mime_type: None,
3094            icon: None,
3095            version: None,
3096            tags: vec![marker.to_string()],
3097        }
3098    }
3099
3100    fn marked_builder(marker: &'static str) -> ServerBuilder {
3101        ServerBuilder::new("marked", "1.0")
3102            .tool(MarkedTool(marker))
3103            .resource(MarkedResource(marker))
3104            .resource_template(marked_resource_template(marker))
3105            .prompt(MarkedPrompt(marker))
3106    }
3107
3108    fn assert_marked_server(server: &crate::Server, marker: &str) {
3109        assert_eq!(server.tools().len(), 1);
3110        assert_eq!(server.resources().len(), 1);
3111        assert_eq!(server.resource_templates().len(), 1);
3112        assert_eq!(server.prompts().len(), 1);
3113        assert_eq!(server.tools()[0].tags, vec![marker.to_string()]);
3114        assert_eq!(server.resources()[0].tags, vec![marker.to_string()]);
3115        assert_eq!(
3116            server.resource_templates()[0].tags,
3117            vec![marker.to_string()]
3118        );
3119        assert_eq!(server.prompts()[0].tags, vec![marker.to_string()]);
3120    }
3121
3122    #[cfg(feature = "proxy")]
3123    struct DuplicatePolicyProxyBackend;
3124
3125    #[cfg(feature = "proxy")]
3126    impl crate::proxy::ProxyBackend for DuplicatePolicyProxyBackend {
3127        fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
3128            Ok(duplicate_policy_proxy_catalog().tools)
3129        }
3130
3131        fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
3132            Ok(duplicate_policy_proxy_catalog().resources)
3133        }
3134
3135        fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
3136            Ok(duplicate_policy_proxy_catalog().resource_templates)
3137        }
3138
3139        fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
3140            Ok(duplicate_policy_proxy_catalog().prompts)
3141        }
3142
3143        fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
3144            Ok(vec![])
3145        }
3146
3147        fn call_tool_with_progress(
3148            &mut self,
3149            _: &str,
3150            _: serde_json::Value,
3151            _: crate::proxy::ProgressCallback<'_>,
3152        ) -> McpResult<Vec<Content>> {
3153            Ok(vec![])
3154        }
3155
3156        fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
3157            Ok(vec![])
3158        }
3159
3160        fn get_prompt(
3161            &mut self,
3162            _: &str,
3163            _: std::collections::HashMap<String, String>,
3164        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
3165            Ok(vec![])
3166        }
3167    }
3168
3169    #[cfg(all(feature = "proxy", feature = "tasks"))]
3170    struct OrdinaryProxyTasksListener {
3171        accepted: Option<SubscriptionFilter>,
3172    }
3173
3174    #[cfg(all(feature = "proxy", feature = "tasks"))]
3175    impl ProxyFinalTaskListener for OrdinaryProxyTasksListener {
3176        fn next(
3177            &mut self,
3178            _cx: &Cx,
3179            _request_cancellation: &fastmcp_core::McpRequestCancellation,
3180        ) -> McpResult<ProxyFinalTaskListenerEvent> {
3181            match self.accepted.take() {
3182                Some(accepted) => Ok(ProxyFinalTaskListenerEvent::Acknowledged(accepted)),
3183                None => Ok(ProxyFinalTaskListenerEvent::Terminal),
3184            }
3185        }
3186    }
3187
3188    /// A modern-only upstream whose public final tools/call path can return
3189    /// either official Tasks or MRTR input_required results. The test uses it
3190    /// only through the ordinary public `ServerBuilder::proxy` entry point.
3191    #[cfg(all(feature = "proxy", feature = "tasks"))]
3192    struct OrdinaryProxyTasksBackend {
3193        calls: Arc<Mutex<Vec<String>>>,
3194        updates: Arc<Mutex<Vec<serde_json::Value>>>,
3195        task: CreateTaskResult,
3196    }
3197
3198    #[cfg(all(feature = "proxy", feature = "tasks"))]
3199    impl crate::proxy::ProxyBackend for OrdinaryProxyTasksBackend {
3200        fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
3201            Ok(Vec::new())
3202        }
3203
3204        fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
3205            Ok(Vec::new())
3206        }
3207
3208        fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
3209            Ok(Vec::new())
3210        }
3211
3212        fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
3213            Ok(Vec::new())
3214        }
3215
3216        fn call_tool(
3217            &mut self,
3218            _name: &str,
3219            _arguments: serde_json::Value,
3220        ) -> McpResult<Vec<Content>> {
3221            Err(fastmcp_core::McpError::internal_error(
3222                "ordinary Tasks proxy test must retain the final result algebra",
3223            ))
3224        }
3225
3226        fn call_tool_with_progress(
3227            &mut self,
3228            name: &str,
3229            arguments: serde_json::Value,
3230            _on_progress: crate::proxy::ProgressCallback<'_>,
3231        ) -> McpResult<Vec<Content>> {
3232            self.call_tool(name, arguments)
3233        }
3234
3235        fn read_resource(&mut self, _uri: &str) -> McpResult<Vec<ResourceContent>> {
3236            Err(fastmcp_core::McpError::internal_error("not used"))
3237        }
3238
3239        fn get_prompt(
3240            &mut self,
3241            _name: &str,
3242            _arguments: std::collections::HashMap<String, String>,
3243        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
3244            Err(fastmcp_core::McpError::internal_error("not used"))
3245        }
3246
3247        fn supports_final_tasks_relay(&mut self) -> McpResult<bool> {
3248            Ok(true)
3249        }
3250
3251        fn call_tool_final_outcome(
3252            &mut self,
3253            name: &str,
3254            arguments: serde_json::Value,
3255        ) -> McpResult<FinalToolCallOutcome> {
3256            self.calls
3257                .lock()
3258                .expect("ordinary proxy task call log is not poisoned")
3259                .push(format!("tools/call:{name}"));
3260            if arguments.get("outcome") == Some(&serde_json::json!("task")) {
3261                return Ok(FinalToolCallOutcome::Task(self.task.clone()));
3262            }
3263            let (decoded, diagnostic) = decode_peer_result(
3264                r#"{"resultType":"input_required","requestState":"upstream-forged-state"}"#,
3265                ResultPeerEra::Modern,
3266                &CoreResultDiscriminatorPolicy,
3267            )
3268            .map_err(|error| fastmcp_core::McpError::invalid_request(error.to_string()))?;
3269            assert!(diagnostic.is_none(), "the fixed final fixture is explicit");
3270            let DecodedResult::InputRequired(result) = decoded else {
3271                return Err(fastmcp_core::McpError::internal_error(
3272                    "ordinary proxy test fixture must decode as input_required",
3273                ));
3274            };
3275            Ok(FinalToolCallOutcome::InputRequired(result))
3276        }
3277
3278        fn update_final_task(
3279            &mut self,
3280            task: &fastmcp_protocol::Task,
3281            input_responses: fastmcp_protocol::TaskInputResponses,
3282        ) -> McpResult<fastmcp_protocol::UpdateTaskResult> {
3283            self.calls
3284                .lock()
3285                .expect("ordinary proxy task call log is not poisoned")
3286                .push("tasks/update".to_owned());
3287            assert_eq!(
3288                task.base().task_id.as_str(),
3289                self.task.task.base().task_id.as_str(),
3290                "the relay supplies the retained upstream task rather than admitting an arbitrary id"
3291            );
3292            self.updates
3293                .lock()
3294                .expect("ordinary proxy task update receipt is not poisoned")
3295                .push(
3296                    serde_json::to_value(&input_responses)
3297                        .expect("the exact public Tasks input response map serializes"),
3298                );
3299            Ok(EmptyTaskResult::default())
3300        }
3301
3302        fn open_final_task_listener(
3303            &mut self,
3304            notifications: SubscriptionFilter,
3305        ) -> McpResult<Box<dyn ProxyFinalTaskListener>> {
3306            self.calls
3307                .lock()
3308                .expect("ordinary proxy task call log is not poisoned")
3309                .push("subscriptions/listen".to_owned());
3310            Ok(Box::new(OrdinaryProxyTasksListener {
3311                accepted: Some(notifications),
3312            }))
3313        }
3314    }
3315
3316    #[cfg(feature = "proxy")]
3317    fn duplicate_policy_proxy_catalog() -> ProxyCatalog {
3318        ProxyCatalog {
3319            tool_catalog_era: Some(ProtocolEra::Legacy2024),
3320            tools: vec![Tool {
3321                name: "test_tool".to_string(),
3322                description: Some("proxied tool".to_string()),
3323                // Final projection of a proxied legacy tool requires an
3324                // object-typed input schema.
3325                input_schema: serde_json::json!({"type": "object"}),
3326                output_schema: None,
3327                icon: None,
3328                version: None,
3329                tags: vec![],
3330                annotations: None,
3331            }],
3332            resources: vec![Resource {
3333                uri: "file:///test".to_string(),
3334                name: "proxied resource".to_string(),
3335                description: Some("proxied resource".to_string()),
3336                mime_type: None,
3337                icon: None,
3338                version: None,
3339                tags: vec![],
3340            }],
3341            prompts: vec![Prompt {
3342                name: "test_prompt".to_string(),
3343                description: Some("proxied prompt".to_string()),
3344                arguments: vec![],
3345                icon: None,
3346                version: None,
3347                tags: vec![],
3348            }],
3349            ..ProxyCatalog::default()
3350        }
3351    }
3352
3353    // ── Builder defaults ─────────────────────────────────────────────
3354
3355    #[test]
3356    fn builder_new_sets_info() {
3357        let builder = ServerBuilder::new("my-server", "2.0.0");
3358        let server = builder.build();
3359        assert_eq!(server.info().name, "my-server");
3360        assert_eq!(server.info().version, "2.0.0");
3361    }
3362
3363    #[test]
3364    fn builder_default_has_logging_capability() {
3365        let builder = ServerBuilder::new("srv", "1.0");
3366        let server = builder.build();
3367        assert!(server.capabilities().logging.is_some());
3368    }
3369
3370    #[test]
3371    fn builder_default_has_no_tool_resource_prompt_capabilities() {
3372        let builder = ServerBuilder::new("srv", "1.0");
3373        let server = builder.build();
3374        assert!(server.capabilities().tools.is_none());
3375        assert!(server.capabilities().resources.is_none());
3376        assert!(server.capabilities().prompts.is_none());
3377    }
3378
3379    #[cfg(feature = "apps")]
3380    #[test]
3381    fn builder_manual_apps_discovery_requires_the_exact_empty_marker_before_build() {
3382        let mut accepted_descriptors = fastmcp_protocol::ExtensionDescriptorRegistry::new();
3383        let accepted_id =
3384            fastmcp_protocol::register_official_mcp_apps_extension(&mut accepted_descriptors)
3385                .expect("the official Apps descriptor registers");
3386        let accepted = ServerBuilder::new("apps-marker", "1.0").extension_registry(
3387            crate::ExtensionHandlerRegistry::new(accepted_descriptors),
3388            ServerExtensionDiscovery {
3389                extensions: std::collections::BTreeMap::from([(
3390                    accepted_id,
3391                    fastmcp_protocol::official_mcp_apps_empty_server_settings(),
3392                )]),
3393            },
3394            |_descriptor: &fastmcp_protocol::ExtensionDescriptor,
3395             _client: &fastmcp_protocol::ExtensionSettings,
3396             _server: &fastmcp_protocol::ExtensionSettings|
3397             -> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
3398                Ok(fastmcp_protocol::official_mcp_apps_empty_server_settings())
3399            },
3400        );
3401        assert!(
3402            accepted.is_ok(),
3403            "the exact empty official Apps marker is accepted during builder configuration"
3404        );
3405
3406        let mut rejected_descriptors = fastmcp_protocol::ExtensionDescriptorRegistry::new();
3407        let rejected_id =
3408            fastmcp_protocol::register_official_mcp_apps_extension(&mut rejected_descriptors)
3409                .expect("the official Apps descriptor registers");
3410        let rejected = ServerBuilder::new("apps-marker", "1.0").extension_registry(
3411            crate::ExtensionHandlerRegistry::new(rejected_descriptors),
3412            ServerExtensionDiscovery {
3413                extensions: std::collections::BTreeMap::from([(
3414                    rejected_id,
3415                    fastmcp_protocol::ExtensionSettings::new(serde_json::json!({
3416                        "unexpected": true,
3417                    }))
3418                    .expect("the one-field alternate is generic extension metadata"),
3419                )]),
3420            },
3421            |_descriptor: &fastmcp_protocol::ExtensionDescriptor,
3422             _client: &fastmcp_protocol::ExtensionSettings,
3423             _server: &fastmcp_protocol::ExtensionSettings|
3424             -> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
3425                Ok(fastmcp_protocol::official_mcp_apps_empty_server_settings())
3426            },
3427        );
3428        assert!(
3429            matches!(
3430                rejected,
3431                Err(crate::ServerExtensionConfigurationError::Registry(
3432                    fastmcp_protocol::ExtensionRegistryError::OfficialMcpAppsServerSettingsNotEmpty
3433                ))
3434            ),
3435            "adding one discovery setting is rejected by extension_registry before build"
3436        );
3437    }
3438
3439    #[test]
3440    fn builder_completion_handler_activates_exact_discovery_capability() {
3441        let server = ServerBuilder::new("srv", "1.0")
3442            .completion_handler(TestCompletion)
3443            .build();
3444        let discovery = server
3445            .server_discovery()
3446            .expect("installed completion handler produces discovery");
3447        let wire = serde_json::to_value(discovery).expect("discovery serializes");
3448
3449        assert_eq!(wire["capabilities"]["completions"], serde_json::json!({}));
3450    }
3451
3452    #[test]
3453    fn builder_provider_specific_completion_requires_an_admitted_final_target() {
3454        let unmatched = ServerBuilder::new("srv", "1.0")
3455            .prompt_completion_handler("duplicate_prompt", TestCompletion)
3456            .build();
3457        let unmatched_discovery = unmatched
3458            .server_discovery()
3459            .expect("the unmatched provider-only server still discovers");
3460        let unmatched_wire =
3461            serde_json::to_value(unmatched_discovery).expect("discovery serializes");
3462        assert!(
3463            unmatched_wire["capabilities"].get("completions").is_none(),
3464            "an unbound provider-only registration must not advertise completion"
3465        );
3466
3467        let matched = ServerBuilder::new("srv", "1.0")
3468            .prompt(MarkedPrompt("provider-target"))
3469            .prompt_completion_handler("duplicate_prompt", TestCompletion)
3470            .build();
3471        let matched_discovery = matched
3472            .server_discovery()
3473            .expect("an admitted final prompt activates its provider route");
3474        let matched_wire = serde_json::to_value(matched_discovery).expect("discovery serializes");
3475
3476        assert_eq!(
3477            matched_wire["capabilities"]["completions"],
3478            serde_json::json!({}),
3479            "adding only the final prompt target makes the provider discoverable"
3480        );
3481    }
3482
3483    #[test]
3484    fn builder_without_completion_handler_omits_discovery_capability() {
3485        let server = ServerBuilder::new("srv", "1.0").build();
3486        let discovery = server
3487            .server_discovery()
3488            .expect("server without completion handler still discovers");
3489        let wire = serde_json::to_value(discovery).expect("discovery serializes");
3490
3491        assert!(
3492            wire["capabilities"].get("completions").is_none(),
3493            "absence of the handler must not advertise completion"
3494        );
3495    }
3496
3497    #[test]
3498    fn builder_completion_handler_rejects_final_metadata_before_handler_state_changes() {
3499        let invocations = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3500        let server = ServerBuilder::new("srv", "1.0")
3501            .completion_handler(CountingCompletion(std::sync::Arc::clone(&invocations)))
3502            .build();
3503        let request_ctx = McpContext::new(asupersync::Cx::for_testing(), 93);
3504        let baseline = fastmcp_protocol::JsonRpcRequest::new(
3505            "completion/complete",
3506            Some(serde_json::json!({
3507                "ref": {"type": "ref/prompt", "name": "deploy"},
3508                "argument": {"name": "environment", "value": "sta"},
3509            })),
3510            93_i64,
3511        );
3512        let mut planted = baseline.clone();
3513        planted
3514            .params
3515            .as_mut()
3516            .and_then(serde_json::Value::as_object_mut)
3517            .expect("completion parameters are an object")
3518            .insert(
3519                "_meta".to_string(),
3520                serde_json::json!({
3521                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
3522                }),
3523            );
3524
3525        let baseline_result = server
3526            .router
3527            .dispatch_legacy_completion(&request_ctx, &baseline)
3528            .expect("baseline legacy completion reaches the builder-installed handler");
3529        assert_eq!(
3530            invocations.load(std::sync::atomic::Ordering::SeqCst),
3531            1,
3532            "the accepted request invokes the installed handler once"
3533        );
3534        let planted_before = serde_json::to_vec(&planted).expect("planted request serializes");
3535
3536        let error = server
3537            .router
3538            .dispatch_legacy_completion(&request_ctx, &planted)
3539            .expect_err("the sole final metadata field is refused in the exact legacy route");
3540        assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
3541        assert_eq!(
3542            invocations.load(std::sync::atomic::Ordering::SeqCst),
3543            1,
3544            "cross-era rejection occurs before handler-owned state can change"
3545        );
3546        assert_eq!(
3547            serde_json::to_vec(&planted).expect("rejected request serializes"),
3548            planted_before,
3549            "the rejected request remains caller-owned and unchanged"
3550        );
3551        assert_eq!(
3552            server
3553                .router
3554                .dispatch_legacy_completion(&request_ctx, &baseline)
3555                .expect("baseline remains dispatchable after the rejection"),
3556            baseline_result,
3557            "the rejected one-field variant cannot alter the accepted completion result"
3558        );
3559        assert_eq!(
3560            invocations.load(std::sync::atomic::Ordering::SeqCst),
3561            2,
3562            "only accepted requests mutate handler-owned state"
3563        );
3564    }
3565
3566    #[test]
3567    fn builder_default_stats_enabled() {
3568        let server = ServerBuilder::new("srv", "1.0").build();
3569        assert!(server.stats().is_some());
3570    }
3571
3572    #[test]
3573    fn builder_default_request_timeout() {
3574        let builder = ServerBuilder::new("srv", "1.0");
3575        assert_eq!(builder.request_timeout_secs(), DEFAULT_REQUEST_TIMEOUT_SECS);
3576    }
3577
3578    #[test]
3579    fn builder_bidirectional_limit_has_exact_validated_boundaries() {
3580        let default = ServerBuilder::new("srv", "1.0");
3581        assert_eq!(
3582            default.max_bidirectional_requests_per_connection,
3583            crate::bidirectional::DEFAULT_MAX_IN_FLIGHT_REQUESTS
3584        );
3585
3586        for valid in [1, crate::bidirectional::HARD_MAX_IN_FLIGHT_REQUESTS] {
3587            let server = ServerBuilder::new("srv", "1.0")
3588                .max_bidirectional_requests_per_connection(valid)
3589                .expect("boundary must be valid")
3590                .build();
3591            assert_eq!(server.max_bidirectional_requests_per_connection, valid);
3592            assert_eq!(
3593                server.new_pending_requests_for_connection().max_in_flight(),
3594                valid
3595            );
3596        }
3597
3598        for invalid in [0, crate::bidirectional::HARD_MAX_IN_FLIGHT_REQUESTS + 1] {
3599            let Err(error) =
3600                ServerBuilder::new("srv", "1.0").max_bidirectional_requests_per_connection(invalid)
3601            else {
3602                panic!("out-of-range limit must fail closed");
3603            };
3604            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
3605        }
3606    }
3607
3608    #[test]
3609    fn builder_default_error_masking_disabled() {
3610        let builder = ServerBuilder::new("srv", "1.0");
3611        assert!(!builder.is_error_masking_enabled());
3612    }
3613
3614    #[test]
3615    fn builder_default_strict_validation_disabled() {
3616        let builder = ServerBuilder::new("srv", "1.0");
3617        assert!(!builder.is_strict_input_validation_enabled());
3618    }
3619
3620    #[test]
3621    fn explicit_protocol_policy_applies_without_reserved_launch_setting() {
3622        let server = ServerBuilder::from_launch_protocol_policy("srv", "1.0", Ok(None))
3623            .expect("unset launch policy must construct a builder")
3624            .protocol_policy(ProtocolPolicy::ModernOnly)
3625            .expect("ModernOnly must be available to this test build")
3626            .try_build()
3627            .expect("unset launch policy must permit a server");
3628
3629        assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
3630    }
3631
3632    #[test]
3633    fn launch_policy_unset_defaults_to_auto() {
3634        assert_eq!(protocol_policy_from_server_launch_value(None), Ok(None));
3635    }
3636
3637    #[cfg(not(feature = "legacy-2024-11-05"))]
3638    #[test]
3639    fn no_legacy_public_builder_defaults_to_modern_only() {
3640        let server = ServerBuilder::try_new("srv", "1.0")
3641            .expect("no-legacy construction must succeed")
3642            .try_build()
3643            .expect("the default no-legacy builder must build");
3644
3645        assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
3646    }
3647
3648    #[cfg(not(feature = "legacy-2024-11-05"))]
3649    #[test]
3650    fn no_legacy_source_exposes_a_modern_endpoint_builder_and_gates_the_dual_era_one() {
3651        let source = include_str!("builder.rs");
3652        assert!(source.contains(
3653            "/// Builds a live modern Streamable HTTP endpoint.\n    #[cfg(not(any(feature = \"legacy-2024-11-05\", test)))]\n    pub fn build_http_endpoint"
3654        ));
3655        assert!(
3656            source.contains(
3657                "/// Builds a live dual-era HTTP endpoint with an exact legacy SSE origin."
3658            )
3659        );
3660        assert!(source.contains("crate::ServerHttpEndpointError"));
3661        assert!(!source.contains("fastmcp_transport::http::DualEraHttpEndpointError"));
3662    }
3663
3664    #[cfg(not(feature = "legacy-2024-11-05"))]
3665    #[test]
3666    fn no_legacy_public_builder_rejects_legacy_policies_without_mutation() {
3667        for policy in [ProtocolPolicy::Auto, ProtocolPolicy::LegacyOnly] {
3668            let mut builder =
3669                ServerBuilder::try_new("srv", "1.0").expect("no-legacy construction must succeed");
3670
3671            assert_eq!(
3672                builder.try_set_protocol_policy(policy),
3673                Err(ServerLaunchPolicyError::FeatureUnavailable),
3674                "{policy:?} must reject before changing a no-legacy builder"
3675            );
3676
3677            let server = builder
3678                .try_build()
3679                .expect("a rejected policy must leave the builder buildable");
3680            assert_eq!(
3681                server.protocol_policy(),
3682                ProtocolPolicy::ModernOnly,
3683                "{policy:?} differs from the default only by requiring unavailable legacy behavior"
3684            );
3685        }
3686    }
3687
3688    #[cfg(feature = "legacy-2024-11-05")]
3689    #[test]
3690    fn legacy_enabled_public_builder_preserves_auto() {
3691        let mut builder =
3692            ServerBuilder::try_new("srv", "1.0").expect("legacy-enabled construction must succeed");
3693        builder
3694            .try_set_protocol_policy(ProtocolPolicy::Auto)
3695            .expect("Auto must remain available with the legacy adapter enabled");
3696
3697        let server = builder
3698            .try_build()
3699            .expect("legacy-enabled Auto builder must build");
3700        assert_eq!(server.protocol_policy(), ProtocolPolicy::Auto);
3701    }
3702
3703    #[test]
3704    fn launch_policy_accepts_exact_public_values() {
3705        assert_eq!(
3706            protocol_policy_from_server_launch_value(Some(OsStr::new("auto"))),
3707            Ok(Some(ProtocolPolicy::Auto))
3708        );
3709        assert_eq!(
3710            protocol_policy_from_server_launch_value(Some(OsStr::new("modern-only"))),
3711            Ok(Some(ProtocolPolicy::ModernOnly))
3712        );
3713        assert_eq!(
3714            protocol_policy_from_server_launch_value(Some(OsStr::new("legacy-only"))),
3715            Ok(Some(ProtocolPolicy::LegacyOnly))
3716        );
3717    }
3718
3719    #[cfg(feature = "legacy-2024-11-05")]
3720    #[test]
3721    fn valid_launch_policy_wins_over_explicit_builder_policy() {
3722        let server = ServerBuilder::from_launch_protocol_policy(
3723            "srv",
3724            "1.0",
3725            Ok(Some(ProtocolPolicy::ModernOnly)),
3726        )
3727        .expect("valid launch policy must construct a builder")
3728        .protocol_policy(ProtocolPolicy::LegacyOnly)
3729        .expect("the unit-test dual-era build supports LegacyOnly")
3730        .try_build()
3731        .expect("valid launch policy must build");
3732
3733        assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
3734    }
3735
3736    #[cfg(feature = "legacy-2024-11-05")]
3737    #[test]
3738    fn fixed_policy_constructor_reserves_policy_against_later_setter() {
3739        let server = ServerBuilder::try_new_with_fixed_protocol_policy(
3740            "srv",
3741            "1.0",
3742            ProtocolPolicy::ModernOnly,
3743        )
3744        .expect("ModernOnly is available in every feature profile")
3745        .protocol_policy(ProtocolPolicy::LegacyOnly)
3746        .expect("the later policy is valid in this dual-era test build")
3747        .try_build()
3748        .expect("the fixed-policy builder remains buildable");
3749
3750        assert_eq!(
3751            server.protocol_policy(),
3752            ProtocolPolicy::ModernOnly,
3753            "the fixed component policy must not be replaced by a later builder setter"
3754        );
3755    }
3756
3757    #[cfg(not(feature = "legacy-2024-11-05"))]
3758    #[test]
3759    fn fixed_policy_constructor_rejects_unavailable_policy_before_construction() {
3760        assert!(matches!(
3761            ServerBuilder::try_new_with_fixed_protocol_policy("srv", "1.0", ProtocolPolicy::Auto,),
3762            Err(ServerLaunchPolicyError::FeatureUnavailable)
3763        ));
3764    }
3765
3766    #[test]
3767    fn invalid_launch_policy_is_rejected_before_builder_construction() {
3768        let result = ServerBuilder::from_launch_protocol_policy(
3769            "srv",
3770            "1.0",
3771            Err(ServerLaunchPolicyError::InvalidValue),
3772        );
3773
3774        assert!(matches!(result, Err(ServerLaunchPolicyError::InvalidValue)));
3775    }
3776
3777    #[test]
3778    fn launch_policy_parser_rejects_unknown_value_without_auto_fallback() {
3779        assert_eq!(
3780            protocol_policy_from_server_launch_value(Some(OsStr::new("mcp-2025-11-25"))),
3781            Err(ServerLaunchPolicyError::InvalidValue)
3782        );
3783    }
3784
3785    #[cfg(unix)]
3786    #[test]
3787    fn launch_policy_parser_rejects_non_unicode_value_without_panic() {
3788        use std::os::unix::ffi::OsStrExt;
3789
3790        assert_eq!(
3791            protocol_policy_from_server_launch_value(Some(OsStr::from_bytes(b"modern-only\xff"))),
3792            Err(ServerLaunchPolicyError::NonUnicode)
3793        );
3794    }
3795
3796    // ── Fluent API setters ───────────────────────────────────────────
3797
3798    #[test]
3799    fn builder_request_timeout() {
3800        let builder = ServerBuilder::new("srv", "1.0").request_timeout(60);
3801        assert_eq!(builder.request_timeout_secs(), 60);
3802    }
3803
3804    #[test]
3805    fn builder_request_timeout_zero_omits_server_ceiling() {
3806        let builder = ServerBuilder::new("srv", "1.0").request_timeout(0);
3807        assert_eq!(builder.request_timeout_secs(), 0);
3808    }
3809
3810    #[test]
3811    fn builder_without_stats() {
3812        let server = ServerBuilder::new("srv", "1.0").without_stats().build();
3813        assert!(server.stats().is_none());
3814    }
3815
3816    #[test]
3817    fn builder_mask_error_details() {
3818        let builder = ServerBuilder::new("srv", "1.0").mask_error_details(true);
3819        assert!(builder.is_error_masking_enabled());
3820    }
3821
3822    #[test]
3823    fn builder_strict_input_validation() {
3824        let builder = ServerBuilder::new("srv", "1.0").strict_input_validation(true);
3825        assert!(builder.is_strict_input_validation_enabled());
3826    }
3827
3828    #[test]
3829    fn builder_instructions() {
3830        let server = ServerBuilder::new("srv", "1.0")
3831            .instructions("Use this server wisely")
3832            .build();
3833        // instructions stored internally - verify build succeeds
3834        let _ = server;
3835    }
3836
3837    #[test]
3838    fn builder_log_level() {
3839        let builder = ServerBuilder::new("srv", "1.0").log_level(Level::Debug);
3840        assert_eq!(builder.logging.level, LevelFilter::Debug);
3841        assert_eq!(builder.console_config.log_level, LevelFilter::Debug);
3842    }
3843
3844    #[test]
3845    fn builder_log_level_filter() {
3846        let builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Warn);
3847        assert_eq!(builder.logging.level, LevelFilter::Warn);
3848        assert_eq!(builder.console_config.log_level, LevelFilter::Warn);
3849    }
3850
3851    #[test]
3852    fn builder_log_timestamps_and_targets() {
3853        let builder = ServerBuilder::new("srv", "1.0")
3854            .log_timestamps(false)
3855            .log_targets(false)
3856            .log_file_line(true);
3857        assert!(!builder.logging.timestamps);
3858        assert!(!builder.console_config.log_timestamps);
3859        assert!(!builder.logging.targets);
3860        assert!(!builder.console_config.log_targets);
3861        assert!(builder.logging.file_line);
3862        assert!(builder.console_config.log_file_line);
3863    }
3864
3865    // ── Console configuration ────────────────────────────────────────
3866
3867    #[test]
3868    fn builder_without_banner() {
3869        let builder = ServerBuilder::new("srv", "1.0").without_banner();
3870        let config = builder.console_config();
3871        assert_eq!(config.banner_style, BannerStyle::None);
3872    }
3873
3874    #[test]
3875    fn builder_with_banner_compact() {
3876        let builder = ServerBuilder::new("srv", "1.0").with_banner(BannerStyle::Compact);
3877        let config = builder.console_config();
3878        assert_eq!(config.banner_style, BannerStyle::Compact);
3879    }
3880
3881    #[test]
3882    fn builder_plain_mode() {
3883        let builder = ServerBuilder::new("srv", "1.0").plain_mode();
3884        let _config = builder.console_config();
3885    }
3886
3887    // ── Handler registration ─────────────────────────────────────────
3888
3889    #[test]
3890    fn builder_tool_enables_capability() {
3891        let server = ServerBuilder::new("srv", "1.0").tool(TestTool).build();
3892        assert!(server.capabilities().tools.is_some());
3893        assert!(
3894            server
3895                .capabilities()
3896                .tools
3897                .as_ref()
3898                .is_some_and(|tools| tools.list_changed),
3899            "registering a tool must advertise tools.listChanged so clients can watch catalog mutations"
3900        );
3901        assert!(server.has_tools());
3902    }
3903
3904    #[cfg(feature = "apps")]
3905    #[test]
3906    fn builder_mcp_apps_tool_requires_apps_opt_in() {
3907        let Err(error) = ServerBuilder::new("srv", "1.0").mcp_apps_tool(TestTool) else {
3908            panic!("Apps tools must not register before Apps negotiation is configured");
3909        };
3910        assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
3911    }
3912
3913    #[test]
3914    fn builder_legacy_tool_is_explicit_and_does_not_claim_modern_tools() {
3915        let server = ServerBuilder::new("srv", "1.0")
3916            .legacy_tool(ExactLegacyOnlyTool)
3917            .build();
3918        assert!(server.capabilities().tools.is_some());
3919        assert!(server.has_tools());
3920        let router = server.into_router();
3921        assert_eq!(router.tools_count(), 1);
3922        assert!(
3923            !router
3924                .server_discovery_behavior_registry()
3925                .contains(fastmcp_protocol::ServerBehavior::ToolsList)
3926        );
3927    }
3928
3929    #[test]
3930    fn builder_resource_enables_capability() {
3931        let server = ServerBuilder::new("srv", "1.0")
3932            .resource(TestResource)
3933            .build();
3934        assert!(server.capabilities().resources.is_some());
3935        assert!(
3936            server
3937                .capabilities()
3938                .resources
3939                .as_ref()
3940                .is_some_and(|resources| resources.subscribe && resources.list_changed),
3941            "registering a resource must advertise subscribe and listChanged"
3942        );
3943        assert!(server.has_resources());
3944    }
3945
3946    #[test]
3947    fn builder_prompt_enables_capability() {
3948        let server = ServerBuilder::new("srv", "1.0").prompt(TestPrompt).build();
3949        assert!(server.capabilities().prompts.is_some());
3950        assert!(
3951            server
3952                .capabilities()
3953                .prompts
3954                .as_ref()
3955                .is_some_and(|prompts| prompts.list_changed),
3956            "registering a prompt must advertise prompts.listChanged"
3957        );
3958        assert!(server.has_prompts());
3959    }
3960
3961    #[test]
3962    fn builder_all_handlers() {
3963        let server = ServerBuilder::new("srv", "1.0")
3964            .tool(TestTool)
3965            .resource(TestResource)
3966            .prompt(TestPrompt)
3967            .build();
3968        assert!(server.has_tools());
3969        assert!(server.has_resources());
3970        assert!(server.has_prompts());
3971    }
3972
3973    #[test]
3974    fn builder_no_handlers_means_no_capabilities() {
3975        let server = ServerBuilder::new("srv", "1.0").build();
3976        assert!(!server.has_tools());
3977        assert!(!server.has_resources());
3978        assert!(!server.has_prompts());
3979    }
3980
3981    // ── Duplicate behavior ───────────────────────────────────────────
3982
3983    #[test]
3984    fn builder_on_duplicate_default_is_warn() {
3985        let _builder = ServerBuilder::new("srv", "1.0");
3986        // DuplicateBehavior::default() is Warn - builder should use that
3987    }
3988
3989    #[test]
3990    fn builder_on_duplicate_ignore() {
3991        let server = ServerBuilder::new("srv", "1.0")
3992            .on_duplicate(DuplicateBehavior::Ignore)
3993            .tool(TestTool)
3994            .build();
3995        assert!(server.has_tools());
3996    }
3997
3998    #[test]
3999    fn builder_on_duplicate_replace() {
4000        let server = ServerBuilder::new("srv", "1.0")
4001            .on_duplicate(DuplicateBehavior::Replace)
4002            .tool(TestTool)
4003            .build();
4004        assert!(server.has_tools());
4005    }
4006
4007    #[test]
4008    fn builder_resource_template_honors_duplicate_policy() {
4009        for behavior in [
4010            DuplicateBehavior::Warn,
4011            DuplicateBehavior::Ignore,
4012            DuplicateBehavior::Error,
4013        ] {
4014            let server = ServerBuilder::new("srv", "1.0")
4015                .on_duplicate(behavior)
4016                .resource_template(marked_resource_template("original"))
4017                .resource_template(marked_resource_template("incoming"))
4018                .build();
4019            assert_eq!(server.resource_templates().len(), 1);
4020            assert_eq!(server.resource_templates()[0].name, "original");
4021        }
4022
4023        let server = ServerBuilder::new("srv", "1.0")
4024            .on_duplicate(DuplicateBehavior::Replace)
4025            .resource_template(marked_resource_template("original"))
4026            .resource_template(marked_resource_template("incoming"))
4027            .build();
4028        assert_eq!(server.resource_templates().len(), 1);
4029        assert_eq!(server.resource_templates()[0].name, "incoming");
4030    }
4031
4032    // ── Lifecycle hooks ──────────────────────────────────────────────
4033
4034    #[test]
4035    fn builder_on_startup_builds() {
4036        let server = ServerBuilder::new("srv", "1.0")
4037            .on_startup(|| -> Result<(), std::io::Error> { Ok(()) })
4038            .build();
4039        let _ = server;
4040    }
4041
4042    #[test]
4043    fn builder_on_shutdown_builds() {
4044        let server = ServerBuilder::new("srv", "1.0").on_shutdown(|| {}).build();
4045        let _ = server;
4046    }
4047
4048    // ── Console config on built server ───────────────────────────────
4049
4050    #[test]
4051    fn built_server_console_config_matches_builder() {
4052        let server = ServerBuilder::new("srv", "1.0").without_banner().build();
4053        assert_eq!(server.console_config().banner_style, BannerStyle::None);
4054    }
4055
4056    // ── Chaining ─────────────────────────────────────────────────────
4057
4058    #[test]
4059    fn builder_chaining_fluent_api() {
4060        let server = ServerBuilder::new("chain", "3.0")
4061            .request_timeout(120)
4062            .mask_error_details(true)
4063            .strict_input_validation(true)
4064            .without_banner()
4065            .plain_mode()
4066            .tool(TestTool)
4067            .resource(TestResource)
4068            .prompt(TestPrompt)
4069            .on_shutdown(|| {})
4070            .build();
4071
4072        assert_eq!(server.info().name, "chain");
4073        assert_eq!(server.info().version, "3.0");
4074        assert!(server.has_tools());
4075        assert!(server.has_resources());
4076        assert!(server.has_prompts());
4077    }
4078
4079    // ── Console configuration extended ─────────────────────────────
4080
4081    #[test]
4082    fn builder_with_console_config() {
4083        let mut config = ConsoleConfig::new().with_banner(BannerStyle::None);
4084        config.log_level = LevelFilter::Trace;
4085        config.log_timestamps = false;
4086        config.log_targets = false;
4087        config.log_file_line = true;
4088        let builder = ServerBuilder::new("srv", "1.0").with_console_config(config);
4089        assert_eq!(builder.console_config().banner_style, BannerStyle::None);
4090        assert_eq!(builder.logging.level, LevelFilter::Trace);
4091        assert!(!builder.logging.timestamps);
4092        assert!(!builder.logging.targets);
4093        assert!(builder.logging.file_line);
4094    }
4095
4096    #[test]
4097    fn logging_and_console_setters_keep_one_effective_configuration() {
4098        let console = ConsoleConfig::new().with_log_level_filter(LevelFilter::Off);
4099        let builder = ServerBuilder::new("srv", "1.0")
4100            .log_level(Level::Debug)
4101            .with_console_config(console);
4102        assert_eq!(builder.logging.level, LevelFilter::Off);
4103        assert_eq!(builder.console_config.log_level, LevelFilter::Off);
4104
4105        let builder = builder.log_level_filter(LevelFilter::Warn);
4106        assert_eq!(builder.logging.level, LevelFilter::Warn);
4107        assert_eq!(builder.console_config.log_level, LevelFilter::Warn);
4108    }
4109
4110    #[test]
4111    fn builder_with_traffic_logging() {
4112        let builder = ServerBuilder::new("srv", "1.0").with_traffic_logging(TrafficVerbosity::Full);
4113        let config = builder.console_config();
4114        assert_eq!(config.traffic_verbosity, TrafficVerbosity::Full);
4115    }
4116
4117    #[test]
4118    fn builder_force_color() {
4119        let builder = ServerBuilder::new("srv", "1.0").force_color();
4120        let _config = builder.console_config();
4121        // Just verify the chain completes without panic
4122    }
4123
4124    // ── Logging config ─────────────────────────────────────────────
4125
4126    #[test]
4127    fn builder_logging_full_config() {
4128        let config = LoggingConfig {
4129            level: LevelFilter::Trace,
4130            timestamps: false,
4131            targets: false,
4132            file_line: true,
4133        };
4134        let _builder = ServerBuilder::new("srv", "1.0").logging(config);
4135    }
4136
4137    // ── List page size ─────────────────────────────────────────────
4138
4139    #[test]
4140    fn builder_list_page_size() {
4141        let server = ServerBuilder::new("srv", "1.0")
4142            .list_page_size(50)
4143            .tool(TestTool)
4144            .build();
4145        assert!(server.has_tools());
4146    }
4147
4148    // ── Resource template ──────────────────────────────────────────
4149
4150    #[test]
4151    fn builder_resource_template_enables_capability() {
4152        let template = ResourceTemplate {
4153            uri_template: "file://{path}".to_string(),
4154            name: "Template".to_string(),
4155            description: None,
4156            mime_type: None,
4157            icon: None,
4158            version: None,
4159            tags: vec![],
4160        };
4161        let server = ServerBuilder::new("srv", "1.0")
4162            .resource_template(template)
4163            .build();
4164        assert!(server.capabilities().resources.is_some());
4165    }
4166
4167    // ── Middleware ──────────────────────────────────────────────────
4168
4169    struct NoopMiddleware;
4170    impl crate::Middleware for NoopMiddleware {}
4171
4172    #[test]
4173    fn builder_middleware() {
4174        let server = ServerBuilder::new("srv", "1.0")
4175            .middleware(NoopMiddleware)
4176            .build();
4177        let _ = server;
4178    }
4179
4180    #[test]
4181    fn builder_multiple_middleware() {
4182        let server = ServerBuilder::new("srv", "1.0")
4183            .middleware(NoopMiddleware)
4184            .middleware(NoopMiddleware)
4185            .build();
4186        let _ = server;
4187    }
4188
4189    // ── Auth provider ──────────────────────────────────────────────
4190
4191    struct TestAuthProvider;
4192    impl crate::AuthProvider for TestAuthProvider {
4193        fn authenticate(
4194            &self,
4195            _ctx: &McpContext,
4196            _request: crate::auth::AuthRequest<'_>,
4197        ) -> McpResult<fastmcp_core::AuthContext> {
4198            Ok(fastmcp_core::AuthContext::with_subject("test-user"))
4199        }
4200    }
4201
4202    #[test]
4203    fn builder_auth_provider() {
4204        let server = ServerBuilder::new("srv", "1.0")
4205            .auth_provider(TestAuthProvider)
4206            .build();
4207        let _ = server;
4208    }
4209
4210    // ── auto_mask_errors ───────────────────────────────────────────
4211
4212    #[test]
4213    fn builder_auto_mask_errors() {
4214        // In debug builds (test mode), auto_mask_errors defaults to false
4215        let builder = ServerBuilder::new("srv", "1.0").auto_mask_errors();
4216        // In debug_assertions mode, masking should be disabled
4217        assert!(!builder.is_error_masking_enabled());
4218    }
4219
4220    // ── Duplicate behavior error ───────────────────────────────────
4221
4222    struct DupTool(&'static str);
4223    impl crate::ToolHandler for DupTool {
4224        fn definition(&self) -> Tool {
4225            Tool {
4226                name: self.0.to_string(),
4227                description: None,
4228                input_schema: serde_json::json!({"type": "object"}),
4229                output_schema: None,
4230                icon: None,
4231                version: None,
4232                tags: vec![],
4233                annotations: None,
4234            }
4235        }
4236        fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
4237            Ok(vec![Content::text("ok")])
4238        }
4239    }
4240
4241    #[test]
4242    fn builder_on_duplicate_error_logs_but_continues() {
4243        // With DuplicateBehavior::Error, duplicate registration logs error
4244        // but builder doesn't panic
4245        let server = ServerBuilder::new("srv", "1.0")
4246            .on_duplicate(DuplicateBehavior::Error)
4247            .tool(DupTool("dup"))
4248            .tool(DupTool("dup")) // duplicate - will log error
4249            .build();
4250        assert!(server.has_tools());
4251    }
4252
4253    // ── Mount ──────────────────────────────────────────────────────
4254
4255    #[test]
4256    fn builder_mount_with_prefix() {
4257        let source = ServerBuilder::new("sub", "1.0")
4258            .tool(TestTool)
4259            .resource(TestResource)
4260            .prompt(TestPrompt)
4261            .build();
4262
4263        let main = ServerBuilder::new("main", "1.0")
4264            .mount(source, Some("sub"))
4265            .build();
4266
4267        assert!(main.has_tools());
4268        assert!(main.has_resources());
4269        assert!(main.has_prompts());
4270    }
4271
4272    #[test]
4273    fn builder_mount_without_prefix() {
4274        let source = ServerBuilder::new("sub", "1.0").tool(TestTool).build();
4275
4276        let main = ServerBuilder::new("main", "1.0")
4277            .mount(source, None)
4278            .build();
4279
4280        assert!(main.has_tools());
4281    }
4282
4283    #[cfg(feature = "apps")]
4284    #[test]
4285    fn builder_mount_rejects_apps_bound_child_without_apps_opt_in_atomically() {
4286        let main = ServerBuilder::new("main", "1.0")
4287            .tool(TestTool)
4288            .resource(TestResource)
4289            .mount(apps_mount_child(), None)
4290            .build();
4291
4292        assert!(main.has_tools());
4293        assert!(main.has_resources());
4294        assert!(
4295            main.extension_registry_receipt().is_none(),
4296            "rejecting a child must not adopt its Apps extension runtime"
4297        );
4298
4299        let router = main.into_router();
4300        assert_eq!(router.tools_count(), 1, "the rejected child adds no tools");
4301        assert_eq!(
4302            router.resources_count(),
4303            1,
4304            "the rejected child adds no resources"
4305        );
4306    }
4307
4308    #[cfg(feature = "apps")]
4309    #[test]
4310    fn builder_mount_accepts_apps_bound_child_with_active_apps_opt_in() {
4311        let main = ServerBuilder::new("main", "1.0")
4312            .mcp_apps()
4313            .expect("parent Apps extension installs")
4314            .mount(apps_mount_child(), None)
4315            .build();
4316
4317        assert!(main.has_tools());
4318        assert!(main.has_resources());
4319        assert_eq!(
4320            main.extension_registry_receipt()
4321                .expect("parent retains its Apps extension runtime")
4322                .descriptor_count(),
4323            1,
4324            "mounting does not transfer or merge the child extension runtime"
4325        );
4326
4327        let router = main.into_router();
4328        assert_eq!(router.tools_count(), 1);
4329        assert_eq!(router.resources_count(), 1);
4330    }
4331
4332    #[cfg(feature = "apps")]
4333    #[test]
4334    fn builder_mount_tools_rejects_apps_bound_child_without_apps_opt_in_atomically() {
4335        let main = ServerBuilder::new("main", "1.0")
4336            .resource(TestResource)
4337            .mount_tools(apps_resource_bound_tool_mount_child(), None)
4338            .build();
4339
4340        assert!(!main.has_tools());
4341        assert!(main.has_resources());
4342        assert!(
4343            main.extension_registry_receipt().is_none(),
4344            "rejecting a child must not adopt its Apps extension runtime"
4345        );
4346
4347        let router = main.into_router();
4348        assert_eq!(router.tools_count(), 0, "the rejected child adds no tools");
4349        assert_eq!(
4350            router.resources_count(),
4351            1,
4352            "the rejected child leaves existing destination resources unchanged"
4353        );
4354    }
4355
4356    #[cfg(feature = "apps")]
4357    #[test]
4358    fn builder_mount_tools_accepts_apps_bound_child_with_active_apps_opt_in() {
4359        let resource = McpAppsUiResource::try_new(
4360            AbsoluteUri::parse("ui://mount/dashboard").expect("fixed Apps mount URI is valid"),
4361            "mounted-apps-dashboard",
4362            "<main>Apps mount fixture</main>",
4363        )
4364        .expect("fixed Apps mount resource is valid");
4365        let main = ServerBuilder::new("main", "1.0")
4366            .mcp_apps()
4367            .expect("parent Apps extension installs")
4368            .mcp_apps_ui_resource(resource)
4369            .expect("parent Apps resource registers")
4370            .mount_tools(apps_mount_child(), None)
4371            .build();
4372
4373        assert!(main.has_tools());
4374        assert!(main.has_resources());
4375        assert_eq!(
4376            main.extension_registry_receipt()
4377                .expect("parent retains its Apps extension runtime")
4378                .descriptor_count(),
4379            1,
4380            "mounting does not transfer or merge the child extension runtime"
4381        );
4382
4383        let router = main.into_router();
4384        assert_eq!(router.tools_count(), 1);
4385        assert_eq!(router.resources_count(), 1);
4386    }
4387
4388    #[cfg(feature = "apps")]
4389    #[test]
4390    fn builder_mount_resources_rejects_apps_bound_child_without_apps_opt_in_atomically() {
4391        let main = ServerBuilder::new("main", "1.0")
4392            .tool(TestTool)
4393            .mount_resources(apps_mount_child(), None)
4394            .build();
4395
4396        assert!(main.has_tools());
4397        assert!(!main.has_resources());
4398        assert!(
4399            main.extension_registry_receipt().is_none(),
4400            "rejecting a child must not adopt its Apps extension runtime"
4401        );
4402
4403        let router = main.into_router();
4404        assert_eq!(
4405            router.tools_count(),
4406            1,
4407            "the rejected child leaves existing destination tools unchanged"
4408        );
4409        assert_eq!(
4410            router.resources_count(),
4411            0,
4412            "the rejected child adds no resources"
4413        );
4414    }
4415
4416    #[cfg(feature = "apps")]
4417    #[test]
4418    fn builder_mount_resources_accepts_apps_bound_child_with_active_apps_opt_in() {
4419        let main = ServerBuilder::new("main", "1.0")
4420            .mcp_apps()
4421            .expect("parent Apps extension installs")
4422            .mount_resources(apps_mount_child(), None)
4423            .build();
4424
4425        assert!(!main.has_tools());
4426        assert!(main.has_resources());
4427        assert_eq!(
4428            main.extension_registry_receipt()
4429                .expect("parent retains its Apps extension runtime")
4430                .descriptor_count(),
4431            1,
4432            "mounting does not transfer or merge the child extension runtime"
4433        );
4434
4435        let router = main.into_router();
4436        assert_eq!(router.tools_count(), 0);
4437        assert_eq!(router.resources_count(), 1);
4438    }
4439
4440    #[test]
4441    fn builder_mount_tools_only() {
4442        let source = ServerBuilder::new("sub", "1.0")
4443            .tool(TestTool)
4444            .resource(TestResource)
4445            .prompt(TestPrompt)
4446            .build();
4447
4448        let main = ServerBuilder::new("main", "1.0")
4449            .mount_tools(source, Some("sub"))
4450            .build();
4451
4452        assert!(main.has_tools());
4453        // Resources and prompts should NOT be mounted
4454        assert!(!main.has_resources());
4455        assert!(!main.has_prompts());
4456    }
4457
4458    #[test]
4459    fn builder_mount_resources_only() {
4460        let source = ServerBuilder::new("sub", "1.0")
4461            .tool(TestTool)
4462            .resource(TestResource)
4463            .prompt(TestPrompt)
4464            .build();
4465
4466        let main = ServerBuilder::new("main", "1.0")
4467            .mount_resources(source, Some("data"))
4468            .build();
4469
4470        assert!(!main.has_tools());
4471        assert!(main.has_resources());
4472        assert!(!main.has_prompts());
4473    }
4474
4475    #[test]
4476    fn builder_mount_prompts_only() {
4477        let source = ServerBuilder::new("sub", "1.0")
4478            .tool(TestTool)
4479            .resource(TestResource)
4480            .prompt(TestPrompt)
4481            .build();
4482
4483        let main = ServerBuilder::new("main", "1.0")
4484            .mount_prompts(source, Some("tmpl"))
4485            .build();
4486
4487        assert!(!main.has_tools());
4488        assert!(!main.has_resources());
4489        assert!(main.has_prompts());
4490    }
4491
4492    #[test]
4493    fn builder_mount_empty_server() {
4494        let source = ServerBuilder::new("empty", "1.0").build();
4495
4496        let main = ServerBuilder::new("main", "1.0")
4497            .mount(source, Some("empty"))
4498            .build();
4499
4500        assert!(!main.has_tools());
4501        assert!(!main.has_resources());
4502        assert!(!main.has_prompts());
4503    }
4504
4505    #[test]
4506    fn builder_full_mount_honors_duplicate_policy_for_all_component_kinds() {
4507        for behavior in [
4508            DuplicateBehavior::Warn,
4509            DuplicateBehavior::Ignore,
4510            DuplicateBehavior::Replace,
4511            DuplicateBehavior::Error,
4512        ] {
4513            let server = marked_builder("original")
4514                .on_duplicate(behavior)
4515                .mount(marked_builder("incoming").build(), None)
4516                .build();
4517            assert_marked_server(
4518                &server,
4519                if behavior == DuplicateBehavior::Replace {
4520                    "incoming"
4521                } else {
4522                    "original"
4523                },
4524            );
4525        }
4526    }
4527
4528    #[test]
4529    fn builder_partial_mounts_honor_duplicate_policy() {
4530        for behavior in [
4531            DuplicateBehavior::Warn,
4532            DuplicateBehavior::Ignore,
4533            DuplicateBehavior::Replace,
4534            DuplicateBehavior::Error,
4535        ] {
4536            let replacement_marker = if behavior == DuplicateBehavior::Replace {
4537                "incoming"
4538            } else {
4539                "original"
4540            };
4541
4542            let tools = marked_builder("original")
4543                .on_duplicate(behavior)
4544                .mount_tools(marked_builder("incoming").build(), None)
4545                .build();
4546            assert_eq!(tools.tools()[0].tags, vec![replacement_marker.to_string()]);
4547            assert_eq!(tools.resources()[0].tags, vec!["original".to_string()]);
4548            assert_eq!(
4549                tools.resource_templates()[0].tags,
4550                vec!["original".to_string()]
4551            );
4552            assert_eq!(tools.prompts()[0].tags, vec!["original".to_string()]);
4553
4554            let resources = marked_builder("original")
4555                .on_duplicate(behavior)
4556                .mount_resources(marked_builder("incoming").build(), None)
4557                .build();
4558            assert_eq!(resources.tools()[0].tags, vec!["original".to_string()]);
4559            assert_eq!(
4560                resources.resources()[0].tags,
4561                vec![replacement_marker.to_string()]
4562            );
4563            assert_eq!(
4564                resources.resource_templates()[0].tags,
4565                vec![replacement_marker.to_string()]
4566            );
4567            assert_eq!(resources.prompts()[0].tags, vec!["original".to_string()]);
4568
4569            let prompts = marked_builder("original")
4570                .on_duplicate(behavior)
4571                .mount_prompts(marked_builder("incoming").build(), None)
4572                .build();
4573            assert_eq!(prompts.tools()[0].tags, vec!["original".to_string()]);
4574            assert_eq!(prompts.resources()[0].tags, vec!["original".to_string()]);
4575            assert_eq!(
4576                prompts.resource_templates()[0].tags,
4577                vec!["original".to_string()]
4578            );
4579            assert_eq!(
4580                prompts.prompts()[0].tags,
4581                vec![replacement_marker.to_string()]
4582            );
4583        }
4584    }
4585
4586    #[test]
4587    fn builder_full_and_partial_mounts_reject_invalid_prefixes() {
4588        let full = marked_builder("original")
4589            .on_duplicate(DuplicateBehavior::Replace)
4590            .mount(marked_builder("incoming").build(), Some("peer/secret"))
4591            .build();
4592        assert_marked_server(&full, "original");
4593
4594        let tools = marked_builder("original")
4595            .on_duplicate(DuplicateBehavior::Replace)
4596            .mount_tools(marked_builder("incoming").build(), Some("peer/secret"))
4597            .build();
4598        assert_marked_server(&tools, "original");
4599
4600        let resources = marked_builder("original")
4601            .on_duplicate(DuplicateBehavior::Replace)
4602            .mount_resources(marked_builder("incoming").build(), Some("peer/secret"))
4603            .build();
4604        assert_marked_server(&resources, "original");
4605
4606        let prompts = marked_builder("original")
4607            .on_duplicate(DuplicateBehavior::Replace)
4608            .mount_prompts(marked_builder("incoming").build(), Some("peer/secret"))
4609            .build();
4610        assert_marked_server(&prompts, "original");
4611    }
4612
4613    #[cfg(feature = "proxy")]
4614    mod proxy_registration_tests {
4615        use super::*;
4616
4617        // ── Proxy registration ─────────────────────────────────────────
4618
4619        struct CompletionProxyBackend {
4620            supported: bool,
4621            result: CoreResult,
4622            calls: Arc<Mutex<Vec<fastmcp_client::CompletionParams>>>,
4623        }
4624
4625        struct LocalFinalCompletionPrompt;
4626
4627        impl crate::PromptHandler for LocalFinalCompletionPrompt {
4628            fn definition(&self) -> Prompt {
4629                Prompt {
4630                    name: "final-deploy".to_owned(),
4631                    description: Some("local completion target".to_owned()),
4632                    arguments: vec![fastmcp_protocol::PromptArgument {
4633                        name: "environment".to_owned(),
4634                        description: None,
4635                        required: false,
4636                    }],
4637                    icon: None,
4638                    version: None,
4639                    tags: Vec::new(),
4640                }
4641            }
4642
4643            fn final_definition(&self) -> Option<fastmcp_protocol::FinalPrompt> {
4644                Some(fastmcp_protocol::FinalPrompt {
4645                    name: "final-deploy".to_owned(),
4646                    title: Some("Local Final Deploy".to_owned()),
4647                    description: Some("local completion target".to_owned()),
4648                    icons: None,
4649                    arguments: Some(vec![fastmcp_protocol::FinalPromptArgument {
4650                        name: "environment".to_owned(),
4651                        title: Some("Local Environment".to_owned()),
4652                        description: None,
4653                        required: Some(false),
4654                    }]),
4655                    meta: None,
4656                })
4657            }
4658
4659            fn get(
4660                &self,
4661                _ctx: &McpContext,
4662                _args: std::collections::HashMap<String, String>,
4663            ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
4664                Ok(Vec::new())
4665            }
4666        }
4667
4668        impl crate::proxy::ProxyBackend for CompletionProxyBackend {
4669            fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
4670                Ok(Vec::new())
4671            }
4672
4673            fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
4674                Ok(Vec::new())
4675            }
4676
4677            fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
4678                Ok(Vec::new())
4679            }
4680
4681            fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
4682                Ok(Vec::new())
4683            }
4684
4685            fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
4686                Ok(Vec::new())
4687            }
4688
4689            fn call_tool_with_progress(
4690                &mut self,
4691                _: &str,
4692                _: serde_json::Value,
4693                _: crate::proxy::ProgressCallback<'_>,
4694            ) -> McpResult<Vec<Content>> {
4695                Ok(Vec::new())
4696            }
4697
4698            fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
4699                Ok(Vec::new())
4700            }
4701
4702            fn get_prompt(
4703                &mut self,
4704                _: &str,
4705                _: std::collections::HashMap<String, String>,
4706            ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
4707                Ok(Vec::new())
4708            }
4709
4710            fn supports_completion(&mut self) -> McpResult<bool> {
4711                Ok(self.supported)
4712            }
4713
4714            fn complete_result(
4715                &mut self,
4716                params: fastmcp_client::CompletionParams,
4717            ) -> McpResult<CoreResult> {
4718                self.calls
4719                    .lock()
4720                    .expect("completion proxy call log is not poisoned")
4721                    .push(params);
4722                Ok(self.result.clone())
4723            }
4724        }
4725
4726        fn legacy_completion_proxy_catalog() -> ProxyTypedCatalog {
4727            ProxyTypedCatalog {
4728                tools: ProxyToolCatalog::Legacy(Vec::new()),
4729                resources: ProxyResourceCatalog::Legacy(Vec::new()),
4730                resource_templates: ProxyResourceTemplateCatalog::Legacy(Vec::new()),
4731                prompts: ProxyPromptCatalog::Legacy(vec![Prompt {
4732                    name: "legacy-deploy".to_owned(),
4733                    description: None,
4734                    arguments: Vec::new(),
4735                    icon: None,
4736                    version: None,
4737                    tags: Vec::new(),
4738                }]),
4739            }
4740        }
4741
4742        fn final_completion_proxy_catalog() -> ProxyTypedCatalog {
4743            let prompt = serde_json::from_value(serde_json::json!({
4744                "name": "final-deploy",
4745                "title": "Upstream Final Deploy",
4746                "arguments": [{"name": "environment", "title": "Environment"}]
4747            }))
4748            .expect("the final completion prompt fixture is valid");
4749            ProxyTypedCatalog {
4750                tools: ProxyToolCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4751                resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4752                resource_templates: ProxyResourceTemplateCatalog::Final(ProxyFinalCatalog::new(
4753                    Vec::new(),
4754                )),
4755                prompts: ProxyPromptCatalog::Final(ProxyFinalCatalog::new(vec![prompt])),
4756            }
4757        }
4758
4759        fn final_completion_proxy_template_catalog() -> ProxyTypedCatalog {
4760            let template = serde_json::from_value(serde_json::json!({
4761                "uriTemplate": "completion://{environment}",
4762                "name": "upstream-completion-template",
4763                "title": "Upstream Completion Template",
4764            }))
4765            .expect("the final completion resource-template fixture is valid");
4766            ProxyTypedCatalog {
4767                tools: ProxyToolCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4768                resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4769                resource_templates: ProxyResourceTemplateCatalog::Final(ProxyFinalCatalog::new(
4770                    vec![template],
4771                )),
4772                prompts: ProxyPromptCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4773            }
4774        }
4775
4776        fn legacy_completion_proxy_template_catalog() -> ProxyTypedCatalog {
4777            ProxyTypedCatalog {
4778                tools: ProxyToolCatalog::Legacy(Vec::new()),
4779                resources: ProxyResourceCatalog::Legacy(Vec::new()),
4780                resource_templates: ProxyResourceTemplateCatalog::Legacy(vec![ResourceTemplate {
4781                    uri_template: "completion://{environment}".to_owned(),
4782                    name: "upstream-completion-template".to_owned(),
4783                    description: None,
4784                    mime_type: None,
4785                    icon: None,
4786                    version: None,
4787                    tags: Vec::new(),
4788                }]),
4789                prompts: ProxyPromptCatalog::Legacy(Vec::new()),
4790            }
4791        }
4792
4793        fn local_completion_template() -> ResourceTemplate {
4794            ResourceTemplate {
4795                uri_template: "completion://{environment}".to_owned(),
4796                name: "local-completion-template".to_owned(),
4797                description: Some("local completion target".to_owned()),
4798                mime_type: None,
4799                icon: None,
4800                version: None,
4801                tags: Vec::new(),
4802            }
4803        }
4804
4805        fn legacy_completion_proxy_result() -> CoreResult {
4806            CoreResult::Legacy(LegacyCoreResult::Completion(
4807                fastmcp_protocol::LegacyCompletionResult {
4808                    completion: fastmcp_protocol::CompletionValues {
4809                        values: vec!["legacy-staging".to_owned()],
4810                        total: Some(1),
4811                        has_more: Some(false),
4812                    },
4813                    meta: None,
4814                },
4815            ))
4816        }
4817
4818        fn final_completion_proxy_result() -> CoreResult {
4819            CoreResult::Final(FinalCoreResult::Completion {
4820                result: CompleteResult::new(
4821                    fastmcp_protocol::FinalCompletionResult {
4822                        completion: fastmcp_protocol::FinalCompletionValues {
4823                            values: vec!["final-staging".to_owned()],
4824                            total: Some(
4825                                serde_json::from_str("92233720368547758081234567890")
4826                                    .expect("the fixed exact completion total is a JSON integer"),
4827                            ),
4828                            has_more: Some(false),
4829                        },
4830                    },
4831                    ResultMeta::server_generated(
4832                        Implementation::try_new("completion-upstream", "1.0")
4833                            .expect("the fixed completion implementation is valid"),
4834                    ),
4835                ),
4836                diagnostic: None,
4837            })
4838        }
4839
4840        fn final_completion_request(id: i64) -> JsonRpcRequest {
4841            JsonRpcRequest::new(
4842                "completion/complete",
4843                Some(serde_json::json!({
4844                    "_meta": {
4845                        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4846                        "io.modelcontextprotocol/clientCapabilities": {},
4847                    },
4848                    "ref": {
4849                        "type": "ref/prompt",
4850                        "name": "final-deploy",
4851                        "title": "Final Deploy",
4852                    },
4853                    "argument": {"name": "environment", "value": "sta"},
4854                    "context": {"arguments": {"region": "us-east-1"}},
4855                })),
4856                id,
4857            )
4858        }
4859
4860        fn final_resource_template_completion_request(id: i64) -> JsonRpcRequest {
4861            JsonRpcRequest::new(
4862                "completion/complete",
4863                Some(serde_json::json!({
4864                    "_meta": {
4865                        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4866                        "io.modelcontextprotocol/clientCapabilities": {},
4867                    },
4868                    "ref": {
4869                        "type": "ref/resource",
4870                        "uri": "completion://{environment}",
4871                    },
4872                    "argument": {"name": "environment", "value": "sta"},
4873                })),
4874                id,
4875            )
4876        }
4877
4878        fn legacy_completion_request(id: i64, reference: serde_json::Value) -> JsonRpcRequest {
4879            JsonRpcRequest::new(
4880                "completion/complete",
4881                Some(serde_json::json!({
4882                    "ref": reference,
4883                    "argument": {"name": "environment", "value": "sta"},
4884                })),
4885                id,
4886            )
4887        }
4888
4889        fn final_proxy_catalog() -> ProxyCatalog {
4890            ProxyCatalog {
4891                tool_catalog_era: Some(ProtocolEra::Modern2026),
4892                final_tools: vec![
4893                    serde_json::from_value(serde_json::json!({
4894                        "name": "weather",
4895                        "title": "Weather Forecast",
4896                        "description": "Returns a precise forecast.",
4897                        "icons": [{
4898                            "src": "https://example.test/icons/weather.svg",
4899                            "mimeType": "image/svg+xml",
4900                            "sizes": ["16x16", "32x32"],
4901                            "theme": "light",
4902                            "com.example/icon": {"retained": true}
4903                        }],
4904                        "inputSchema": {
4905                            "type": "object",
4906                            "properties": {"city": {"type": "string"}}
4907                        },
4908                        "outputSchema": {"type": "object"},
4909                        "annotations": {
4910                            "title": "Forecast",
4911                            "destructiveHint": false,
4912                            "idempotentHint": true,
4913                            "readOnlyHint": true,
4914                            "openWorldHint": false
4915                        },
4916                        "_meta": {"com.example/catalog": {"retained": true}}
4917                    }))
4918                    .expect("the exact final tool fixture is valid"),
4919                ],
4920                ..ProxyCatalog::default()
4921            }
4922        }
4923
4924        fn legacy_proxy_catalog() -> ProxyCatalog {
4925            ProxyCatalog {
4926                tool_catalog_era: Some(ProtocolEra::Legacy2024),
4927                tools: vec![Tool {
4928                    name: "legacy-weather".to_owned(),
4929                    description: Some("Exact legacy proxy fixture".to_owned()),
4930                    input_schema: serde_json::json!({"type": "object"}),
4931                    output_schema: None,
4932                    icon: None,
4933                    version: None,
4934                    tags: Vec::new(),
4935                    annotations: None,
4936                }],
4937                ..ProxyCatalog::default()
4938            }
4939        }
4940
4941        fn final_tools_list_request(id: i64) -> JsonRpcRequest {
4942            JsonRpcRequest::new(
4943                "tools/list",
4944                Some(serde_json::json!({
4945                    "_meta": {
4946                        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4947                        "io.modelcontextprotocol/clientCapabilities": {},
4948                    },
4949                })),
4950                id,
4951            )
4952        }
4953
4954        fn final_tools_call_request(
4955            name: &str,
4956            arguments: serde_json::Value,
4957            id: i64,
4958        ) -> JsonRpcRequest {
4959            JsonRpcRequest::new(
4960                "tools/call",
4961                Some(serde_json::json!({
4962                    "_meta": {
4963                        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4964                        "io.modelcontextprotocol/clientCapabilities": {},
4965                    },
4966                    "name": name,
4967                    "arguments": arguments,
4968                })),
4969                id,
4970            )
4971        }
4972
4973        fn bound_proxy_client<B: crate::proxy::ProxyBackend + 'static>(
4974            backend: B,
4975            era: ProtocolEra,
4976        ) -> ProxyClient {
4977            let mut bindings = ProxyClient::upstream_binding_registry();
4978            let policy = match era {
4979                ProtocolEra::Modern2026 => ProtocolPolicy::ModernOnly,
4980                ProtocolEra::Legacy2024 => ProtocolPolicy::LegacyOnly,
4981            };
4982            let opening = match era {
4983                ProtocolEra::Modern2026 => StdioOpeningFrame::ModernRequest {
4984                    protocol_version: era.version().as_str().to_owned(),
4985                },
4986                ProtocolEra::Legacy2024 => StdioOpeningFrame::LegacyInitialize,
4987            };
4988            let binding = bindings
4989                .bind_stdio(
4990                    "weather-route",
4991                    "stdio:weather",
4992                    "final-catalog-receipt",
4993                    1,
4994                    policy,
4995                    opening,
4996                )
4997                .expect("the route selects the requested exact era");
4998            let upstream_protocol_version = era.version().as_str().to_owned();
4999            ProxyClient::from_backend_with_upstream_binding(
5000                backend,
5001                binding,
5002                &upstream_protocol_version,
5003            )
5004            .expect("the selected upstream version matches its immutable binding")
5005        }
5006
5007        fn final_catalog_proxy_client(era: ProtocolEra) -> ProxyClient {
5008            bound_proxy_client(DuplicatePolicyProxyBackend, era)
5009        }
5010
5011        #[cfg(feature = "tasks")]
5012        fn ordinary_proxy_tasks_client(
5013            calls: Arc<Mutex<Vec<String>>>,
5014            updates: Arc<Mutex<Vec<serde_json::Value>>>,
5015        ) -> ProxyClient {
5016            let task = serde_json::from_value(serde_json::json!({
5017                "resultType": "task",
5018                "taskId": "ordinary-proxy-task-71",
5019                "status": "input_required",
5020                "createdAt": "2026-07-28T12:00:00Z",
5021                "lastUpdatedAt": "2026-07-28T12:00:00Z",
5022                "ttlMs": null,
5023                "inputRequests": {},
5024            }))
5025            .expect("the ordinary proxy Task fixture is exact");
5026            bound_proxy_client(
5027                OrdinaryProxyTasksBackend {
5028                    calls,
5029                    updates,
5030                    task,
5031                },
5032                ProtocolEra::Modern2026,
5033            )
5034        }
5035
5036        fn discovered_duplicate_policy_proxy() -> (ProxyClient, ProxyCatalog) {
5037            let client = ProxyClient::from_backend(DuplicatePolicyProxyBackend);
5038            let catalog = client
5039                .catalog()
5040                .expect("backend discovery supplies the legacy era evidence");
5041            (client, catalog)
5042        }
5043
5044        #[derive(Clone, Copy)]
5045        enum DualEraProxyRoute {
5046            Legacy,
5047            Final,
5048        }
5049
5050        struct RecordingDualEraProxyBackend {
5051            route: DualEraProxyRoute,
5052            calls: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5053        }
5054
5055        impl RecordingDualEraProxyBackend {
5056            fn record(&self, name: &str, arguments: serde_json::Value) {
5057                self.calls
5058                    .lock()
5059                    .expect("the test call log lock is not poisoned")
5060                    .push((name.to_owned(), arguments));
5061            }
5062        }
5063
5064        impl crate::proxy::ProxyBackend for RecordingDualEraProxyBackend {
5065            fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
5066                Ok(Vec::new())
5067            }
5068
5069            fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
5070                Ok(Vec::new())
5071            }
5072
5073            fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
5074                Ok(Vec::new())
5075            }
5076
5077            fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
5078                Ok(Vec::new())
5079            }
5080
5081            fn call_tool(
5082                &mut self,
5083                name: &str,
5084                arguments: serde_json::Value,
5085            ) -> McpResult<Vec<Content>> {
5086                self.record(name, arguments);
5087                Ok(vec![Content::text(match self.route {
5088                    DualEraProxyRoute::Legacy => "bound legacy proxy",
5089                    DualEraProxyRoute::Final => "bound final proxy",
5090                })])
5091            }
5092
5093            fn call_tool_with_progress(
5094                &mut self,
5095                name: &str,
5096                arguments: serde_json::Value,
5097                _: crate::proxy::ProgressCallback<'_>,
5098            ) -> McpResult<Vec<Content>> {
5099                self.call_tool(name, arguments)
5100            }
5101
5102            fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
5103                Ok(Vec::new())
5104            }
5105
5106            fn get_prompt(
5107                &mut self,
5108                _: &str,
5109                _: std::collections::HashMap<String, String>,
5110            ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
5111                Ok(Vec::new())
5112            }
5113
5114            fn call_tool_result(
5115                &mut self,
5116                name: &str,
5117                arguments: serde_json::Value,
5118            ) -> McpResult<CoreResult> {
5119                self.record(name, arguments);
5120                match self.route {
5121                    DualEraProxyRoute::Legacy => Ok(CoreResult::Legacy(
5122                        LegacyCoreResult::ToolsCall(CallToolResult {
5123                            content: vec![LegacyContent::Text {
5124                                text: "bound legacy proxy".to_owned(),
5125                                annotations: None,
5126                                additional: std::collections::BTreeMap::new(),
5127                            }],
5128                            is_error: false,
5129                            meta: None,
5130                            additional: std::collections::BTreeMap::new(),
5131                        }),
5132                    )),
5133                    DualEraProxyRoute::Final => Ok(CoreResult::Final(FinalCoreResult::ToolsCall {
5134                        result: CompleteResult::new(
5135                            FinalCallToolResult {
5136                                content: vec![ContentBlock::text("bound final proxy")],
5137                                is_error: false,
5138                                structured_content: Some(serde_json::json!({"route": "final"})),
5139                            },
5140                            ResultMeta::server_generated(
5141                                Implementation::try_new("bound-final-upstream", "1.0")
5142                                    .expect("the fixed test implementation is valid"),
5143                            ),
5144                        ),
5145                        diagnostic: None,
5146                    })),
5147                }
5148            }
5149        }
5150
5151        fn dual_era_proxy_client(
5152            route: DualEraProxyRoute,
5153            calls: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5154        ) -> ProxyClient {
5155            let era = match route {
5156                DualEraProxyRoute::Legacy => ProtocolEra::Legacy2024,
5157                DualEraProxyRoute::Final => ProtocolEra::Modern2026,
5158            };
5159            let (route_id, upstream_identity) = match route {
5160                DualEraProxyRoute::Legacy => ("dual-era-legacy-route", "stdio:dual-era-legacy"),
5161                DualEraProxyRoute::Final => ("dual-era-final-route", "stdio:dual-era-final"),
5162            };
5163            let policy = match era {
5164                ProtocolEra::Modern2026 => ProtocolPolicy::ModernOnly,
5165                ProtocolEra::Legacy2024 => ProtocolPolicy::LegacyOnly,
5166            };
5167            let opening = match era {
5168                ProtocolEra::Modern2026 => StdioOpeningFrame::ModernRequest {
5169                    protocol_version: era.version().as_str().to_owned(),
5170                },
5171                ProtocolEra::Legacy2024 => StdioOpeningFrame::LegacyInitialize,
5172            };
5173            let mut bindings = ProxyClient::upstream_binding_registry();
5174            let binding = bindings
5175                .bind_stdio(
5176                    route_id,
5177                    upstream_identity,
5178                    "dual-era-receipt",
5179                    1,
5180                    policy,
5181                    opening,
5182                )
5183                .expect("the test route selects one immutable era");
5184            let upstream_protocol_version = era.version().as_str().to_owned();
5185            ProxyClient::from_backend_with_upstream_binding(
5186                RecordingDualEraProxyBackend { route, calls },
5187                binding,
5188                &upstream_protocol_version,
5189            )
5190            .expect("the selected upstream version matches its immutable binding")
5191        }
5192
5193        fn dual_era_proxy_server() -> (
5194            crate::Server,
5195            Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5196            Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5197        ) {
5198            let legacy_calls = Arc::new(Mutex::new(Vec::new()));
5199            let final_calls = Arc::new(Mutex::new(Vec::new()));
5200            let server = ServerBuilder::new("srv", "1.0")
5201                .proxy(
5202                    dual_era_proxy_client(DualEraProxyRoute::Legacy, Arc::clone(&legacy_calls)),
5203                    legacy_proxy_catalog(),
5204                )
5205                .expect("the legacy proxy catalog agrees with its bound route")
5206                .proxy(
5207                    dual_era_proxy_client(DualEraProxyRoute::Final, Arc::clone(&final_calls)),
5208                    final_proxy_catalog(),
5209                )
5210                .expect("the final proxy catalog agrees with its bound route")
5211                .build();
5212            (server, legacy_calls, final_calls)
5213        }
5214
5215        fn initialized_legacy_proxy_session(server: &crate::Server) -> crate::Session {
5216            let mut session =
5217                crate::Session::new(server.info().clone(), server.capabilities().clone());
5218            session.initialize(
5219                fastmcp_protocol::ClientInfo {
5220                    name: "dual-era-legacy-client".to_owned(),
5221                    version: "1.0".to_owned(),
5222                },
5223                fastmcp_protocol::ClientCapabilities::default(),
5224                "2024-11-05".to_owned(),
5225            );
5226            session
5227        }
5228
5229        fn assert_rejected_proxy_catalog_returns_an_error(catalog: ProxyCatalog) {
5230            let error = match ServerBuilder::new("srv", "1.0").tool(TestTool).proxy(
5231                ProxyClient::from_backend(DuplicatePolicyProxyBackend),
5232                catalog,
5233            ) {
5234                Ok(_) => {
5235                    panic!("the malformed or unbound proxy catalog is rejected before registration")
5236                }
5237                Err(error) => error,
5238            };
5239
5240            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
5241        }
5242
5243        #[test]
5244        fn builder_proxy_accepts_a_coherent_legacy_catalog() {
5245            let server = ServerBuilder::new("srv", "1.0")
5246                .proxy(
5247                    final_catalog_proxy_client(ProtocolEra::Legacy2024),
5248                    legacy_proxy_catalog(),
5249                )
5250                .expect("the coherent legacy catalog agrees with its route")
5251                .build();
5252
5253            assert!(server.has_tools());
5254            let tools = server.tools();
5255            assert_eq!(
5256                tools[0].name, "legacy-weather",
5257                "the public builder path registers a catalog whose marker and entries select legacy"
5258            );
5259        }
5260
5261        #[test]
5262        fn builder_proxy_rejects_an_unbound_caller_asserted_legacy_catalog() {
5263            let error = match ServerBuilder::new("srv", "1.0").proxy(
5264                ProxyClient::from_backend(DuplicatePolicyProxyBackend),
5265                legacy_proxy_catalog(),
5266            ) {
5267                Ok(_) => panic!("a caller-supplied catalog cannot bind an unbound proxy route"),
5268                Err(error) => error,
5269            };
5270
5271            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
5272            assert!(error.message.contains("cannot bind an unbound route"));
5273        }
5274
5275        #[test]
5276        fn builder_proxy_rejects_legacy_tools_when_only_the_marker_changes_to_modern() {
5277            let mut catalog = legacy_proxy_catalog();
5278            catalog.tool_catalog_era = Some(ProtocolEra::Modern2026);
5279
5280            assert_rejected_proxy_catalog_returns_an_error(catalog);
5281        }
5282
5283        #[test]
5284        fn builder_proxy_rejects_final_tools_when_only_the_marker_changes_to_legacy() {
5285            let mut catalog = final_proxy_catalog();
5286            catalog.tool_catalog_era = Some(ProtocolEra::Legacy2024);
5287
5288            assert_rejected_proxy_catalog_returns_an_error(catalog);
5289        }
5290
5291        #[test]
5292        fn builder_proxy_rejects_mixed_vectors_when_only_final_tools_are_added() {
5293            let mut catalog = legacy_proxy_catalog();
5294            catalog.final_tools = final_proxy_catalog().final_tools;
5295
5296            assert_rejected_proxy_catalog_returns_an_error(catalog);
5297        }
5298
5299        #[test]
5300        fn builder_proxy_rejects_a_missing_marker() {
5301            let mut catalog = legacy_proxy_catalog();
5302            catalog.tool_catalog_era = None;
5303
5304            assert_rejected_proxy_catalog_returns_an_error(catalog);
5305        }
5306
5307        #[test]
5308        fn public_builder_proxy_advertises_and_lists_the_exact_final_tool_catalog() {
5309            let catalog = final_proxy_catalog();
5310            let expected = serde_json::to_value(&catalog.final_tools[0])
5311                .expect("the final fixture serializes");
5312            let server = ServerBuilder::new("srv", "1.0")
5313                .proxy(final_catalog_proxy_client(ProtocolEra::Modern2026), catalog)
5314                .expect("the final catalog agrees with its bound route")
5315                .build();
5316
5317            assert!(server.has_tools());
5318            assert!(server.tools().is_empty());
5319            let discovery = serde_json::to_value(
5320                server
5321                    .server_discovery()
5322                    .expect("the public final proxy server is discoverable"),
5323            )
5324            .expect("final discovery serializes");
5325            assert_eq!(
5326                discovery.pointer("/capabilities/tools"),
5327                Some(&serde_json::json!({}))
5328            );
5329            let inbound = crate::InboundRequestContext::new(
5330                Cx::for_testing(),
5331                701,
5332                crate::InboundRequestTransport::Memory,
5333            );
5334            let response = server
5335                .dispatch_stateless(&inbound, &final_tools_list_request(701))
5336                .expect("the public server path responds to the modern tools/list request");
5337            assert!(response.error.is_none());
5338            let catalog = response
5339                .result
5340                .expect("the modern tools/list response has a result payload");
5341            assert_eq!(catalog["tools"][0], expected);
5342        }
5343
5344        #[test]
5345        fn public_builder_proxy_final_catalog_absence_changes_only_tool_advertisement_and_registry()
5346        {
5347            let mut catalog = final_proxy_catalog();
5348            let configured_tool_count = catalog.final_tools.len();
5349            catalog.final_tools.clear();
5350            assert_eq!(
5351                configured_tool_count, 1,
5352                "the planted negative removes only the final tool catalog entry"
5353            );
5354
5355            let server = ServerBuilder::new("srv", "1.0")
5356                .proxy(final_catalog_proxy_client(ProtocolEra::Modern2026), catalog)
5357                .expect("an otherwise identical empty final catalog remains admissible")
5358                .build();
5359
5360            assert!(!server.has_tools());
5361            let discovery = serde_json::to_value(
5362                server
5363                    .server_discovery()
5364                    .expect("the empty final proxy server remains discoverable"),
5365            )
5366            .expect("empty final discovery serializes");
5367            assert!(discovery.pointer("/capabilities/tools").is_none());
5368            let inbound = crate::InboundRequestContext::new(
5369                Cx::for_testing(),
5370                702,
5371                crate::InboundRequestTransport::Memory,
5372            );
5373            let response = server
5374                .dispatch_stateless(&inbound, &final_tools_list_request(702))
5375                .expect("the public final tools/list path responds for an empty catalog");
5376            assert_eq!(response.result, Some(serde_json::json!({"tools": []})));
5377            assert!(
5378                server.tools().is_empty(),
5379                "removing only the final catalog entry leaves no legacy registry mutation"
5380            );
5381        }
5382
5383        #[cfg(feature = "tasks")]
5384        #[test]
5385        fn public_builder_proxy_relays_modern_tasks_input_required_and_listener() {
5386            let calls = Arc::new(Mutex::new(Vec::new()));
5387            let updates = Arc::new(Mutex::new(Vec::new()));
5388            let proxy = ordinary_proxy_tasks_client(Arc::clone(&calls), Arc::clone(&updates));
5389            let server = Arc::new(
5390                ServerBuilder::new("ordinary-proxy-tasks", "1.0")
5391                    .proxy(proxy.clone(), final_proxy_catalog())
5392                    .expect("the ordinary public proxy path installs the admitted modern route")
5393                    .build(),
5394            );
5395            let discovery = serde_json::to_value(
5396                server
5397                    .server_discovery()
5398                    .expect("the Tasks proxy is publicly discoverable"),
5399            )
5400            .expect("Tasks proxy discovery serializes");
5401            assert_eq!(
5402                discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
5403                Some(&serde_json::json!({})),
5404                "the ordinary proxy path advertises the same Tasks extension as the typed path"
5405            );
5406
5407            let cx = Cx::for_testing();
5408            let connection = crate::ModernConnection::new();
5409            let emitted_notifications = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
5410            let notification_sender: crate::NotificationSender = {
5411                let emitted_notifications = Arc::clone(&emitted_notifications);
5412                Arc::new(move |notification| {
5413                    emitted_notifications
5414                        .lock()
5415                        .expect("ordinary proxy emitted notification log is not poisoned")
5416                        .push(notification);
5417                })
5418            };
5419            let dispatch_modern = |request_id, request| {
5420                let inbound = crate::InboundRequestContext::with_modern_connection(
5421                    cx.clone(),
5422                    request_id,
5423                    crate::InboundRequestTransport::Memory,
5424                    &connection,
5425                );
5426                block_on(Arc::clone(&server).dispatch_with_protocol_policy_owned(
5427                    server.protocol_policy,
5428                    &inbound,
5429                    request,
5430                    None,
5431                    None,
5432                    None,
5433                    None,
5434                    fastmcp_core::McpRequestCancellation::new(),
5435                    None,
5436                    Arc::clone(&notification_sender),
5437                ))
5438                .expect("public ordinary proxy request receives a JSON-RPC response")
5439            };
5440            let task_parameters = serde_json::json!({
5441                "_meta": {
5442                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
5443                    "io.modelcontextprotocol/clientCapabilities": {
5444                        "extensions": {"io.modelcontextprotocol/tasks": {}}
5445                    },
5446                },
5447                "name": "weather",
5448                "arguments": {"outcome": "task"},
5449            });
5450            let task = dispatch_modern(
5451                703,
5452                JsonRpcRequest::new("tools/call", Some(task_parameters.clone()), 703_i64),
5453            );
5454            assert_eq!(
5455                task.result
5456                    .as_ref()
5457                    .and_then(|result| result.get("resultType")),
5458                Some(&serde_json::json!("task"))
5459            );
5460            assert_eq!(
5461                task.result.as_ref().and_then(|result| result.get("taskId")),
5462                Some(&serde_json::json!("ordinary-proxy-task-71"))
5463            );
5464            let relay_after_task = proxy
5465                .final_task_registry_snapshot_for_test()
5466                .expect("the retained ordinary proxy exposes its route-local Task snapshot");
5467            assert_eq!(
5468                relay_after_task.pointer("/tasks/ordinary-proxy-task-71/status"),
5469                Some(&serde_json::json!("input_required")),
5470                "the public tools/call task result is retained by the ordinary route-local relay"
5471            );
5472
5473            let update = dispatch_modern(
5474                704,
5475                JsonRpcRequest::new(
5476                    "tasks/update",
5477                    Some(serde_json::json!({
5478                        "taskId": "ordinary-proxy-task-71",
5479                        "inputResponses": {},
5480                        "_meta": task_parameters["_meta"].clone(),
5481                    })),
5482                    704_i64,
5483                ),
5484            );
5485            assert_eq!(
5486                update
5487                    .result
5488                    .as_ref()
5489                    .and_then(|result| result.get("resultType")),
5490                Some(&serde_json::json!("complete")),
5491                "the public Tasks update preserves the upstream final result algebra"
5492            );
5493            assert_eq!(
5494                updates
5495                    .lock()
5496                    .expect("ordinary proxy task update receipt is not poisoned")
5497                    .as_slice(),
5498                [serde_json::json!({})],
5499                "the public Tasks update reaches the upstream backend through the local relay"
5500            );
5501            assert_eq!(
5502                calls
5503                    .lock()
5504                    .expect("ordinary proxy task call log is not poisoned")
5505                    .as_slice(),
5506                ["tools/call:weather".to_owned(), "tasks/update".to_owned()],
5507                "the public update reaches the selected upstream only after local relay admission"
5508            );
5509
5510            let mut input_required_parameters = task_parameters.clone();
5511            input_required_parameters["arguments"] = serde_json::json!({});
5512            let listened = dispatch_modern(
5513                705,
5514                JsonRpcRequest::new(
5515                    "subscriptions/listen",
5516                    Some(serde_json::json!({
5517                        "notifications": {"taskIds": []},
5518                        "_meta": task_parameters["_meta"].clone(),
5519                    })),
5520                    705_i64,
5521                ),
5522            );
5523            assert_eq!(
5524                listened
5525                    .result
5526                    .as_ref()
5527                    .and_then(|result| result.get("resultType")),
5528                Some(&serde_json::json!("complete"))
5529            );
5530            let emitted_before_rejection = emitted_notifications
5531                .lock()
5532                .expect("ordinary proxy emitted notification log is not poisoned")
5533                .clone();
5534            assert_eq!(
5535                emitted_before_rejection.len(),
5536                1,
5537                "the public listener emits its acknowledgement before terminal completion"
5538            );
5539            assert_eq!(
5540                emitted_before_rejection
5541                    .first()
5542                    .map(|notification| notification.method.as_str()),
5543                Some("notifications/subscriptions/acknowledged"),
5544                "the captured local notification is the listener acknowledgement"
5545            );
5546            let calls_before_rejection = calls
5547                .lock()
5548                .expect("ordinary proxy task call log is not poisoned")
5549                .clone();
5550            let relay_before_rejection = proxy
5551                .final_task_registry_snapshot_for_test()
5552                .expect("the ordinary proxy keeps its local final Task registry");
5553            let subscriptions_before_rejection = server.final_subscription_snapshot_for_test();
5554            let mut missing_capability = task_parameters;
5555            assert!(
5556                missing_capability
5557                    .pointer_mut("/_meta/io.modelcontextprotocol~1clientCapabilities/extensions")
5558                    .and_then(serde_json::Value::as_object_mut)
5559                    .expect("the admitted request has an extension map")
5560                    .remove("io.modelcontextprotocol/tasks")
5561                    .is_some(),
5562                "the RH-5 negative changes only the client Tasks capability"
5563            );
5564            let rejected = dispatch_modern(
5565                706,
5566                JsonRpcRequest::new("tools/call", Some(missing_capability), 706_i64),
5567            );
5568            assert!(rejected.error.is_some());
5569            assert_eq!(
5570                calls
5571                    .lock()
5572                    .expect("ordinary proxy task call log is not poisoned")
5573                    .clone(),
5574                calls_before_rejection,
5575                "the one-field capability rejection must not invoke or mutate the upstream route"
5576            );
5577            assert_eq!(
5578                proxy
5579                    .final_task_registry_snapshot_for_test()
5580                    .expect("the ordinary proxy keeps its local final Task registry"),
5581                relay_before_rejection,
5582                "the one-field capability rejection must not mutate the local relay task registry"
5583            );
5584            assert_eq!(
5585                server.final_subscription_snapshot_for_test(),
5586                subscriptions_before_rejection,
5587                "the one-field capability rejection must not mutate local subscription delivery state"
5588            );
5589            assert_eq!(
5590                serde_json::to_value(
5591                    emitted_notifications
5592                        .lock()
5593                        .expect("ordinary proxy emitted notification log is not poisoned")
5594                        .clone()
5595                )
5596                .expect("emitted notification log serializes"),
5597                serde_json::to_value(emitted_before_rejection)
5598                    .expect("baseline notification log serializes"),
5599                "the one-field capability rejection must not emit or alter local notification state"
5600            );
5601
5602            let input_required = dispatch_modern(
5603                707,
5604                JsonRpcRequest::new("tools/call", Some(input_required_parameters), 707_i64),
5605            );
5606            assert_eq!(
5607                input_required
5608                    .result
5609                    .as_ref()
5610                    .and_then(|result| result.get("resultType")),
5611                Some(&serde_json::json!("input_required"))
5612            );
5613            assert_ne!(
5614                input_required
5615                    .result
5616                    .as_ref()
5617                    .and_then(|result| result.get("requestState")),
5618                Some(&serde_json::json!("upstream-forged-state")),
5619                "the downstream router must mint rather than replay upstream MRTR state"
5620            );
5621
5622            assert_eq!(
5623                calls
5624                    .lock()
5625                    .expect("ordinary proxy task call log is not poisoned")
5626                    .as_slice(),
5627                [
5628                    "tools/call:weather".to_owned(),
5629                    "tasks/update".to_owned(),
5630                    "subscriptions/listen".to_owned(),
5631                    "tools/call:weather".to_owned(),
5632                ],
5633                "public task update, listener, and input_required requests stay on the one admitted modern upstream"
5634            );
5635        }
5636
5637        #[test]
5638        fn builder_proxy_dual_era_preserves_final_catalog_and_routes_bound_calls() {
5639            let expected_final_tool = serde_json::to_value(&final_proxy_catalog().final_tools[0])
5640                .expect("the final fixture serializes");
5641            let (server, legacy_calls, final_calls) = dual_era_proxy_server();
5642            let mut legacy_session = initialized_legacy_proxy_session(&server);
5643            let notification_sender: crate::NotificationSender = Arc::new(|_| {});
5644            let request_sender = crate::RequestSender::new(
5645                Arc::new(crate::PendingRequests::new()),
5646                Arc::new(|message| {
5647                    Err(format!("unexpected outbound message in test: {message:?}"))
5648                }),
5649            );
5650
5651            let legacy_catalog = server
5652                .dispatch_request(
5653                    &Cx::for_testing(),
5654                    &mut legacy_session,
5655                    JsonRpcRequest::new("tools/list", Some(serde_json::json!({})), 801_i64),
5656                    &notification_sender,
5657                    &request_sender,
5658                )
5659                .expect("the legacy tools/list request receives a response")
5660                .result
5661                .expect("the legacy tools/list response has a result payload");
5662            assert_eq!(legacy_catalog["tools"].as_array().map(Vec::len), Some(1));
5663            assert_eq!(legacy_catalog["tools"][0]["name"], "legacy-weather");
5664
5665            let final_inbound = crate::InboundRequestContext::new(
5666                Cx::for_testing(),
5667                802,
5668                crate::InboundRequestTransport::Memory,
5669            );
5670            let final_catalog = server
5671                .dispatch_stateless(&final_inbound, &final_tools_list_request(802_i64))
5672                .expect("the final tools/list request receives a response")
5673                .result
5674                .expect("the final tools/list response has a result payload");
5675            assert_eq!(final_catalog["tools"].as_array().map(Vec::len), Some(1));
5676            assert_eq!(final_catalog["tools"][0]["name"], "weather");
5677            assert_eq!(
5678                serde_json::to_vec(&final_catalog["tools"][0])
5679                    .expect("the emitted final tool normalizes to JSON"),
5680                serde_json::to_vec(&expected_final_tool)
5681                    .expect("the exact final fixture normalizes to JSON"),
5682                "the final proxy path retains the full normalized FinalTool model"
5683            );
5684
5685            let legacy_call = server
5686                .dispatch_request(
5687                    &Cx::for_testing(),
5688                    &mut legacy_session,
5689                    JsonRpcRequest::new(
5690                        "tools/call",
5691                        Some(serde_json::json!({
5692                            "name": "legacy-weather",
5693                            "arguments": {"city": "Portland"},
5694                        })),
5695                        803_i64,
5696                    ),
5697                    &notification_sender,
5698                    &request_sender,
5699                )
5700                .expect("the legacy tools/call request receives a response")
5701                .result
5702                .expect("the legacy tools/call response has a result payload");
5703            assert_eq!(legacy_call["content"][0]["text"], "bound legacy proxy");
5704
5705            let final_call_inbound = crate::InboundRequestContext::new(
5706                Cx::for_testing(),
5707                804,
5708                crate::InboundRequestTransport::Memory,
5709            );
5710            let final_call = server
5711                .dispatch_stateless(
5712                    &final_call_inbound,
5713                    &final_tools_call_request(
5714                        "weather",
5715                        serde_json::json!({"city": "Boston"}),
5716                        804,
5717                    ),
5718                )
5719                .expect("the final tools/call request receives a response")
5720                .result
5721                .expect("the final tools/call response has a result payload");
5722            assert_eq!(final_call["resultType"], "complete");
5723            assert_eq!(final_call["content"][0]["text"], "bound final proxy");
5724            assert_eq!(
5725                final_call["structuredContent"],
5726                serde_json::json!({"route": "final"})
5727            );
5728
5729            assert_eq!(
5730                legacy_calls
5731                    .lock()
5732                    .expect("the test call log lock is not poisoned")
5733                    .clone(),
5734                vec![(
5735                    "legacy-weather".to_owned(),
5736                    serde_json::json!({"city": "Portland"}),
5737                )],
5738                "the legacy request reaches only its bound legacy upstream"
5739            );
5740            assert_eq!(
5741                final_calls
5742                    .lock()
5743                    .expect("the test call log lock is not poisoned")
5744                    .clone(),
5745                vec![("weather".to_owned(), serde_json::json!({"city": "Boston"}))],
5746                "the final request reaches only its bound final upstream"
5747            );
5748        }
5749
5750        #[test]
5751        fn builder_proxy_dual_era_rejects_cross_era_names_without_upstream_calls() {
5752            let (server, legacy_calls, final_calls) = dual_era_proxy_server();
5753            let mut legacy_session = initialized_legacy_proxy_session(&server);
5754            let notification_sender: crate::NotificationSender = Arc::new(|_| {});
5755            let request_sender = crate::RequestSender::new(
5756                Arc::new(crate::PendingRequests::new()),
5757                Arc::new(|message| {
5758                    Err(format!("unexpected outbound message in test: {message:?}"))
5759                }),
5760            );
5761
5762            let legacy_rejected = server
5763                .dispatch_request(
5764                    &Cx::for_testing(),
5765                    &mut legacy_session,
5766                    JsonRpcRequest::new(
5767                        "tools/call",
5768                        Some(serde_json::json!({
5769                            "name": "weather",
5770                            "arguments": {},
5771                        })),
5772                        805_i64,
5773                    ),
5774                    &notification_sender,
5775                    &request_sender,
5776                )
5777                .expect("the legacy cross-era request receives a JSON-RPC error");
5778            assert!(legacy_rejected.result.is_none());
5779            assert_eq!(
5780                legacy_rejected.error.and_then(|error| error.code.as_i32()),
5781                Some(-32601),
5782                "the final-only name is absent from the legacy call route"
5783            );
5784
5785            let final_inbound = crate::InboundRequestContext::new(
5786                Cx::for_testing(),
5787                806,
5788                crate::InboundRequestTransport::Memory,
5789            );
5790            let final_rejected = server
5791                .dispatch_stateless(
5792                    &final_inbound,
5793                    &final_tools_call_request("legacy-weather", serde_json::json!({}), 806),
5794                )
5795                .expect("the final cross-era request receives a JSON-RPC error");
5796            assert!(final_rejected.result.is_none());
5797            assert_eq!(
5798                final_rejected.error.and_then(|error| error.code.as_i32()),
5799                Some(-32602),
5800                "the legacy-only name is absent from the final call route"
5801            );
5802
5803            assert!(
5804                legacy_calls
5805                    .lock()
5806                    .expect("the test call log lock is not poisoned")
5807                    .is_empty(),
5808                "the final-only name must be rejected before the legacy upstream is called"
5809            );
5810            assert!(
5811                final_calls
5812                    .lock()
5813                    .expect("the test call log lock is not poisoned")
5814                    .is_empty(),
5815                "the legacy-only name must be rejected before the final upstream is called"
5816            );
5817        }
5818
5819        #[test]
5820        fn public_proxy_completion_forwards_exact_legacy_and_final_results() {
5821            let legacy_calls = Arc::new(Mutex::new(Vec::new()));
5822            let final_calls = Arc::new(Mutex::new(Vec::new()));
5823            let server = ServerBuilder::new("completion-proxy", "1.0")
5824                .proxy_typed(
5825                    bound_proxy_client(
5826                        CompletionProxyBackend {
5827                            supported: true,
5828                            result: legacy_completion_proxy_result(),
5829                            calls: Arc::clone(&legacy_calls),
5830                        },
5831                        ProtocolEra::Legacy2024,
5832                    ),
5833                    legacy_completion_proxy_catalog(),
5834                )
5835                .expect("the exact legacy completion proxy installs")
5836                .proxy_typed(
5837                    bound_proxy_client(
5838                        CompletionProxyBackend {
5839                            supported: true,
5840                            result: final_completion_proxy_result(),
5841                            calls: Arc::clone(&final_calls),
5842                        },
5843                        ProtocolEra::Modern2026,
5844                    ),
5845                    final_completion_proxy_catalog(),
5846                )
5847                .expect("the exact final completion proxy installs")
5848                .build();
5849            assert!(
5850                server.capabilities().completions.is_some(),
5851                "installing a proxied completion provider must advertise initialize completions"
5852            );
5853
5854            let mut legacy_session = initialized_legacy_proxy_session(&server);
5855            let notification_sender: crate::NotificationSender = Arc::new(|_| {});
5856            let request_sender = crate::RequestSender::new(
5857                Arc::new(crate::PendingRequests::new()),
5858                Arc::new(|message| {
5859                    Err(format!("unexpected outbound message in test: {message:?}"))
5860                }),
5861            );
5862            let legacy = server
5863                .dispatch_request(
5864                    &Cx::for_testing(),
5865                    &mut legacy_session,
5866                    JsonRpcRequest::new(
5867                        "completion/complete",
5868                        Some(serde_json::json!({
5869                            "ref": {"type": "ref/prompt", "name": "legacy-deploy"},
5870                            "argument": {"name": "environment", "value": "sta"},
5871                        })),
5872                        821_i64,
5873                    ),
5874                    &notification_sender,
5875                    &request_sender,
5876                )
5877                .expect("the public exact-2024 completion path returns a response")
5878                .result
5879                .expect("the exact-2024 completion response has a result payload");
5880            assert_eq!(
5881                legacy["completion"]["values"],
5882                serde_json::json!(["legacy-staging"])
5883            );
5884
5885            let final_inbound = crate::InboundRequestContext::new(
5886                Cx::for_testing(),
5887                822,
5888                crate::InboundRequestTransport::Memory,
5889            );
5890            let final_response = server
5891                .dispatch_stateless(&final_inbound, &final_completion_request(822))
5892                .expect("the public final completion path returns a response")
5893                .result
5894                .expect("the final completion response has a result payload");
5895            assert_eq!(final_response["resultType"], "complete");
5896            assert_eq!(
5897                final_response["completion"]["values"],
5898                serde_json::json!(["final-staging"])
5899            );
5900            assert_eq!(
5901                final_response["completion"]["total"],
5902                serde_json::json!(92233720368547758081234567890_u128),
5903                "the proxy retains the final arbitrary-precision completion total"
5904            );
5905
5906            let legacy_calls = legacy_calls
5907                .lock()
5908                .expect("legacy completion call log is not poisoned");
5909            assert_eq!(legacy_calls.len(), 1);
5910            assert!(matches!(
5911                &legacy_calls[0].reference,
5912                fastmcp_client::CompletionReference::Prompt { name } if name == "legacy-deploy"
5913            ));
5914            assert!(legacy_calls[0].context.is_none());
5915            let final_calls = final_calls
5916                .lock()
5917                .expect("final completion call log is not poisoned");
5918            assert_eq!(final_calls.len(), 1);
5919            assert!(matches!(
5920                &final_calls[0].reference,
5921                fastmcp_client::CompletionReference::PromptWithTitle { name, title }
5922                    if name == "final-deploy" && title == "Final Deploy"
5923            ));
5924            assert_eq!(
5925                final_calls[0]
5926                    .context
5927                    .as_ref()
5928                    .and_then(|context| context.arguments.as_ref())
5929                    .and_then(|arguments| arguments.get("region")),
5930                Some(&"us-east-1".to_owned()),
5931                "the proxy forwards final completion context unchanged"
5932            );
5933        }
5934
5935        #[test]
5936        fn public_proxy_completion_unsupported_upstream_rejects_without_downstream_mutation() {
5937            let calls = Arc::new(Mutex::new(Vec::new()));
5938            let server = ServerBuilder::new("completion-proxy", "1.0")
5939                .proxy_typed(
5940                    bound_proxy_client(
5941                        CompletionProxyBackend {
5942                            supported: false,
5943                            result: final_completion_proxy_result(),
5944                            calls: Arc::clone(&calls),
5945                        },
5946                        ProtocolEra::Modern2026,
5947                    ),
5948                    final_completion_proxy_catalog(),
5949                )
5950                .expect("changing only upstream completion support preserves catalog registration")
5951                .build();
5952            assert!(
5953                server.capabilities().completions.is_none(),
5954                "an unsupported upstream must not advertise initialize completions"
5955            );
5956            let discovery = serde_json::to_value(
5957                server
5958                    .server_discovery()
5959                    .expect("the final proxy server remains discoverable"),
5960            )
5961            .expect("discovery serializes");
5962            assert!(
5963                discovery["capabilities"].get("completions").is_none(),
5964                "an unsupported upstream must not create a downstream completion claim"
5965            );
5966
5967            let before_inbound = crate::InboundRequestContext::new(
5968                Cx::for_testing(),
5969                823,
5970                crate::InboundRequestTransport::Memory,
5971            );
5972            let before = server
5973                .dispatch_stateless(
5974                    &before_inbound,
5975                    &JsonRpcRequest::new(
5976                        "prompts/list",
5977                        Some(serde_json::json!({
5978                            "_meta": {
5979                                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
5980                                "io.modelcontextprotocol/clientCapabilities": {},
5981                            },
5982                        })),
5983                        823_i64,
5984                    ),
5985                )
5986                .expect("the proxied final prompt catalog is public before rejection")
5987                .result
5988                .expect("the proxied final prompt catalog has a result payload");
5989            let rejected_inbound = crate::InboundRequestContext::new(
5990                Cx::for_testing(),
5991                824,
5992                crate::InboundRequestTransport::Memory,
5993            );
5994            let rejected = server
5995                .dispatch_stateless(&rejected_inbound, &final_completion_request(824))
5996                .expect("the unsupported completion request receives a JSON-RPC error");
5997            assert_eq!(
5998                rejected.error.and_then(|error| error.code.as_i32()),
5999                Some(-32601),
6000                "the absent local completion handler rejects before an upstream call"
6001            );
6002            assert!(rejected.result.is_none());
6003            let after_inbound = crate::InboundRequestContext::new(
6004                Cx::for_testing(),
6005                825,
6006                crate::InboundRequestTransport::Memory,
6007            );
6008            let after = server
6009                .dispatch_stateless(
6010                    &after_inbound,
6011                    &JsonRpcRequest::new(
6012                        "prompts/list",
6013                        Some(serde_json::json!({
6014                            "_meta": {
6015                                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6016                                "io.modelcontextprotocol/clientCapabilities": {},
6017                            },
6018                        })),
6019                        825_i64,
6020                    ),
6021                )
6022                .expect("the proxied final prompt catalog remains public after rejection")
6023                .result
6024                .expect("the proxied final prompt catalog still has a result payload");
6025            assert_eq!(
6026                before, after,
6027                "the rejected request cannot mutate downstream catalog state"
6028            );
6029            assert!(
6030                calls
6031                    .lock()
6032                    .expect("completion proxy call log is not poisoned")
6033                    .is_empty(),
6034                "the unsupported upstream is never invoked"
6035            );
6036        }
6037
6038        #[test]
6039        fn public_proxy_completion_duplicate_local_target_keeps_no_upstream_mapping() {
6040            for duplicate_behavior in [DuplicateBehavior::Warn, DuplicateBehavior::Ignore] {
6041                let calls = Arc::new(Mutex::new(Vec::new()));
6042                let server = ServerBuilder::new("completion-proxy", "1.0")
6043                    .prompt(LocalFinalCompletionPrompt)
6044                    .on_duplicate(duplicate_behavior)
6045                    .proxy_typed(
6046                        bound_proxy_client(
6047                            CompletionProxyBackend {
6048                                supported: true,
6049                                result: final_completion_proxy_result(),
6050                                calls: Arc::clone(&calls),
6051                            },
6052                            ProtocolEra::Modern2026,
6053                        ),
6054                        final_completion_proxy_catalog(),
6055                    )
6056                    .expect("retaining the local final prompt is a successful duplicate admission")
6057                    .build();
6058
6059                let discovery = serde_json::to_value(
6060                    server
6061                        .server_discovery()
6062                        .expect("the retained local prompt server remains discoverable"),
6063                )
6064                .expect("discovery serializes");
6065                assert!(
6066                    discovery["capabilities"].get("completions").is_none(),
6067                    "{duplicate_behavior:?} must not advertise an upstream provider for a retained local target"
6068                );
6069
6070                let before_inbound = crate::InboundRequestContext::new(
6071                    Cx::for_testing(),
6072                    826,
6073                    crate::InboundRequestTransport::Memory,
6074                );
6075                let before = server
6076                    .dispatch_stateless(
6077                        &before_inbound,
6078                        &JsonRpcRequest::new(
6079                            "prompts/list",
6080                            Some(serde_json::json!({
6081                                "_meta": {
6082                                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6083                                    "io.modelcontextprotocol/clientCapabilities": {},
6084                                },
6085                            })),
6086                            826_i64,
6087                        ),
6088                    )
6089                    .expect("the retained local prompt is visible through the public final list")
6090                    .result
6091                    .expect("the retained local prompt list has a result payload");
6092                assert_eq!(
6093                    before["prompts"][0]["title"], "Local Final Deploy",
6094                    "{duplicate_behavior:?} retains the pre-existing local prompt target"
6095                );
6096
6097                let rejected_inbound = crate::InboundRequestContext::new(
6098                    Cx::for_testing(),
6099                    827,
6100                    crate::InboundRequestTransport::Memory,
6101                );
6102                let rejected = server
6103                    .dispatch_stateless(&rejected_inbound, &final_completion_request(827))
6104                    .expect("the absent completion mapping returns a JSON-RPC error");
6105                assert_eq!(
6106                    rejected.error.and_then(|error| error.code.as_i32()),
6107                    Some(-32601),
6108                    "{duplicate_behavior:?} rejects before invocation because no upstream mapping was installed"
6109                );
6110                assert!(rejected.result.is_none());
6111
6112                let after_inbound = crate::InboundRequestContext::new(
6113                    Cx::for_testing(),
6114                    828,
6115                    crate::InboundRequestTransport::Memory,
6116                );
6117                let after = server
6118                    .dispatch_stateless(
6119                        &after_inbound,
6120                        &JsonRpcRequest::new(
6121                            "prompts/list",
6122                            Some(serde_json::json!({
6123                                "_meta": {
6124                                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6125                                    "io.modelcontextprotocol/clientCapabilities": {},
6126                                },
6127                            })),
6128                            828_i64,
6129                        ),
6130                    )
6131                    .expect("the rejected request leaves the retained local target public")
6132                    .result
6133                    .expect("the retained local prompt list still has a result payload");
6134                assert_eq!(
6135                    before, after,
6136                    "{duplicate_behavior:?} completion rejection cannot mutate the retained local target"
6137                );
6138                assert!(
6139                    calls
6140                        .lock()
6141                        .expect("completion proxy call log is not poisoned")
6142                        .is_empty(),
6143                    "{duplicate_behavior:?} must not invoke the upstream completion backend"
6144                );
6145            }
6146        }
6147
6148        #[test]
6149        fn public_proxy_completion_replace_installs_upstream_mapping_for_replaced_target() {
6150            let calls = Arc::new(Mutex::new(Vec::new()));
6151            let server = ServerBuilder::new("completion-proxy", "1.0")
6152                .prompt(LocalFinalCompletionPrompt)
6153                .on_duplicate(DuplicateBehavior::Replace)
6154                .proxy_typed(
6155                    bound_proxy_client(
6156                        CompletionProxyBackend {
6157                            supported: true,
6158                            result: final_completion_proxy_result(),
6159                            calls: Arc::clone(&calls),
6160                        },
6161                        ProtocolEra::Modern2026,
6162                    ),
6163                    final_completion_proxy_catalog(),
6164                )
6165                .expect("Replace admits the exact upstream final prompt target")
6166                .build();
6167
6168            let discovery = serde_json::to_value(
6169                server
6170                    .server_discovery()
6171                    .expect("the replacement proxy server remains discoverable"),
6172            )
6173            .expect("discovery serializes");
6174            assert_eq!(
6175                discovery["capabilities"]["completions"],
6176                serde_json::json!({}),
6177                "the real upstream mapping advertises final completion support"
6178            );
6179
6180            let list_inbound = crate::InboundRequestContext::new(
6181                Cx::for_testing(),
6182                829,
6183                crate::InboundRequestTransport::Memory,
6184            );
6185            let prompts = server
6186                .dispatch_stateless(
6187                    &list_inbound,
6188                    &JsonRpcRequest::new(
6189                        "prompts/list",
6190                        Some(serde_json::json!({
6191                            "_meta": {
6192                                "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6193                                "io.modelcontextprotocol/clientCapabilities": {},
6194                            },
6195                        })),
6196                        829_i64,
6197                    ),
6198                )
6199                .expect("the replaced prompt is public through the final list")
6200                .result
6201                .expect("the replaced prompt list has a result payload");
6202            assert_eq!(
6203                prompts["prompts"][0]["title"], "Upstream Final Deploy",
6204                "Replace exposes the admitted upstream prompt instead of the local target"
6205            );
6206
6207            let completion_inbound = crate::InboundRequestContext::new(
6208                Cx::for_testing(),
6209                830,
6210                crate::InboundRequestTransport::Memory,
6211            );
6212            let completion = server
6213                .dispatch_stateless(&completion_inbound, &final_completion_request(830))
6214                .expect("the replaced target forwards through the public final completion route")
6215                .result
6216                .expect("the replaced target completion has a result payload");
6217            assert_eq!(
6218                completion["completion"]["values"],
6219                serde_json::json!(["final-staging"])
6220            );
6221            assert_eq!(
6222                calls
6223                    .lock()
6224                    .expect("completion proxy call log is not poisoned")
6225                    .len(),
6226                1,
6227                "Replace installs exactly one upstream completion invocation path"
6228            );
6229        }
6230
6231        #[test]
6232        fn public_final_proxy_completion_replacement_evicts_proxy_targets_and_restores_local_providers()
6233         {
6234            let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6235            let template_calls = Arc::new(Mutex::new(Vec::new()));
6236            let evicted = ServerBuilder::new("completion-proxy", "1.0")
6237                .proxy_typed(
6238                    bound_proxy_client(
6239                        CompletionProxyBackend {
6240                            supported: true,
6241                            result: final_completion_proxy_result(),
6242                            calls: Arc::clone(&prompt_calls),
6243                        },
6244                        ProtocolEra::Modern2026,
6245                    ),
6246                    final_completion_proxy_catalog(),
6247                )
6248                .expect("the final prompt proxy installs")
6249                .proxy_typed(
6250                    bound_proxy_client(
6251                        CompletionProxyBackend {
6252                            supported: true,
6253                            result: final_completion_proxy_result(),
6254                            calls: Arc::clone(&template_calls),
6255                        },
6256                        ProtocolEra::Modern2026,
6257                    ),
6258                    final_completion_proxy_template_catalog(),
6259                )
6260                .expect("the final resource-template proxy installs")
6261                .on_duplicate(DuplicateBehavior::Replace)
6262                .prompt(LocalFinalCompletionPrompt)
6263                .resource_template(local_completion_template())
6264                .build();
6265
6266            let discovery = serde_json::to_value(
6267                evicted
6268                    .server_discovery()
6269                    .expect("the locally replaced catalog remains discoverable"),
6270            )
6271            .expect("discovery serializes");
6272            assert!(
6273                discovery["capabilities"].get("completions").is_none(),
6274                "replacing every proxied final target removes proxy completion advertisement"
6275            );
6276            for (id, request) in [
6277                (831, final_completion_request(831)),
6278                (832, final_resource_template_completion_request(832)),
6279            ] {
6280                let inbound = crate::InboundRequestContext::new(
6281                    Cx::for_testing(),
6282                    id,
6283                    crate::InboundRequestTransport::Memory,
6284                );
6285                let rejected = evicted
6286                    .dispatch_stateless(&inbound, &request)
6287                    .expect("the evicted completion mapping returns a JSON-RPC error");
6288                assert_eq!(
6289                    rejected.error.and_then(|error| error.code.as_i32()),
6290                    Some(-32601),
6291                    "the local replacement has no retained upstream completion provider"
6292                );
6293                assert!(rejected.result.is_none());
6294            }
6295            assert!(
6296                prompt_calls
6297                    .lock()
6298                    .expect("prompt completion proxy call log is not poisoned")
6299                    .is_empty()
6300            );
6301            assert!(
6302                template_calls
6303                    .lock()
6304                    .expect("template completion proxy call log is not poisoned")
6305                    .is_empty()
6306            );
6307
6308            let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6309            let template_calls = Arc::new(Mutex::new(Vec::new()));
6310            let restored = ServerBuilder::new("completion-proxy", "1.0")
6311                .proxy_typed(
6312                    bound_proxy_client(
6313                        CompletionProxyBackend {
6314                            supported: true,
6315                            result: final_completion_proxy_result(),
6316                            calls: Arc::clone(&prompt_calls),
6317                        },
6318                        ProtocolEra::Modern2026,
6319                    ),
6320                    final_completion_proxy_catalog(),
6321                )
6322                .expect("the final prompt proxy installs")
6323                .proxy_typed(
6324                    bound_proxy_client(
6325                        CompletionProxyBackend {
6326                            supported: true,
6327                            result: final_completion_proxy_result(),
6328                            calls: Arc::clone(&template_calls),
6329                        },
6330                        ProtocolEra::Modern2026,
6331                    ),
6332                    final_completion_proxy_template_catalog(),
6333                )
6334                .expect("the final resource-template proxy installs")
6335                .on_duplicate(DuplicateBehavior::Replace)
6336                .prompt(LocalFinalCompletionPrompt)
6337                .resource_template(local_completion_template())
6338                .prompt_completion_handler("final-deploy", TestCompletion)
6339                .resource_template_completion_handler("completion://{environment}", TestCompletion)
6340                .build();
6341            let discovery = serde_json::to_value(
6342                restored
6343                    .server_discovery()
6344                    .expect("the local completion providers make final discovery available"),
6345            )
6346            .expect("discovery serializes");
6347            assert_eq!(
6348                discovery["capabilities"]["completions"],
6349                serde_json::json!({}),
6350                "only the restored local final providers advertise completion support"
6351            );
6352            for (id, request) in [
6353                (833, final_completion_request(833)),
6354                (834, final_resource_template_completion_request(834)),
6355            ] {
6356                let inbound = crate::InboundRequestContext::new(
6357                    Cx::for_testing(),
6358                    id,
6359                    crate::InboundRequestTransport::Memory,
6360                );
6361                let response = restored
6362                    .dispatch_stateless(&inbound, &request)
6363                    .expect("the local replacement provider handles the public final request")
6364                    .result
6365                    .expect("the local replacement provider returns a result payload");
6366                assert_eq!(
6367                    response["completion"]["values"],
6368                    serde_json::json!(["staging"])
6369                );
6370            }
6371            assert!(
6372                prompt_calls
6373                    .lock()
6374                    .expect("prompt completion proxy call log is not poisoned")
6375                    .is_empty(),
6376                "local prompt providers must not invoke the displaced proxy"
6377            );
6378            assert!(
6379                template_calls
6380                    .lock()
6381                    .expect("template completion proxy call log is not poisoned")
6382                    .is_empty(),
6383                "local template providers must not invoke the displaced proxy"
6384            );
6385        }
6386
6387        #[test]
6388        fn public_legacy_proxy_completion_replacement_evicts_proxy_targets_and_restores_local_provider()
6389         {
6390            let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6391            let template_calls = Arc::new(Mutex::new(Vec::new()));
6392            let evicted = ServerBuilder::new("completion-proxy", "1.0")
6393                .proxy_typed(
6394                    bound_proxy_client(
6395                        CompletionProxyBackend {
6396                            supported: true,
6397                            result: legacy_completion_proxy_result(),
6398                            calls: Arc::clone(&prompt_calls),
6399                        },
6400                        ProtocolEra::Legacy2024,
6401                    ),
6402                    legacy_completion_proxy_catalog(),
6403                )
6404                .expect("the legacy prompt proxy installs")
6405                .proxy_typed(
6406                    bound_proxy_client(
6407                        CompletionProxyBackend {
6408                            supported: true,
6409                            result: legacy_completion_proxy_result(),
6410                            calls: Arc::clone(&template_calls),
6411                        },
6412                        ProtocolEra::Legacy2024,
6413                    ),
6414                    legacy_completion_proxy_template_catalog(),
6415                )
6416                .expect("the legacy resource-template proxy installs")
6417                .on_duplicate(DuplicateBehavior::Replace)
6418                .legacy_prompt(LocalFinalCompletionPrompt)
6419                .legacy_resource_template(local_completion_template())
6420                .build();
6421            let mut legacy_session = initialized_legacy_proxy_session(&evicted);
6422            let notification_sender: crate::NotificationSender = Arc::new(|_| {});
6423            let request_sender = crate::RequestSender::new(
6424                Arc::new(crate::PendingRequests::new()),
6425                Arc::new(|message| {
6426                    Err(format!("unexpected outbound message in test: {message:?}"))
6427                }),
6428            );
6429            for request in [
6430                legacy_completion_request(
6431                    835,
6432                    serde_json::json!({"type": "ref/prompt", "name": "legacy-deploy"}),
6433                ),
6434                legacy_completion_request(
6435                    836,
6436                    serde_json::json!({"type": "ref/resource", "uri": "completion://{environment}"}),
6437                ),
6438            ] {
6439                let rejected = evicted
6440                    .dispatch_request(
6441                        &Cx::for_testing(),
6442                        &mut legacy_session,
6443                        request,
6444                        &notification_sender,
6445                        &request_sender,
6446                    )
6447                    .expect("the public replaced legacy target returns a JSON-RPC error");
6448                assert_eq!(
6449                    rejected.error.and_then(|error| error.code.as_i32()),
6450                    Some(-32601),
6451                    "the local replacement has no retained upstream completion provider"
6452                );
6453                assert!(rejected.result.is_none());
6454            }
6455            assert!(
6456                prompt_calls
6457                    .lock()
6458                    .expect("prompt completion proxy call log is not poisoned")
6459                    .is_empty()
6460            );
6461            assert!(
6462                template_calls
6463                    .lock()
6464                    .expect("template completion proxy call log is not poisoned")
6465                    .is_empty()
6466            );
6467
6468            let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6469            let template_calls = Arc::new(Mutex::new(Vec::new()));
6470            let restored = ServerBuilder::new("completion-proxy", "1.0")
6471                .proxy_typed(
6472                    bound_proxy_client(
6473                        CompletionProxyBackend {
6474                            supported: true,
6475                            result: legacy_completion_proxy_result(),
6476                            calls: Arc::clone(&prompt_calls),
6477                        },
6478                        ProtocolEra::Legacy2024,
6479                    ),
6480                    legacy_completion_proxy_catalog(),
6481                )
6482                .expect("the legacy prompt proxy installs")
6483                .proxy_typed(
6484                    bound_proxy_client(
6485                        CompletionProxyBackend {
6486                            supported: true,
6487                            result: legacy_completion_proxy_result(),
6488                            calls: Arc::clone(&template_calls),
6489                        },
6490                        ProtocolEra::Legacy2024,
6491                    ),
6492                    legacy_completion_proxy_template_catalog(),
6493                )
6494                .expect("the legacy resource-template proxy installs")
6495                .on_duplicate(DuplicateBehavior::Replace)
6496                .legacy_prompt(LocalFinalCompletionPrompt)
6497                .legacy_resource_template(local_completion_template())
6498                .legacy_completion_handler(TestCompletion)
6499                .build();
6500            let mut legacy_session = initialized_legacy_proxy_session(&restored);
6501            let notification_sender: crate::NotificationSender = Arc::new(|_| {});
6502            let request_sender = crate::RequestSender::new(
6503                Arc::new(crate::PendingRequests::new()),
6504                Arc::new(|message| {
6505                    Err(format!("unexpected outbound message in test: {message:?}"))
6506                }),
6507            );
6508            for request in [
6509                legacy_completion_request(
6510                    837,
6511                    serde_json::json!({"type": "ref/prompt", "name": "legacy-deploy"}),
6512                ),
6513                legacy_completion_request(
6514                    838,
6515                    serde_json::json!({"type": "ref/resource", "uri": "completion://{environment}"}),
6516                ),
6517            ] {
6518                let response = restored
6519                    .dispatch_request(
6520                        &Cx::for_testing(),
6521                        &mut legacy_session,
6522                        request,
6523                        &notification_sender,
6524                        &request_sender,
6525                    )
6526                    .expect("the public local legacy provider returns a response")
6527                    .result
6528                    .expect("the local legacy provider returns a result payload");
6529                assert_eq!(
6530                    response["completion"]["values"],
6531                    serde_json::json!(["staging"])
6532                );
6533            }
6534            assert!(
6535                prompt_calls
6536                    .lock()
6537                    .expect("prompt completion proxy call log is not poisoned")
6538                    .is_empty()
6539            );
6540            assert!(
6541                template_calls
6542                    .lock()
6543                    .expect("template completion proxy call log is not poisoned")
6544                    .is_empty()
6545            );
6546        }
6547
6548        #[test]
6549        fn builder_proxy_rejects_final_tools_with_one_legacy_resource_vector() {
6550            let mut catalog = final_proxy_catalog();
6551            catalog.resources.push(Resource {
6552                uri: "file:///must-not-project".to_owned(),
6553                name: "must-not-project".to_owned(),
6554                description: None,
6555                mime_type: None,
6556                icon: None,
6557                version: None,
6558                tags: Vec::new(),
6559            });
6560
6561            assert_rejected_proxy_catalog_returns_an_error(catalog);
6562        }
6563
6564        #[test]
6565        fn public_legacy_and_stateless_ping_answer_empty_object_without_entering_the_final_request_union()
6566         {
6567            let server = ServerBuilder::new("srv", "1.0").build();
6568            let mut legacy_session =
6569                crate::Session::new(server.info().clone(), server.capabilities().clone());
6570            legacy_session.initialize(
6571                fastmcp_protocol::ClientInfo {
6572                    name: "exact-2024-ping-client".to_owned(),
6573                    version: "1.0".to_owned(),
6574                },
6575                fastmcp_protocol::ClientCapabilities::default(),
6576                "2024-11-05".to_owned(),
6577            );
6578            let state_before = legacy_session.state().len();
6579            let legacy_ping = JsonRpcRequest::new("ping", Some(serde_json::json!({})), 714_i64);
6580            let mut final_ping = legacy_ping.clone();
6581            final_ping
6582                .params
6583                .as_mut()
6584                .expect("the cloned ping request retains its object parameters")
6585                .as_object_mut()
6586                .expect("the cloned ping parameters remain an object")
6587                .insert(
6588                    "_meta".to_owned(),
6589                    serde_json::json!({
6590                        "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6591                        "io.modelcontextprotocol/clientCapabilities": {},
6592                    }),
6593                );
6594            let mut final_without_metadata = final_ping
6595                .params
6596                .clone()
6597                .expect("the final ping parameters are retained for the negative-control check");
6598            final_without_metadata
6599                .as_object_mut()
6600                .expect("the final ping parameters remain an object")
6601                .remove("_meta");
6602            assert_eq!(final_ping.method, legacy_ping.method);
6603            assert_eq!(final_ping.id, legacy_ping.id);
6604            assert_eq!(Some(final_without_metadata), legacy_ping.params);
6605
6606            let notification_sender: crate::NotificationSender = Arc::new(|_| {});
6607            let request_sender = crate::RequestSender::new(
6608                Arc::new(crate::PendingRequests::new()),
6609                Arc::new(|message| {
6610                    Err(format!("unexpected outbound message in test: {message:?}"))
6611                }),
6612            );
6613            let legacy = server
6614                .dispatch_request(
6615                    &Cx::for_testing(),
6616                    &mut legacy_session,
6617                    legacy_ping,
6618                    &notification_sender,
6619                    &request_sender,
6620                )
6621                .expect("the exact-2024 public dispatch path responds to ping");
6622            assert_eq!(legacy.result, Some(serde_json::json!({})));
6623            assert!(legacy.error.is_none());
6624
6625            let inbound = crate::InboundRequestContext::new(
6626                Cx::for_testing(),
6627                714,
6628                crate::InboundRequestTransport::Memory,
6629            );
6630            let answered = server
6631                .dispatch_stateless(&inbound, &final_ping)
6632                .expect("stateless modern ping is a connection health-check");
6633            assert_eq!(answered.result, Some(serde_json::json!({})));
6634            assert!(answered.error.is_none());
6635            assert_eq!(
6636                legacy_session.state().len(),
6637                state_before,
6638                "ping cannot mutate session state"
6639            );
6640
6641            assert!(
6642                !fastmcp_protocol::methods::FINAL_2026_07_28_METHODS
6643                    .iter()
6644                    .any(|method| method.name == "ping"),
6645                "ping must remain outside the official 2026 client-request union"
6646            );
6647            let decode = fastmcp_protocol::CoreRequest::decode(
6648                ProtocolEra::Modern2026,
6649                "ping",
6650                Some(&serde_json::json!({})),
6651            )
6652            .expect_err("FinalCoreRequest must not grow a Ping variant");
6653            assert!(matches!(
6654                decode,
6655                fastmcp_protocol::CoreDispatchError::UnsupportedMethod {
6656                    era: ProtocolEra::Modern2026,
6657                    method,
6658                } if method == "ping"
6659            ));
6660        }
6661
6662        #[test]
6663        fn typed_proxy_registration_retains_final_resource_template_and_prompt_metadata() {
6664            let resource = serde_json::json!({
6665                "uri": "mcp://upstream/resource",
6666                "name": "upstream-resource",
6667                "size": 4096,
6668                "_meta": {"com.example/resource": {"retained": true}}
6669            });
6670            let template = serde_json::json!({
6671                "uriTemplate": "mcp://upstream/{name}",
6672                "name": "upstream-template",
6673                "_meta": {"com.example/template": {"retained": true}}
6674            });
6675            let prompt = serde_json::json!({
6676                "name": "upstream-prompt",
6677                "arguments": [{"name": "region", "title": "Region"}],
6678                "_meta": {"com.example/prompt": {"retained": true}}
6679            });
6680            let typed = ProxyTypedCatalog {
6681                tools: ProxyToolCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
6682                resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(vec![
6683                    serde_json::from_value(resource.clone())
6684                        .expect("the final resource fixture is valid"),
6685                ])),
6686                resource_templates: ProxyResourceTemplateCatalog::Final(ProxyFinalCatalog::new(
6687                    vec![
6688                        serde_json::from_value(template.clone())
6689                            .expect("the final resource-template fixture is valid"),
6690                    ],
6691                )),
6692                prompts: ProxyPromptCatalog::Final(ProxyFinalCatalog::new(vec![
6693                    serde_json::from_value(prompt.clone())
6694                        .expect("the final prompt fixture is valid"),
6695                ])),
6696            };
6697            let server = ServerBuilder::new("srv", "1.0")
6698                .proxy_typed(
6699                    bound_proxy_client(DuplicatePolicyProxyBackend, ProtocolEra::Modern2026),
6700                    typed,
6701                )
6702                .expect("coherent typed catalog registers")
6703                .build();
6704            for (method, member, expected, id) in [
6705                ("resources/list", "resources", resource, 711_i64),
6706                (
6707                    "resources/templates/list",
6708                    "resourceTemplates",
6709                    template,
6710                    712_i64,
6711                ),
6712                ("prompts/list", "prompts", prompt, 713_i64),
6713            ] {
6714                let inbound = crate::InboundRequestContext::new(
6715                    Cx::for_testing(),
6716                    u64::try_from(id).expect("test request IDs are non-negative"),
6717                    crate::InboundRequestTransport::Memory,
6718                );
6719                let response = server
6720                    .dispatch_stateless(
6721                        &inbound,
6722                        &JsonRpcRequest::new(
6723                            method,
6724                            Some(serde_json::json!({
6725                                "_meta": {
6726                                    "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6727                                    "io.modelcontextprotocol/clientCapabilities": {},
6728                                },
6729                            })),
6730                            id,
6731                        ),
6732                    )
6733                    .expect("final discovery dispatch succeeds");
6734                assert_eq!(
6735                    response.result.expect("final discovery has a result")[member][0],
6736                    expected,
6737                    "final proxy registration must retain exact {member} metadata"
6738                );
6739            }
6740
6741            assert!(server.resources().is_empty());
6742            assert!(server.resource_templates().is_empty());
6743            assert!(server.prompts().is_empty());
6744            let router = server.into_router();
6745            let state = fastmcp_core::SessionState::new();
6746            let request_ctx =
6747                fastmcp_core::McpContext::with_state(Cx::for_testing(), 714, state.clone());
6748            let legacy_resource = router
6749                .handle_resources_read(
6750                    &request_ctx,
6751                    &fastmcp_protocol::ReadResourceParams {
6752                        uri: "mcp://upstream/resource".to_owned(),
6753                        meta: None,
6754                    },
6755                    state.clone(),
6756                    None,
6757                    None,
6758                )
6759                .expect_err("typed-final resource registration is not legacy-visible");
6760            assert_eq!(
6761                legacy_resource.code,
6762                fastmcp_core::McpErrorCode::ResourceNotFound
6763            );
6764            let legacy_prompt = router
6765                .handle_prompts_get(
6766                    &request_ctx,
6767                    fastmcp_protocol::GetPromptParams {
6768                        name: "upstream-prompt".to_owned(),
6769                        arguments: None,
6770                        meta: None,
6771                    },
6772                    state,
6773                    None,
6774                    None,
6775                )
6776                .expect_err("typed-final prompt registration is not legacy-visible");
6777            assert_eq!(
6778                legacy_prompt.code,
6779                fastmcp_core::McpErrorCode::PromptNotFound
6780            );
6781        }
6782
6783        #[test]
6784        fn typed_proxy_registration_rejects_an_unbound_caller_asserted_final_catalog() {
6785            let error = match ServerBuilder::new("srv", "1.0").proxy_typed(
6786                ProxyClient::from_backend(DuplicatePolicyProxyBackend),
6787                final_completion_proxy_catalog(),
6788            ) {
6789                Ok(_) => {
6790                    panic!("a caller-supplied typed catalog cannot bind an unbound proxy route")
6791                }
6792                Err(error) => error,
6793            };
6794
6795            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6796            assert!(error.message.contains("cannot bind an unbound route"));
6797        }
6798
6799        #[test]
6800        fn typed_proxy_registration_rejects_one_mixed_era_component_vector() {
6801            let typed = ProxyTypedCatalog {
6802                tools: ProxyToolCatalog::Legacy(Vec::new()),
6803                resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
6804                resource_templates: ProxyResourceTemplateCatalog::Legacy(Vec::new()),
6805                prompts: ProxyPromptCatalog::Legacy(Vec::new()),
6806            };
6807            let error = match ServerBuilder::new("srv", "1.0").register_raw_typed_proxy_catalog(
6808                bound_proxy_client(DuplicatePolicyProxyBackend, ProtocolEra::Legacy2024),
6809                typed,
6810            ) {
6811                Err(error) => error,
6812                Ok(_) => {
6813                    panic!("changing only resources to final rejects a mixed-era proxy catalog")
6814                }
6815            };
6816            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6817        }
6818
6819        #[test]
6820        fn builder_proxy_rejects_the_same_final_catalog_for_a_legacy_binding() {
6821            let error = match ServerBuilder::new("srv", "1.0").proxy(
6822                final_catalog_proxy_client(ProtocolEra::Legacy2024),
6823                final_proxy_catalog(),
6824            ) {
6825                Ok(_) => panic!("the catalog era cannot contradict the immutable route binding"),
6826                Err(error) => error,
6827            };
6828
6829            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6830        }
6831
6832        #[test]
6833        fn builder_proxy_with_catalog() {
6834            use crate::proxy::ProxyClient;
6835
6836            struct DummyBackend;
6837            impl crate::proxy::ProxyBackend for DummyBackend {
6838                fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
6839                    Ok(vec![Tool {
6840                        name: "proxy-tool".to_owned(),
6841                        description: None,
6842                        input_schema: serde_json::json!({}),
6843                        output_schema: None,
6844                        icon: None,
6845                        version: None,
6846                        tags: Vec::new(),
6847                        annotations: None,
6848                    }])
6849                }
6850                fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
6851                    Ok(vec![])
6852                }
6853                fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
6854                    Ok(vec![])
6855                }
6856                fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
6857                    Ok(vec![])
6858                }
6859                fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
6860                    Ok(vec![])
6861                }
6862                fn call_tool_with_progress(
6863                    &mut self,
6864                    _: &str,
6865                    _: serde_json::Value,
6866                    _: crate::proxy::ProgressCallback<'_>,
6867                ) -> McpResult<Vec<Content>> {
6868                    Ok(vec![])
6869                }
6870                fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
6871                    Ok(vec![])
6872                }
6873                fn get_prompt(
6874                    &mut self,
6875                    _: &str,
6876                    _: std::collections::HashMap<String, String>,
6877                ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
6878                    Ok(vec![])
6879                }
6880            }
6881
6882            let client = ProxyClient::from_backend(DummyBackend);
6883            let catalog = client
6884                .catalog()
6885                .expect("backend discovery supplies the legacy era evidence");
6886
6887            let server = ServerBuilder::new("srv", "1.0")
6888                .proxy(client, catalog)
6889                .expect("the backend-observed catalog is admitted")
6890                .build();
6891            assert!(server.has_tools());
6892        }
6893
6894        #[test]
6895        fn builder_proxy_honors_duplicate_policy_for_every_component_kind() {
6896            for behavior in [
6897                DuplicateBehavior::Warn,
6898                DuplicateBehavior::Ignore,
6899                DuplicateBehavior::Error,
6900            ] {
6901                let (client, catalog) = discovered_duplicate_policy_proxy();
6902                let server = ServerBuilder::new("srv", "1.0")
6903                    .on_duplicate(behavior)
6904                    .tool(TestTool)
6905                    .resource(TestResource)
6906                    .prompt(TestPrompt)
6907                    .proxy(client, catalog)
6908                    .expect("the backend-observed catalog is admitted")
6909                    .build();
6910                let router = server.into_router();
6911
6912                assert_eq!(
6913                    router.tools()[0].description.as_deref(),
6914                    Some("a test tool")
6915                );
6916                assert_eq!(router.resources()[0].name, "test_res");
6917                assert_eq!(router.prompts()[0].description, None);
6918            }
6919
6920            let (client, catalog) = discovered_duplicate_policy_proxy();
6921            let replaced = ServerBuilder::new("srv", "1.0")
6922                .on_duplicate(DuplicateBehavior::Replace)
6923                .tool(TestTool)
6924                .resource(TestResource)
6925                .prompt(TestPrompt)
6926                .proxy(client, catalog)
6927                .expect("the backend-observed catalog is admitted")
6928                .build()
6929                .into_router();
6930
6931            assert_eq!(
6932                replaced.tools()[0].description.as_deref(),
6933                Some("proxied tool")
6934            );
6935            assert_eq!(replaced.resources()[0].name, "proxied resource");
6936            assert_eq!(
6937                replaced.prompts()[0].description.as_deref(),
6938                Some("proxied prompt")
6939            );
6940        }
6941
6942        #[cfg(feature = "tasks")]
6943        #[test]
6944        fn as_proxy_typed_installs_route_bound_final_tasks_relay() {
6945            let calls = Arc::new(Mutex::new(Vec::new()));
6946            let updates = Arc::new(Mutex::new(Vec::new()));
6947            let server = ServerBuilder::new("prefixed-proxy-tasks", "1.0")
6948                .as_proxy_typed(
6949                    "ext",
6950                    ordinary_proxy_tasks_client(calls, updates),
6951                    final_completion_proxy_catalog(),
6952                )
6953                .expect("as_proxy_typed admits a modern catalog with a Tasks-capable route")
6954                .build();
6955            assert!(
6956                server.final_task_runtime().is_none(),
6957                "as_proxy_typed must install the route-bound Tasks relay instead of the default in-memory store"
6958            );
6959            let discovery = serde_json::to_value(
6960                server
6961                    .server_discovery()
6962                    .expect("the prefixed Tasks proxy remains discoverable"),
6963            )
6964            .expect("prefixed Tasks proxy discovery serializes");
6965            assert_eq!(
6966                discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
6967                Some(&serde_json::json!({})),
6968                "as_proxy_typed must advertise the same Tasks extension as proxy_typed"
6969            );
6970        }
6971
6972        #[test]
6973        fn as_proxy_raw_propagates_duplicate_registration_errors() {
6974            let result = ServerBuilder::new("srv", "1.0")
6975                .on_duplicate(DuplicateBehavior::Error)
6976                .tool(TestTool)
6977                .as_proxy_raw_with_proxy_client(ProxyClient::from_backend(
6978                    DuplicatePolicyProxyBackend,
6979                ));
6980
6981            let error = match result {
6982                Ok(_) => panic!("raw proxy registration unexpectedly accepted a duplicate tool"),
6983                Err(error) => error,
6984            };
6985            assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6986            assert!(error.message.starts_with("Tool already exists"));
6987        }
6988    }
6989
6990    // ── DEFAULT_REQUEST_TIMEOUT_SECS constant ──────────────────────
6991
6992    #[test]
6993    fn default_request_timeout_constant() {
6994        assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30);
6995    }
6996
6997    // ── mask_error_details toggling ────────────────────────────────
6998
6999    #[test]
7000    fn builder_mask_error_details_toggle() {
7001        let builder = ServerBuilder::new("srv", "1.0")
7002            .mask_error_details(true)
7003            .mask_error_details(false);
7004        assert!(!builder.is_error_masking_enabled());
7005    }
7006
7007    // ── strict_input_validation toggling ────────────────────────────
7008
7009    #[test]
7010    fn builder_strict_validation_toggle() {
7011        let builder = ServerBuilder::new("srv", "1.0")
7012            .strict_input_validation(true)
7013            .strict_input_validation(false);
7014        assert!(!builder.is_strict_input_validation_enabled());
7015    }
7016
7017    #[cfg(feature = "tasks")]
7018    mod task_manager_tests {
7019        use super::*;
7020
7021        // ── Task manager ──────────────────────────────────────────────────
7022
7023        #[test]
7024        fn default_build_installs_in_memory_official_tasks() {
7025            let server = ServerBuilder::new("srv", "1.0").build();
7026            assert!(
7027                server.final_task_runtime().is_some(),
7028                "default build must install official in-memory Tasks"
7029            );
7030        }
7031
7032        #[test]
7033        fn builder_with_task_manager_retains_manager_without_advertising_capability() {
7034            use crate::tasks::TaskManager;
7035            let tm = TaskManager::new().into_shared();
7036            let server = ServerBuilder::new("srv", "1.0")
7037                .with_task_manager(tm)
7038                .build();
7039            assert!(server.task_manager().is_some());
7040            assert!(server.capabilities().tasks.is_none());
7041            assert!(
7042                server.final_task_runtime().is_none(),
7043                "quarantined task manager must not receive official Tasks"
7044            );
7045        }
7046
7047        #[test]
7048        fn builder_with_notifying_task_manager_keeps_capability_quarantined() {
7049            use crate::tasks::TaskManager;
7050            let tm = TaskManager::with_list_changed_notifications().into_shared();
7051            let server = ServerBuilder::new("srv", "1.0")
7052                .with_task_manager(tm)
7053                .build();
7054            assert!(server.task_manager().is_some());
7055            assert!(server.capabilities().tasks.is_none());
7056        }
7057
7058        #[test]
7059        fn builder_with_non_notifying_task_manager_keeps_capability_quarantined() {
7060            use crate::tasks::TaskManager;
7061            let tm = TaskManager::new().into_shared();
7062            let server = ServerBuilder::new("srv", "1.0")
7063                .with_task_manager(tm)
7064                .build();
7065            assert!(server.task_manager().is_some());
7066            assert!(server.capabilities().tasks.is_none());
7067        }
7068    }
7069
7070    // ── Duplicate behavior for resources and prompts ─────────────────
7071
7072    struct DupResource(&'static str);
7073    impl crate::ResourceHandler for DupResource {
7074        fn definition(&self) -> Resource {
7075            Resource {
7076                uri: format!("file:///{}", self.0),
7077                name: self.0.to_string(),
7078                description: None,
7079                mime_type: None,
7080                icon: None,
7081                version: None,
7082                tags: vec![],
7083            }
7084        }
7085        fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
7086            Ok(vec![])
7087        }
7088    }
7089
7090    struct DupPrompt(&'static str);
7091    impl crate::PromptHandler for DupPrompt {
7092        fn definition(&self) -> Prompt {
7093            Prompt {
7094                name: self.0.to_string(),
7095                description: None,
7096                arguments: vec![],
7097                icon: None,
7098                version: None,
7099                tags: vec![],
7100            }
7101        }
7102        fn get(
7103            &self,
7104            _ctx: &McpContext,
7105            _args: std::collections::HashMap<String, String>,
7106        ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
7107            Ok(vec![])
7108        }
7109    }
7110
7111    #[test]
7112    fn builder_on_duplicate_error_resource_logs_but_continues() {
7113        let server = ServerBuilder::new("srv", "1.0")
7114            .on_duplicate(DuplicateBehavior::Error)
7115            .resource(DupResource("dup"))
7116            .resource(DupResource("dup"))
7117            .build();
7118        assert!(server.has_resources());
7119    }
7120
7121    #[test]
7122    fn builder_on_duplicate_error_prompt_logs_but_continues() {
7123        let server = ServerBuilder::new("srv", "1.0")
7124            .on_duplicate(DuplicateBehavior::Error)
7125            .prompt(DupPrompt("dup"))
7126            .prompt(DupPrompt("dup"))
7127            .build();
7128        assert!(server.has_prompts());
7129    }
7130
7131    #[cfg(feature = "proxy")]
7132    #[test]
7133    fn prefixed_as_proxy_skips_only_duplicate_registration_errors() {
7134        let duplicate =
7135            fastmcp_core::McpError::invalid_request("Tool already exists; component_key=tool_key");
7136        assert!(
7137            ServerBuilder::absorb_prefixed_proxy_duplicate(duplicate).is_ok(),
7138            "a colliding catalog member may skip under DuplicateBehavior::Error"
7139        );
7140        let panicked =
7141            fastmcp_core::McpError::internal_error("tool metadata hook panicked during admission");
7142        assert!(
7143            ServerBuilder::absorb_prefixed_proxy_duplicate(panicked).is_err(),
7144            "a non-duplicate admission failure must fail the proxy / as_proxy_typed install"
7145        );
7146        let apps = fastmcp_core::McpError::invalid_request(
7147            "MCP Apps tool metadata requires ServerBuilder::mcp_apps_tool",
7148        );
7149        assert!(
7150            ServerBuilder::absorb_prefixed_proxy_duplicate(apps).is_err(),
7151            "InvalidRequest that is not a duplicate must not be skipped"
7152        );
7153    }
7154
7155    // ── Proxy with resources and prompts ─────────────────────────────
7156
7157    #[cfg(feature = "proxy")]
7158    #[test]
7159    fn builder_proxy_with_resources_and_prompts() {
7160        use crate::proxy::ProxyClient;
7161
7162        struct DummyBackend2;
7163        impl crate::proxy::ProxyBackend for DummyBackend2 {
7164            fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
7165                Ok(vec![])
7166            }
7167            fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
7168                Ok(vec![Resource {
7169                    uri: "file:///proxy-res".to_owned(),
7170                    name: "proxy-res".to_owned(),
7171                    description: None,
7172                    mime_type: None,
7173                    icon: None,
7174                    version: None,
7175                    tags: Vec::new(),
7176                }])
7177            }
7178            fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
7179                Ok(vec![ResourceTemplate {
7180                    uri_template: "db://{table}".to_owned(),
7181                    name: "db".to_owned(),
7182                    description: None,
7183                    mime_type: None,
7184                    icon: None,
7185                    version: None,
7186                    tags: Vec::new(),
7187                }])
7188            }
7189            fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
7190                Ok(vec![Prompt {
7191                    name: "proxy-prompt".to_owned(),
7192                    description: None,
7193                    arguments: Vec::new(),
7194                    icon: None,
7195                    version: None,
7196                    tags: Vec::new(),
7197                }])
7198            }
7199            fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
7200                Ok(vec![])
7201            }
7202            fn call_tool_with_progress(
7203                &mut self,
7204                _: &str,
7205                _: serde_json::Value,
7206                _: crate::proxy::ProgressCallback<'_>,
7207            ) -> McpResult<Vec<Content>> {
7208                Ok(vec![])
7209            }
7210            fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
7211                Ok(vec![])
7212            }
7213            fn get_prompt(
7214                &mut self,
7215                _: &str,
7216                _: std::collections::HashMap<String, String>,
7217            ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
7218                Ok(vec![])
7219            }
7220        }
7221
7222        let client = ProxyClient::from_backend(DummyBackend2);
7223        let catalog = client
7224            .catalog()
7225            .expect("backend discovery supplies the legacy era evidence");
7226
7227        let server = ServerBuilder::new("srv", "1.0")
7228            .proxy(client, catalog)
7229            .expect("the backend-observed catalog is admitted")
7230            .build();
7231        assert!(server.has_resources());
7232        assert!(server.has_prompts());
7233        assert!(!server.has_tools());
7234    }
7235
7236    // ── Build propagates strict validation to router ─────────────────
7237
7238    #[test]
7239    fn build_propagates_strict_validation_to_router() {
7240        let server = ServerBuilder::new("srv", "1.0")
7241            .strict_input_validation(true)
7242            .build();
7243        let router = server.into_router();
7244        assert!(router.strict_input_validation());
7245    }
7246
7247    #[test]
7248    fn build_propagates_strict_validation_false_to_router() {
7249        let server = ServerBuilder::new("srv", "1.0")
7250            .strict_input_validation(false)
7251            .build();
7252        let router = server.into_router();
7253        assert!(!router.strict_input_validation());
7254    }
7255
7256    // ── log_level_filter with Off ────────────────────────────────────
7257
7258    #[test]
7259    fn builder_log_level_filter_off() {
7260        let builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Off);
7261        assert_eq!(builder.logging.level, LevelFilter::Off);
7262        assert_eq!(builder.console_config.log_level, LevelFilter::Off);
7263    }
7264
7265    // ── mount does not update capabilities when nothing mounted ──────
7266
7267    #[test]
7268    fn builder_mount_no_op_leaves_capabilities_unchanged() {
7269        let source = ServerBuilder::new("sub", "1.0").build();
7270        let main = ServerBuilder::new("main", "1.0")
7271            .mount(source, Some("ns"))
7272            .build();
7273        assert!(!main.has_tools());
7274        assert!(!main.has_resources());
7275        assert!(!main.has_prompts());
7276    }
7277}