1use 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
58const DEFAULT_REQUEST_TIMEOUT_SECS: u64 = 30;
60
61const FASTMCP_PROTOCOL_POLICY_ENV: &str = "FASTMCP_PROTOCOL_POLICY";
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ServerLaunchPolicyError {
68 NonUnicode,
70 InvalidValue,
72 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
140pub struct ServerBuilder {
142 info: ServerInfo,
143 capabilities: ServerCapabilities,
144 router: Router,
145 instructions: Option<String>,
146 title: Option<String>,
148 description: Option<String>,
150 website_url: Option<String>,
152 icons: Vec<fastmcp_protocol::common_types::RawIcon>,
154 request_timeout_secs: u64,
156 stats_enabled: bool,
158 mask_error_details: bool,
160 logging: LoggingConfig,
162 console_config: ConsoleConfig,
164 lifespan: LifespanHooks,
166 auth_provider: Option<Arc<dyn AuthProvider>>,
168 middleware: Vec<Box<dyn crate::Middleware>>,
170 #[cfg(all(test, feature = "tasks"))]
173 task_manager: Option<SharedTaskManager>,
174 on_duplicate: DuplicateBehavior,
176 strict_input_validation: bool,
178 max_bidirectional_requests_per_connection: usize,
180 protocol_policy: ProtocolPolicy,
182 launch_protocol_policy: Option<ProtocolPolicy>,
185 http_config: HttpServerConfig,
187 oauth_http_routes: Option<OAuthHttpRoutes>,
189 extension_runtime: Option<ServerExtensionRuntime>,
191 #[cfg(feature = "tasks")]
193 final_task_runtime: Option<FinalTaskRuntime>,
194 #[cfg(all(feature = "proxy", feature = "tasks"))]
197 final_task_relay: Option<Arc<ProxyFinalTaskRelay>>,
198}
199
200impl ServerBuilder {
201 #[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 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 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, 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 #[must_use]
340 pub fn on_duplicate(mut self, behavior: DuplicateBehavior) -> Self {
341 self.on_duplicate = behavior;
342 self
343 }
344
345 #[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 #[must_use]
359 pub fn without_stats(mut self) -> Self {
360 self.stats_enabled = false;
361 self
362 }
363
364 #[must_use]
372 pub fn request_timeout(mut self, secs: u64) -> Self {
373 self.request_timeout_secs = secs;
374 self
375 }
376
377 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 #[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 #[must_use]
422 pub fn mask_error_details(mut self, enabled: bool) -> Self {
423 self.mask_error_details = enabled;
424 self
425 }
426
427 #[must_use]
445 pub fn auto_mask_errors(mut self) -> Self {
446 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 _ => {} }
459 }
460
461 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 self.mask_error_details = cfg!(not(debug_assertions));
471 self
472 }
473
474 #[must_use]
476 pub fn is_error_masking_enabled(&self) -> bool {
477 self.mask_error_details
478 }
479
480 #[must_use]
496 pub fn strict_input_validation(mut self, enabled: bool) -> Self {
497 self.strict_input_validation = enabled;
498 self
499 }
500
501 #[must_use]
503 pub fn is_strict_input_validation_enabled(&self) -> bool {
504 self.strict_input_validation
505 }
506
507 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
754 pub fn http_config(mut self, config: HttpServerConfig) -> Self {
755 self.http_config = config;
756 self
757 }
758
759 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[cfg(all(feature = "proxy", feature = "tasks"))]
1116 fn install_proxy_final_tasks_relay(
1117 &mut self,
1118 task_relay: Option<Arc<ProxyFinalTaskRelay>>,
1119 ) -> McpResult<()> {
1120 let Some(task_relay) = task_relay else {
1121 return Ok(());
1122 };
1123 if self.final_task_runtime.is_some() || self.final_task_relay.is_some() {
1124 return Err(fastmcp_core::McpError::invalid_request(
1125 "a server may install only one local or route-bound final Tasks service",
1126 ));
1127 }
1128 if let Some(extension_runtime) = self.extension_runtime.as_mut() {
1129 extension_runtime
1130 .install_proxy_final_tasks(Arc::clone(&task_relay))
1131 .map_err(|error| fastmcp_core::McpError::invalid_request(error.to_string()))?;
1132 }
1133 self.router
1134 .set_final_task_relay(Some(Arc::clone(&task_relay)));
1135 self.final_task_relay = Some(task_relay);
1136 Ok(())
1137 }
1138
1139 #[cfg(feature = "proxy")]
1151 pub fn proxy(mut self, client: ProxyClient, catalog: ProxyCatalog) -> McpResult<Self> {
1152 client.admit_catalog(&catalog)?;
1153 let catalog_era = catalog.era()?;
1154 let completion_supported = match client.supports_completion() {
1155 Ok(supported) => supported,
1156 Err(error) => {
1157 log::error!(
1158 target: "fastmcp_rust::builder",
1159 "Failed to determine proxied completion support; code={:?}",
1160 error.code
1161 );
1162 false
1163 }
1164 };
1165 let has_tools = !catalog.tools.is_empty() || !catalog.final_tools.is_empty();
1170 let has_resources = !catalog.resources.is_empty()
1171 || !catalog.resource_templates.is_empty()
1172 || !catalog.final_resources.is_empty()
1173 || !catalog.final_resource_templates.is_empty();
1174 let has_prompts = !catalog.prompts.is_empty() || !catalog.final_prompts.is_empty();
1175 #[cfg(feature = "tasks")]
1176 let task_relay = if catalog_era == ProtocolEra::Modern2026 {
1177 client.final_tasks_relay()?
1178 } else {
1179 None
1180 };
1181 #[cfg(feature = "tasks")]
1182 self.install_proxy_final_tasks_relay(task_relay.clone())?;
1183 #[cfg(not(feature = "tasks"))]
1184 let final_handlers = catalog.final_tool_handlers(client.clone())?;
1185 #[cfg(feature = "tasks")]
1186 let final_handlers = catalog
1187 .final_tools
1188 .iter()
1189 .cloned()
1190 .map(|tool| match task_relay.as_ref() {
1191 Some(task_relay) => ProxyToolHandler::from_final_with_task_relay(
1192 tool,
1193 client.clone(),
1194 Arc::clone(task_relay),
1195 ),
1196 None => ProxyToolHandler::from_final(tool, client.clone()),
1197 })
1198 .collect::<McpResult<Vec<_>>>()?;
1199
1200 for tool in catalog.tools {
1204 if let Err(error) = self.router.add_legacy_tool_with_behavior(
1205 ProxyToolHandler::new(tool, client.clone()),
1206 self.on_duplicate,
1207 ) {
1208 log::error!(
1209 target: "fastmcp_rust::builder",
1210 "Failed to register proxied tool; code={:?}",
1211 error.code
1212 );
1213 }
1214 }
1215
1216 for handler in final_handlers {
1217 match self
1218 .router
1219 .add_final_tool_with_behavior(handler, self.on_duplicate)
1220 {
1221 Ok(()) => {}
1222 Err(error) => {
1223 log::error!(
1224 target: "fastmcp_rust::builder",
1225 "Failed to register exact-final proxied tool; code={:?}",
1226 error.code
1227 );
1228 }
1229 }
1230 }
1231
1232 for resource in catalog.resources {
1233 if let Err(error) = self.router.add_legacy_resource_with_behavior(
1234 ProxyResourceHandler::new(resource, client.clone()),
1235 self.on_duplicate,
1236 ) {
1237 log::error!(
1238 target: "fastmcp_rust::builder",
1239 "Failed to register proxied resource; code={:?}",
1240 error.code
1241 );
1242 }
1243 }
1244
1245 for template in catalog.resource_templates {
1246 let downstream_uri = template.uri_template.clone();
1247 let completion_target_admitted =
1248 self.proxy_resource_template_wins_admission(&downstream_uri);
1249 match self.router.add_legacy_resource_with_behavior(
1250 ProxyResourceHandler::from_template(template, client.clone()),
1251 self.on_duplicate,
1252 ) {
1253 Ok(()) if completion_target_admitted => {
1254 if completion_supported && catalog_era == ProtocolEra::Legacy2024 {
1255 self.router.add_legacy_resource_template_completion_handler(
1256 downstream_uri.clone(),
1257 ProxyCompletionHandler::for_resource_template(
1258 client.clone(),
1259 downstream_uri.clone(),
1260 downstream_uri,
1261 ),
1262 );
1263 self.advertise_completions();
1264 }
1265 }
1266 Ok(()) => {}
1267 Err(error) => {
1268 log::error!(
1269 target: "fastmcp_rust::builder",
1270 "Failed to register proxied resource template; code={:?}",
1271 error.code
1272 );
1273 }
1274 }
1275 }
1276
1277 for prompt in catalog.prompts {
1278 let downstream_name = prompt.name.clone();
1279 let completion_target_admitted = self.proxy_prompt_wins_admission(&downstream_name);
1280 match self.router.add_legacy_prompt_with_behavior(
1281 ProxyPromptHandler::new(prompt, client.clone()),
1282 self.on_duplicate,
1283 ) {
1284 Ok(()) if completion_target_admitted => {
1285 if completion_supported && catalog_era == ProtocolEra::Legacy2024 {
1286 self.router.add_legacy_prompt_completion_handler(
1287 downstream_name.clone(),
1288 ProxyCompletionHandler::for_prompt(
1289 client.clone(),
1290 downstream_name.clone(),
1291 downstream_name,
1292 ),
1293 );
1294 self.advertise_completions();
1295 }
1296 }
1297 Ok(()) => {}
1298 Err(error) => {
1299 log::error!(
1300 target: "fastmcp_rust::builder",
1301 "Failed to register proxied prompt; code={:?}",
1302 error.code
1303 );
1304 }
1305 }
1306 }
1307
1308 for resource in catalog.final_resources {
1309 if let Err(error) = self.router.add_final_resource_with_behavior(
1310 FinalProxyResourceHandler::new(resource, client.clone()),
1311 self.on_duplicate,
1312 ) {
1313 log::error!(
1314 target: "fastmcp_rust::builder",
1315 "Failed to register exact-final proxied resource; code={:?}",
1316 error.code
1317 );
1318 }
1319 }
1320
1321 for template in catalog.final_resource_templates {
1322 let downstream_uri = template.uri_template.clone();
1323 let completion_target_admitted =
1324 self.proxy_resource_template_wins_admission(&downstream_uri);
1325 match self.router.add_final_resource_with_behavior(
1326 FinalProxyResourceTemplateHandler::new(template, client.clone()),
1327 self.on_duplicate,
1328 ) {
1329 Ok(())
1330 if completion_target_admitted
1331 && completion_supported
1332 && catalog_era == ProtocolEra::Modern2026 =>
1333 {
1334 self.router.add_resource_template_completion_handler(
1335 downstream_uri.clone(),
1336 ProxyCompletionHandler::for_resource_template(
1337 client.clone(),
1338 downstream_uri.clone(),
1339 downstream_uri,
1340 ),
1341 );
1342 self.advertise_completions();
1343 }
1344 Ok(()) => {}
1345 Err(error) => {
1346 log::error!(
1347 target: "fastmcp_rust::builder",
1348 "Failed to register exact-final proxied resource template; code={:?}",
1349 error.code
1350 );
1351 }
1352 }
1353 }
1354
1355 for prompt in catalog.final_prompts {
1356 let downstream_name = prompt.name.clone();
1357 let completion_target_admitted = self.proxy_prompt_wins_admission(&downstream_name);
1358 match self.router.add_final_prompt_with_behavior(
1359 FinalProxyPromptHandler::new(prompt, client.clone()),
1360 self.on_duplicate,
1361 ) {
1362 Ok(())
1363 if completion_target_admitted
1364 && completion_supported
1365 && catalog_era == ProtocolEra::Modern2026 =>
1366 {
1367 self.router.add_prompt_completion_handler(
1368 downstream_name.clone(),
1369 ProxyCompletionHandler::for_prompt(
1370 client.clone(),
1371 downstream_name.clone(),
1372 downstream_name,
1373 ),
1374 );
1375 self.advertise_completions();
1376 }
1377 Ok(()) => {}
1378 Err(error) => {
1379 log::error!(
1380 target: "fastmcp_rust::builder",
1381 "Failed to register exact-final proxied prompt; code={:?}",
1382 error.code
1383 );
1384 }
1385 }
1386 }
1387
1388 if has_tools {
1389 self.advertise_legacy_tools_list_changed();
1390 }
1391 if has_resources {
1392 self.advertise_legacy_resource_subscriptions();
1393 }
1394 if has_prompts {
1395 self.advertise_legacy_prompts_list_changed();
1396 }
1397
1398 Ok(self)
1399 }
1400
1401 #[cfg(feature = "proxy")]
1432 pub fn as_proxy(
1433 self,
1434 prefix: &str,
1435 client: fastmcp_client::Client,
1436 ) -> Result<Self, fastmcp_core::McpError> {
1437 let proxy_client = ProxyClient::from_client(client)?;
1438 let catalog = proxy_client.catalog_typed()?;
1439 self.register_prefixed_typed_proxy_catalog(prefix, proxy_client, catalog)
1440 }
1441
1442 #[cfg(feature = "proxy")]
1458 pub fn as_proxy_typed(
1459 self,
1460 prefix: &str,
1461 proxy_client: ProxyClient,
1462 catalog: ProxyTypedCatalog,
1463 ) -> Result<Self, fastmcp_core::McpError> {
1464 self.register_prefixed_typed_proxy_catalog(prefix, proxy_client, catalog)
1465 }
1466
1467 #[cfg(feature = "proxy")]
1470 fn adopt_upstream_proxy_instructions(
1471 &mut self,
1472 proxy_client: &ProxyClient,
1473 ) -> Result<(), fastmcp_core::McpError> {
1474 if self
1475 .instructions
1476 .as_ref()
1477 .is_some_and(|instructions| !instructions.is_empty())
1478 {
1479 return Ok(());
1480 }
1481 if let Some(instructions) = proxy_client.upstream_instructions()?
1482 && !instructions.is_empty()
1483 {
1484 self.instructions = Some(instructions);
1485 }
1486 Ok(())
1487 }
1488
1489 #[cfg(feature = "proxy")]
1495 fn adopt_upstream_proxy_implementation(
1496 &mut self,
1497 proxy_client: &ProxyClient,
1498 ) -> Result<(), fastmcp_core::McpError> {
1499 let gateway_has_extras = self.title.as_ref().is_some_and(|title| !title.is_empty())
1500 || self
1501 .description
1502 .as_ref()
1503 .is_some_and(|description| !description.is_empty())
1504 || self
1505 .website_url
1506 .as_ref()
1507 .is_some_and(|website| !website.is_empty())
1508 || !self.icons.is_empty();
1509 if gateway_has_extras {
1510 return Ok(());
1511 }
1512 let Some(implementation) = proxy_client.upstream_implementation()? else {
1513 return Ok(());
1514 };
1515 if implementation.title.is_none()
1516 && implementation.description.is_none()
1517 && implementation.website_url.is_none()
1518 && implementation.icons.is_empty()
1519 {
1520 return Ok(());
1521 }
1522 self.title = implementation.title;
1523 self.description = implementation.description;
1524 self.website_url = implementation
1525 .website_url
1526 .map(|uri| uri.as_str().to_owned());
1527 self.icons = implementation.icons;
1528 Ok(())
1529 }
1530
1531 #[cfg(feature = "proxy")]
1532 fn register_prefixed_typed_proxy_catalog(
1533 mut self,
1534 prefix: &str,
1535 proxy_client: ProxyClient,
1536 catalog: ProxyTypedCatalog,
1537 ) -> Result<Self, fastmcp_core::McpError> {
1538 proxy_client.admit_typed_catalog(&catalog)?;
1539 self.adopt_upstream_proxy_instructions(&proxy_client)?;
1540 self.adopt_upstream_proxy_implementation(&proxy_client)?;
1541 let completion_supported = proxy_client.supports_completion()?;
1542 #[cfg(feature = "tasks")]
1543 let catalog_era = catalog.era()?;
1544 #[cfg(feature = "tasks")]
1545 let task_relay = if catalog_era == ProtocolEra::Modern2026 {
1546 proxy_client.final_tasks_relay()?
1547 } else {
1548 None
1549 };
1550 #[cfg(feature = "tasks")]
1551 self.install_proxy_final_tasks_relay(task_relay.clone())?;
1552 let (tool_count, resource_count, template_count, prompt_count) = match catalog {
1553 ProxyTypedCatalog {
1554 tools: ProxyToolCatalog::Legacy(tools),
1555 resources: ProxyResourceCatalog::Legacy(resources),
1556 resource_templates: ProxyResourceTemplateCatalog::Legacy(resource_templates),
1557 prompts: ProxyPromptCatalog::Legacy(prompts),
1558 } => {
1559 let counts = (
1560 tools.len(),
1561 resources.len(),
1562 resource_templates.len(),
1563 prompts.len(),
1564 );
1565 for tool in tools {
1569 if let Err(error) = self.router.add_legacy_tool_with_behavior(
1570 ProxyToolHandler::with_prefix(tool, prefix, proxy_client.clone()),
1571 self.on_duplicate,
1572 ) {
1573 log::error!(
1574 target: "fastmcp_rust::builder",
1575 "Failed to register prefixed proxied tool; code={:?}",
1576 error.code
1577 );
1578 }
1579 }
1580 for resource in resources {
1581 if let Err(error) = self.router.add_legacy_resource_with_behavior(
1582 ProxyResourceHandler::with_prefix(resource, prefix, proxy_client.clone()),
1583 self.on_duplicate,
1584 ) {
1585 log::error!(
1586 target: "fastmcp_rust::builder",
1587 "Failed to register prefixed proxied resource; code={:?}",
1588 error.code
1589 );
1590 }
1591 }
1592 for template in resource_templates {
1593 let upstream_uri = template.uri_template.clone();
1594 let downstream_uri = format!("{prefix}/{upstream_uri}");
1595 let completion_target_admitted =
1596 self.proxy_resource_template_wins_admission(&downstream_uri);
1597 match self.router.add_legacy_resource_with_behavior(
1598 ProxyResourceHandler::from_template_with_prefix(
1599 template,
1600 prefix,
1601 proxy_client.clone(),
1602 ),
1603 self.on_duplicate,
1604 ) {
1605 Ok(()) if completion_target_admitted && completion_supported => {
1606 self.router.add_legacy_resource_template_completion_handler(
1607 downstream_uri.clone(),
1608 ProxyCompletionHandler::for_resource_template(
1609 proxy_client.clone(),
1610 downstream_uri,
1611 upstream_uri,
1612 ),
1613 );
1614 self.advertise_completions();
1615 }
1616 Ok(()) => {}
1617 Err(error) => {
1618 log::error!(
1619 target: "fastmcp_rust::builder",
1620 "Failed to register prefixed proxied resource template; code={:?}",
1621 error.code
1622 );
1623 }
1624 }
1625 }
1626 for prompt in prompts {
1627 let upstream_name = prompt.name.clone();
1628 let downstream_name = format!("{prefix}/{upstream_name}");
1629 let completion_target_admitted =
1630 self.proxy_prompt_wins_admission(&downstream_name);
1631 match self.router.add_legacy_prompt_with_behavior(
1632 ProxyPromptHandler::with_prefix(prompt, prefix, proxy_client.clone()),
1633 self.on_duplicate,
1634 ) {
1635 Ok(()) if completion_target_admitted && completion_supported => {
1636 self.router.add_legacy_prompt_completion_handler(
1637 downstream_name.clone(),
1638 ProxyCompletionHandler::for_prompt(
1639 proxy_client.clone(),
1640 downstream_name,
1641 upstream_name,
1642 ),
1643 );
1644 self.advertise_completions();
1645 }
1646 Ok(()) => {}
1647 Err(error) => {
1648 log::error!(
1649 target: "fastmcp_rust::builder",
1650 "Failed to register prefixed proxied prompt; code={:?}",
1651 error.code
1652 );
1653 }
1654 }
1655 }
1656 counts
1657 }
1658 ProxyTypedCatalog {
1659 tools: ProxyToolCatalog::Final(tools),
1660 resources: ProxyResourceCatalog::Final(resources),
1661 resource_templates: ProxyResourceTemplateCatalog::Final(resource_templates),
1662 prompts: ProxyPromptCatalog::Final(prompts),
1663 } => {
1664 let counts = (
1668 tools.len(),
1669 resources.len(),
1670 resource_templates.len(),
1671 prompts.len(),
1672 );
1673 for tool in tools {
1674 #[cfg(feature = "tasks")]
1675 let handler = match task_relay.as_ref() {
1676 Some(task_relay) => ProxyToolHandler::with_prefix_final_with_task_relay(
1677 tool,
1678 prefix,
1679 proxy_client.clone(),
1680 Arc::clone(task_relay),
1681 )?,
1682 None => {
1683 ProxyToolHandler::with_prefix_final(tool, prefix, proxy_client.clone())?
1684 }
1685 };
1686 #[cfg(not(feature = "tasks"))]
1687 let handler =
1688 ProxyToolHandler::with_prefix_final(tool, prefix, proxy_client.clone())?;
1689 if let Err(error) = self
1690 .router
1691 .add_final_tool_with_behavior(handler, self.on_duplicate)
1692 {
1693 log::error!(
1694 target: "fastmcp_rust::builder",
1695 "Failed to register prefixed exact-final proxied tool; code={:?}",
1696 error.code
1697 );
1698 }
1699 }
1700 for resource in resources {
1701 if let Err(error) = self.router.add_final_resource_with_behavior(
1702 FinalProxyResourceHandler::new(resource, proxy_client.clone()),
1703 self.on_duplicate,
1704 ) {
1705 log::error!(
1706 target: "fastmcp_rust::builder",
1707 "Failed to register prefixed exact-final proxied resource; code={:?}",
1708 error.code
1709 );
1710 }
1711 }
1712 for template in resource_templates {
1713 let downstream_uri = template.uri_template.clone();
1714 let completion_target_admitted =
1715 self.proxy_resource_template_wins_admission(&downstream_uri);
1716 match self.router.add_final_resource_with_behavior(
1717 FinalProxyResourceTemplateHandler::new(template, proxy_client.clone()),
1718 self.on_duplicate,
1719 ) {
1720 Ok(()) if completion_target_admitted && completion_supported => {
1721 self.router.add_resource_template_completion_handler(
1722 downstream_uri.clone(),
1723 ProxyCompletionHandler::for_resource_template(
1724 proxy_client.clone(),
1725 downstream_uri.clone(),
1726 downstream_uri,
1727 ),
1728 );
1729 self.advertise_completions();
1730 }
1731 Ok(()) => {}
1732 Err(error) => {
1733 log::error!(
1734 target: "fastmcp_rust::builder",
1735 "Failed to register prefixed exact-final proxied resource template; code={:?}",
1736 error.code
1737 );
1738 }
1739 }
1740 }
1741 for prompt in prompts {
1742 let upstream_name = prompt.name.clone();
1743 let downstream_name = format!("{prefix}/{upstream_name}");
1744 let completion_target_admitted =
1745 self.proxy_prompt_wins_admission(&downstream_name);
1746 match self.router.add_final_prompt_with_behavior(
1747 FinalProxyPromptHandler::with_prefix(prompt, prefix, proxy_client.clone()),
1748 self.on_duplicate,
1749 ) {
1750 Ok(()) if completion_target_admitted && completion_supported => {
1751 self.router.add_prompt_completion_handler(
1752 downstream_name.clone(),
1753 ProxyCompletionHandler::for_prompt(
1754 proxy_client.clone(),
1755 downstream_name,
1756 upstream_name,
1757 ),
1758 );
1759 self.advertise_completions();
1760 }
1761 Ok(()) => {}
1762 Err(error) => {
1763 log::error!(
1764 target: "fastmcp_rust::builder",
1765 "Failed to register prefixed exact-final proxied prompt; code={:?}",
1766 error.code
1767 );
1768 }
1769 }
1770 }
1771 counts
1772 }
1773 _ => {
1774 return Err(fastmcp_core::McpError::invalid_request(
1775 "proxy typed catalog mixes legacy and final component vectors",
1776 ));
1777 }
1778 };
1779
1780 let has_tools = tool_count > 0;
1781 let has_resources = resource_count > 0 || template_count > 0;
1782 let has_prompts = prompt_count > 0;
1783
1784 if has_tools {
1786 self.advertise_legacy_tools_list_changed();
1787 }
1788 if has_resources {
1789 self.advertise_legacy_resource_subscriptions();
1790 }
1791 if has_prompts {
1792 self.advertise_legacy_prompts_list_changed();
1793 }
1794
1795 log::info!(
1796 target: "fastmcp_rust::proxy",
1797 "Proxied {} tools, {} resources, {} templates, and {} prompts with a configured prefix",
1798 tool_count,
1799 resource_count,
1800 template_count,
1801 prompt_count
1802 );
1803
1804 Ok(self)
1805 }
1806
1807 #[cfg(feature = "proxy")]
1826 pub fn as_proxy_raw(
1827 self,
1828 client: fastmcp_client::Client,
1829 ) -> Result<Self, fastmcp_core::McpError> {
1830 self.as_proxy_raw_with_proxy_client(ProxyClient::from_client(client)?)
1831 }
1832
1833 #[cfg(feature = "proxy")]
1846 pub fn proxy_typed(
1847 self,
1848 proxy_client: ProxyClient,
1849 catalog: ProxyTypedCatalog,
1850 ) -> Result<Self, fastmcp_core::McpError> {
1851 self.register_raw_typed_proxy_catalog(proxy_client, catalog)
1852 }
1853
1854 #[cfg(feature = "proxy")]
1855 fn as_proxy_raw_with_proxy_client(
1856 self,
1857 proxy_client: ProxyClient,
1858 ) -> Result<Self, fastmcp_core::McpError> {
1859 let catalog = proxy_client.catalog_typed()?;
1860 self.register_raw_typed_proxy_catalog(proxy_client, catalog)
1861 }
1862
1863 #[cfg(feature = "proxy")]
1867 fn register_raw_typed_proxy_catalog(
1868 mut self,
1869 proxy_client: ProxyClient,
1870 catalog: ProxyTypedCatalog,
1871 ) -> Result<Self, fastmcp_core::McpError> {
1872 proxy_client.admit_typed_catalog(&catalog)?;
1877 self.adopt_upstream_proxy_instructions(&proxy_client)?;
1878 self.adopt_upstream_proxy_implementation(&proxy_client)?;
1879 let catalog_era = catalog.era()?;
1880 let completion_supported = proxy_client.supports_completion()?;
1881 #[cfg(feature = "tasks")]
1882 let task_relay = if catalog_era == ProtocolEra::Modern2026 {
1883 proxy_client.final_tasks_relay()?
1884 } else {
1885 None
1886 };
1887 #[cfg(feature = "tasks")]
1888 self.install_proxy_final_tasks_relay(task_relay.clone())?;
1889 match (
1890 catalog.tools,
1891 catalog.resources,
1892 catalog.resource_templates,
1893 catalog.prompts,
1894 ) {
1895 (
1896 ProxyToolCatalog::Legacy(tools),
1897 ProxyResourceCatalog::Legacy(resources),
1898 ProxyResourceTemplateCatalog::Legacy(resource_templates),
1899 ProxyPromptCatalog::Legacy(prompts),
1900 ) => {
1901 let has_tools = !tools.is_empty();
1902 let has_resources = !resources.is_empty() || !resource_templates.is_empty();
1903 let has_prompts = !prompts.is_empty();
1904 for tool in tools {
1908 self.router.add_legacy_tool_with_behavior(
1909 ProxyToolHandler::new(tool, proxy_client.clone()),
1910 self.on_duplicate,
1911 )?;
1912 }
1913 for resource in resources {
1914 self.router.add_legacy_resource_with_behavior(
1915 ProxyResourceHandler::new(resource, proxy_client.clone()),
1916 self.on_duplicate,
1917 )?;
1918 }
1919 for template in resource_templates {
1920 let downstream_uri = template.uri_template.clone();
1921 let completion_target_admitted =
1922 self.proxy_resource_template_wins_admission(&downstream_uri);
1923 self.router.add_legacy_resource_with_behavior(
1924 ProxyResourceHandler::from_template(template, proxy_client.clone()),
1925 self.on_duplicate,
1926 )?;
1927 if completion_target_admitted && completion_supported {
1928 self.router.add_legacy_resource_template_completion_handler(
1929 downstream_uri.clone(),
1930 ProxyCompletionHandler::for_resource_template(
1931 proxy_client.clone(),
1932 downstream_uri.clone(),
1933 downstream_uri,
1934 ),
1935 );
1936 self.advertise_completions();
1937 }
1938 }
1939 for prompt in prompts {
1940 let downstream_name = prompt.name.clone();
1941 let completion_target_admitted =
1942 self.proxy_prompt_wins_admission(&downstream_name);
1943 self.router.add_legacy_prompt_with_behavior(
1944 ProxyPromptHandler::new(prompt, proxy_client.clone()),
1945 self.on_duplicate,
1946 )?;
1947 if completion_target_admitted && completion_supported {
1948 self.router.add_legacy_prompt_completion_handler(
1949 downstream_name.clone(),
1950 ProxyCompletionHandler::for_prompt(
1951 proxy_client.clone(),
1952 downstream_name.clone(),
1953 downstream_name,
1954 ),
1955 );
1956 self.advertise_completions();
1957 }
1958 }
1959 if has_tools {
1960 self.advertise_legacy_tools_list_changed();
1961 }
1962 if has_resources {
1963 self.advertise_legacy_resource_subscriptions();
1964 }
1965 if has_prompts {
1966 self.advertise_legacy_prompts_list_changed();
1967 }
1968 }
1969 (
1970 ProxyToolCatalog::Final(tools),
1971 ProxyResourceCatalog::Final(resources),
1972 ProxyResourceTemplateCatalog::Final(resource_templates),
1973 ProxyPromptCatalog::Final(prompts),
1974 ) => {
1975 let has_tools = !tools.is_empty();
1976 let has_resources = !resources.is_empty() || !resource_templates.is_empty();
1977 let has_prompts = !prompts.is_empty();
1978 for tool in tools {
1979 let handler = ProxyToolHandler::from_final(tool, proxy_client.clone())?;
1984 self.router
1985 .add_final_tool_with_behavior(handler, self.on_duplicate)?;
1986 }
1987 for resource in resources {
1988 self.router.add_final_resource_with_behavior(
1989 FinalProxyResourceHandler::new(resource, proxy_client.clone()),
1990 self.on_duplicate,
1991 )?;
1992 }
1993 for template in resource_templates {
1994 let downstream_uri = template.uri_template.clone();
1995 let completion_target_admitted =
1996 self.proxy_resource_template_wins_admission(&downstream_uri);
1997 self.router.add_final_resource_with_behavior(
1998 FinalProxyResourceTemplateHandler::new(template, proxy_client.clone()),
1999 self.on_duplicate,
2000 )?;
2001 if completion_target_admitted && completion_supported {
2002 self.router.add_resource_template_completion_handler(
2003 downstream_uri.clone(),
2004 ProxyCompletionHandler::for_resource_template(
2005 proxy_client.clone(),
2006 downstream_uri.clone(),
2007 downstream_uri,
2008 ),
2009 );
2010 self.advertise_completions();
2011 }
2012 }
2013 for prompt in prompts {
2014 let downstream_name = prompt.name.clone();
2015 let completion_target_admitted =
2016 self.proxy_prompt_wins_admission(&downstream_name);
2017 self.router.add_final_prompt_with_behavior(
2018 FinalProxyPromptHandler::new(prompt, proxy_client.clone()),
2019 self.on_duplicate,
2020 )?;
2021 if completion_target_admitted && completion_supported {
2022 self.router.add_prompt_completion_handler(
2023 downstream_name.clone(),
2024 ProxyCompletionHandler::for_prompt(
2025 proxy_client.clone(),
2026 downstream_name.clone(),
2027 downstream_name,
2028 ),
2029 );
2030 self.advertise_completions();
2031 }
2032 }
2033 if has_tools {
2034 self.advertise_legacy_tools_list_changed();
2035 }
2036 if has_resources {
2037 self.advertise_legacy_resource_subscriptions();
2038 }
2039 if has_prompts {
2040 self.advertise_legacy_prompts_list_changed();
2041 }
2042 }
2043 _ => {
2044 return Err(fastmcp_core::McpError::invalid_request(
2045 "proxy typed catalog mixes legacy and final component vectors",
2046 ));
2047 }
2048 }
2049 Ok(self)
2050 }
2051
2052 #[must_use]
2090 pub fn mount(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2091 #[cfg(feature = "apps")]
2092 if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2093 log::error!(
2094 target: "fastmcp_rust::mount",
2095 "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2096 );
2097 return self;
2098 }
2099
2100 let has_tools = server.has_tools();
2101 let has_resources = server.has_resources();
2102 let has_prompts = server.has_prompts();
2103
2104 let source_router = server.into_router();
2105 let result = self
2106 .router
2107 .mount_with_behavior(source_router, prefix, self.on_duplicate);
2108
2109 for warning in &result.warnings {
2111 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2112 }
2113 for error in &result.errors {
2114 log::error!(target: "fastmcp_rust::mount", "{}", error);
2115 }
2116
2117 if has_tools && result.tools > 0 {
2119 self.advertise_legacy_tools_list_changed();
2120 }
2121 if has_resources && (result.resources > 0 || result.resource_templates > 0) {
2122 self.advertise_legacy_resource_subscriptions();
2123 }
2124 if has_prompts && result.prompts > 0 {
2125 self.advertise_legacy_prompts_list_changed();
2126 }
2127
2128 self
2129 }
2130
2131 #[must_use]
2137 pub fn mount_preserving_resource_uris(
2138 mut self,
2139 server: crate::Server,
2140 prefix: Option<&str>,
2141 ) -> Self {
2142 #[cfg(feature = "apps")]
2143 if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2144 log::error!(
2145 target: "fastmcp_rust::mount",
2146 "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2147 );
2148 return self;
2149 }
2150
2151 let has_tools = server.has_tools();
2152 let has_resources = server.has_resources();
2153 let has_prompts = server.has_prompts();
2154
2155 let source_router = server.into_router();
2156 let result =
2157 self.router
2158 .mount_namespaced_with_behavior(source_router, prefix, self.on_duplicate);
2159
2160 for warning in &result.warnings {
2161 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2162 }
2163 for error in &result.errors {
2164 log::error!(target: "fastmcp_rust::mount", "{}", error);
2165 }
2166
2167 if has_tools && result.tools > 0 {
2168 self.advertise_legacy_tools_list_changed();
2169 }
2170 if has_resources && (result.resources > 0 || result.resource_templates > 0) {
2171 self.advertise_legacy_resource_subscriptions();
2172 }
2173 if has_prompts && result.prompts > 0 {
2174 self.advertise_legacy_prompts_list_changed();
2175 }
2176
2177 self
2178 }
2179
2180 #[must_use]
2201 pub fn mount_tools(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2202 #[cfg(feature = "apps")]
2203 if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2204 log::error!(
2205 target: "fastmcp_rust::mount",
2206 "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2207 );
2208 return self;
2209 }
2210
2211 let source_router = server.into_router();
2212 let result =
2213 self.router
2214 .mount_tools_with_behavior(source_router, prefix, self.on_duplicate);
2215
2216 for warning in &result.warnings {
2218 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2219 }
2220 for error in &result.errors {
2221 log::error!(target: "fastmcp_rust::mount", "{}", error);
2222 }
2223
2224 if result.tools > 0 {
2226 self.advertise_legacy_tools_list_changed();
2227 }
2228
2229 self
2230 }
2231
2232 #[must_use]
2254 pub fn mount_resources(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2255 #[cfg(feature = "apps")]
2256 if server.router.has_mcp_apps_bound_components() && !self.has_active_official_mcp_apps() {
2257 log::error!(
2258 target: "fastmcp_rust::mount",
2259 "Mount rejected because the child contains MCP Apps components but the destination has no active compatible MCP Apps extension"
2260 );
2261 return self;
2262 }
2263
2264 let source_router = server.into_router();
2265 let result =
2266 self.router
2267 .mount_resources_with_behavior(source_router, prefix, self.on_duplicate);
2268
2269 for warning in &result.warnings {
2271 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2272 }
2273 for error in &result.errors {
2274 log::error!(target: "fastmcp_rust::mount", "{}", error);
2275 }
2276
2277 if result.resources > 0 || result.resource_templates > 0 {
2279 self.advertise_legacy_resource_subscriptions();
2280 }
2281
2282 self
2283 }
2284
2285 #[must_use]
2306 pub fn mount_prompts(mut self, server: crate::Server, prefix: Option<&str>) -> Self {
2307 let source_router = server.into_router();
2308 let result =
2309 self.router
2310 .mount_prompts_with_behavior(source_router, prefix, self.on_duplicate);
2311
2312 for warning in &result.warnings {
2314 log::warn!(target: "fastmcp_rust::mount", "{}", warning);
2315 }
2316 for error in &result.errors {
2317 log::error!(target: "fastmcp_rust::mount", "{}", error);
2318 }
2319
2320 if result.prompts > 0 {
2322 self.advertise_legacy_prompts_list_changed();
2323 }
2324
2325 self
2326 }
2327
2328 #[must_use]
2330 pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
2331 self.instructions = Some(instructions.into());
2332 self
2333 }
2334
2335 #[must_use]
2337 pub fn title(mut self, title: impl Into<String>) -> Self {
2338 self.title = Some(title.into());
2339 self
2340 }
2341
2342 #[must_use]
2344 pub fn description(mut self, description: impl Into<String>) -> Self {
2345 self.description = Some(description.into());
2346 self
2347 }
2348
2349 #[must_use]
2351 pub fn website_url(mut self, website_url: impl Into<String>) -> Self {
2352 self.website_url = Some(website_url.into());
2353 self
2354 }
2355
2356 #[must_use]
2358 pub fn icons(mut self, icons: Vec<fastmcp_protocol::common_types::RawIcon>) -> Self {
2359 self.icons = icons;
2360 self
2361 }
2362
2363 #[must_use]
2367 pub fn log_level(mut self, level: Level) -> Self {
2368 let filter = level.to_level_filter();
2369 self.logging.level = filter;
2370 self.console_config.log_level = filter;
2371 self
2372 }
2373
2374 #[must_use]
2376 pub fn log_level_filter(mut self, filter: LevelFilter) -> Self {
2377 self.logging.level = filter;
2378 self.console_config.log_level = filter;
2379 self
2380 }
2381
2382 #[must_use]
2386 pub fn log_timestamps(mut self, show: bool) -> Self {
2387 self.logging.timestamps = show;
2388 self.console_config.log_timestamps = show;
2389 self
2390 }
2391
2392 #[must_use]
2396 pub fn log_targets(mut self, show: bool) -> Self {
2397 self.logging.targets = show;
2398 self.console_config.log_targets = show;
2399 self
2400 }
2401
2402 #[must_use]
2404 pub fn log_file_line(mut self, show: bool) -> Self {
2405 self.logging.file_line = show;
2406 self.console_config.log_file_line = show;
2407 self
2408 }
2409
2410 #[must_use]
2412 pub fn logging(mut self, config: LoggingConfig) -> Self {
2413 self.console_config.log_level = config.level;
2414 self.console_config.log_timestamps = config.timestamps;
2415 self.console_config.log_targets = config.targets;
2416 self.console_config.log_file_line = config.file_line;
2417 self.logging = config;
2418 self
2419 }
2420
2421 #[must_use]
2444 pub fn with_console_config(mut self, config: ConsoleConfig) -> Self {
2445 self.logging = LoggingConfig::from(&config);
2446 self.console_config = config;
2447 self
2448 }
2449
2450 #[must_use]
2455 pub fn with_banner(mut self, style: BannerStyle) -> Self {
2456 self.console_config = self.console_config.with_banner(style);
2457 self
2458 }
2459
2460 #[must_use]
2462 pub fn without_banner(mut self) -> Self {
2463 self.console_config = self.console_config.without_banner();
2464 self
2465 }
2466
2467 #[must_use]
2474 pub fn with_traffic_logging(mut self, verbosity: TrafficVerbosity) -> Self {
2475 self.console_config = self.console_config.with_traffic(verbosity);
2476 self
2477 }
2478
2479 #[must_use]
2485 pub fn plain_mode(mut self) -> Self {
2486 self.console_config = self.console_config.plain_mode();
2487 self
2488 }
2489
2490 #[must_use]
2492 pub fn force_color(mut self) -> Self {
2493 self.console_config = self.console_config.force_color(true);
2494 self
2495 }
2496
2497 #[must_use]
2499 pub fn console_config(&self) -> &ConsoleConfig {
2500 &self.console_config
2501 }
2502
2503 #[must_use]
2527 pub fn on_startup<F, E>(mut self, hook: F) -> Self
2528 where
2529 F: FnOnce() -> Result<(), E> + Send + 'static,
2530 E: std::error::Error + Send + Sync + 'static,
2531 {
2532 self.lifespan.on_startup = Some(Box::new(move || {
2533 hook().map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
2534 }));
2535 self
2536 }
2537
2538 #[must_use]
2558 pub fn on_shutdown<F>(mut self, hook: F) -> Self
2559 where
2560 F: FnOnce() + Send + 'static,
2561 {
2562 self.lifespan.on_shutdown = Some(Box::new(hook));
2563 self
2564 }
2565
2566 #[cfg(all(test, feature = "tasks"))]
2569 #[must_use]
2570 pub(crate) fn with_task_manager(mut self, task_manager: SharedTaskManager) -> Self {
2571 self.task_manager = Some(task_manager);
2572 self.capabilities.tasks = None;
2575 self
2576 }
2577
2578 #[cfg(test)]
2580 fn request_timeout_secs(&self) -> u64 {
2581 self.request_timeout_secs
2582 }
2583
2584 #[must_use]
2597 pub fn build(mut self) -> Server {
2598 self.router
2600 .set_strict_input_validation(self.strict_input_validation);
2601 let console = fastmcp_console::console::FastMcpConsole::with_enabled(
2602 self.console_config.should_use_rich(),
2603 );
2604 let final_subscriptions = Arc::new(FinalSubscriptionRegistry::default());
2605 #[cfg(feature = "tasks")]
2606 self.install_default_in_memory_final_tasks();
2607 #[cfg(feature = "tasks")]
2608 let final_task_runtime = self.final_task_runtime.clone();
2609 #[cfg(all(feature = "proxy", feature = "tasks"))]
2610 let final_task_relay = self.final_task_relay.clone();
2611 #[cfg(feature = "tasks")]
2612 if let Some(task_runtime) = final_task_runtime.as_ref() {
2613 let subscriptions = Arc::clone(&final_subscriptions);
2614 task_runtime.add_notification_emitter(Arc::new(move |notification| {
2615 if subscriptions.publish_task(notification).is_err() {
2616 log::error!(
2617 target: "fastmcp_rust::server",
2618 "Failed to publish a typed final Task notification"
2619 );
2620 }
2621 }));
2622 }
2623 #[cfg(feature = "tasks")]
2624 self.router
2625 .set_final_task_runtime(final_task_runtime.clone());
2626 #[cfg(all(feature = "proxy", feature = "tasks"))]
2627 self.router.set_final_task_relay(final_task_relay.clone());
2628 let extension_runtime = match self.extension_runtime {
2629 Some(mut runtime) => {
2630 runtime
2631 .freeze()
2632 .expect("validated server extension descriptors must freeze");
2633 Some(Arc::new(runtime))
2634 }
2635 #[cfg(all(feature = "proxy", feature = "tasks"))]
2636 None => match (final_task_runtime.as_ref(), final_task_relay.as_ref()) {
2637 (Some(task_runtime), None) => {
2638 let mut runtime = ServerExtensionRuntime::with_final_tasks(task_runtime)
2639 .expect("final Tasks must install into an empty extension registry");
2640 runtime
2641 .freeze()
2642 .expect("final Tasks extension descriptors must freeze");
2643 Some(Arc::new(runtime))
2644 }
2645 (None, Some(task_relay)) => {
2646 let mut runtime =
2647 ServerExtensionRuntime::with_proxy_final_tasks(Arc::clone(task_relay))
2648 .expect(
2649 "proxy final Tasks must install into an empty extension registry",
2650 );
2651 runtime
2652 .freeze()
2653 .expect("proxy final Tasks extension descriptors must freeze");
2654 Some(Arc::new(runtime))
2655 }
2656 (None, None) => None,
2657 (Some(_), Some(_)) => unreachable!("builder rejects mixed final Tasks owners"),
2658 },
2659 #[cfg(all(feature = "tasks", not(feature = "proxy")))]
2660 None => match final_task_runtime.as_ref() {
2661 Some(task_runtime) => {
2662 let mut runtime = ServerExtensionRuntime::with_final_tasks(task_runtime)
2663 .expect("final Tasks must install into an empty extension registry");
2664 runtime
2665 .freeze()
2666 .expect("final Tasks extension descriptors must freeze");
2667 Some(Arc::new(runtime))
2668 }
2669 None => None,
2670 },
2671 #[cfg(not(feature = "tasks"))]
2672 None => None,
2673 };
2674
2675 Server {
2676 info: self.info,
2677 title: self.title,
2678 description: self.description,
2679 website_url: self.website_url,
2680 icons: self.icons,
2681 capabilities: self.capabilities,
2682 router: Arc::new(self.router),
2683 instructions: self.instructions,
2684 request_timeout_secs: self.request_timeout_secs,
2685 stats: if self.stats_enabled {
2686 Some(ServerStats::new())
2687 } else {
2688 None
2689 },
2690 mask_error_details: self.mask_error_details,
2691 logging: self.logging,
2692 console_config: self.console_config,
2693 console,
2694 lifespan: Mutex::new(Some(self.lifespan)),
2695 auth_provider: self.auth_provider,
2696 middleware: Arc::new(self.middleware),
2697 active_requests: Arc::new(Mutex::new(HashMap::new())),
2698 #[cfg(all(test, feature = "tasks"))]
2699 task_manager: self.task_manager,
2700 max_bidirectional_requests_per_connection: self
2701 .max_bidirectional_requests_per_connection,
2702 protocol_policy: self.protocol_policy,
2703 http_config: self.http_config,
2704 oauth_http_routes: self.oauth_http_routes,
2705 extension_runtime,
2706 #[cfg(feature = "tasks")]
2707 final_task_runtime: self.final_task_runtime,
2708 #[cfg(all(feature = "proxy", feature = "tasks"))]
2709 final_task_relay,
2710 final_subscriptions,
2711 }
2712 }
2713
2714 pub fn try_build(self) -> Result<Server, ServerLaunchPolicyError> {
2719 Ok(self.build())
2720 }
2721}
2722
2723#[cfg(test)]
2724mod tests {
2725 use super::*;
2726 #[cfg(all(feature = "proxy", feature = "tasks"))]
2727 use crate::proxy::{ProxyFinalTaskListener, ProxyFinalTaskListenerEvent};
2728 #[cfg(feature = "proxy")]
2729 use asupersync::Cx;
2730 #[cfg(all(feature = "proxy", feature = "tasks"))]
2731 use fastmcp_client::FinalToolCallOutcome;
2732 #[cfg(all(feature = "proxy", feature = "tasks"))]
2733 use fastmcp_core::block_on;
2734 use fastmcp_core::{McpContext, McpResult};
2735 #[cfg(feature = "apps")]
2736 use fastmcp_protocol::FinalTool;
2737 #[cfg(feature = "apps")]
2738 use fastmcp_protocol::common_types::AbsoluteUri;
2739 #[cfg(feature = "proxy")]
2740 use fastmcp_protocol::common_types::{ContentBlock, Implementation};
2741 #[cfg(feature = "apps")]
2742 use fastmcp_protocol::extensions::ExtensionNegotiationError;
2743 use fastmcp_protocol::protocol_policy::ProtocolPolicy;
2744 #[cfg(feature = "proxy")]
2745 use fastmcp_protocol::protocol_policy::{ProtocolEra, StdioOpeningFrame};
2746 #[cfg(feature = "proxy")]
2747 use fastmcp_protocol::{
2748 CallToolResult, CompleteResult, CoreResult, FinalCallToolResult, FinalCoreResult,
2749 JsonRpcRequest, LegacyContent, LegacyCoreResult, ResultMeta,
2750 };
2751 use fastmcp_protocol::{Content, Prompt, Resource, ResourceContent, Tool};
2752 #[cfg(all(feature = "proxy", feature = "tasks"))]
2753 use fastmcp_protocol::{
2754 CoreResultDiscriminatorPolicy, CreateTaskResult, DecodedResult, EmptyTaskResult,
2755 ResultPeerEra, SubscriptionFilter, decode_peer_result,
2756 };
2757
2758 struct TestTool;
2761 impl crate::ToolHandler for TestTool {
2762 fn definition(&self) -> Tool {
2763 Tool {
2764 name: "test_tool".to_string(),
2765 description: Some("a test tool".to_string()),
2766 input_schema: serde_json::json!({"type": "object"}),
2767 output_schema: None,
2768 icon: None,
2769 version: None,
2770 tags: vec![],
2771 annotations: None,
2772 }
2773 }
2774 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2775 Ok(vec![Content::text("ok")])
2776 }
2777 }
2778
2779 struct ExactLegacyOnlyTool;
2780 impl crate::ToolHandler for ExactLegacyOnlyTool {
2781 fn definition(&self) -> Tool {
2782 Tool {
2783 name: "exact_legacy_only".to_owned(),
2784 description: Some("exact 2024-only test tool".to_owned()),
2785 input_schema: serde_json::json!({"type": "object"}),
2786 output_schema: Some(serde_json::json!(false)),
2787 icon: None,
2788 version: None,
2789 tags: Vec::new(),
2790 annotations: None,
2791 }
2792 }
2793
2794 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2795 Ok(vec![Content::text("legacy")])
2796 }
2797 }
2798
2799 #[cfg(feature = "apps")]
2800 struct MountedAppsTool;
2801
2802 #[cfg(feature = "apps")]
2803 impl crate::ToolHandler for MountedAppsTool {
2804 fn definition(&self) -> Tool {
2805 Tool {
2806 name: "mounted_apps_tool".to_owned(),
2807 description: Some("final-only Apps mount fixture".to_owned()),
2808 input_schema: serde_json::json!({"type": "object"}),
2809 output_schema: None,
2810 icon: None,
2811 version: None,
2812 tags: Vec::new(),
2813 annotations: None,
2814 }
2815 }
2816
2817 fn final_definition(&self) -> Option<FinalTool> {
2818 let metadata = fastmcp_protocol::McpAppsToolMetadata::try_new(
2819 Some(
2820 AbsoluteUri::parse("ui://mount/dashboard")
2821 .expect("fixed Apps mount URI is valid"),
2822 ),
2823 None,
2824 )
2825 .expect("fixed Apps mount metadata is valid")
2826 .to_open_metadata()
2827 .expect("fixed Apps mount metadata serializes");
2828 Some(FinalTool {
2829 name: "mounted_apps_tool".to_owned(),
2830 title: None,
2831 description: Some("final-only Apps mount fixture".to_owned()),
2832 input_schema: serde_json::json!({"type": "object"}),
2833 output_schema: None,
2834 annotations: None,
2835 icons: None,
2836 meta: Some(metadata),
2837 })
2838 }
2839
2840 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2841 Ok(vec![Content::text("Apps mount fixture")])
2842 }
2843 }
2844
2845 #[cfg(feature = "apps")]
2846 fn apps_mount_child() -> crate::Server {
2847 let resource = McpAppsUiResource::try_new(
2848 AbsoluteUri::parse("ui://mount/dashboard").expect("fixed Apps mount URI is valid"),
2849 "mounted-apps-dashboard",
2850 "<main>Apps mount fixture</main>",
2851 )
2852 .expect("fixed Apps mount resource is valid");
2853 ServerBuilder::new("apps-child", "1.0")
2854 .mcp_apps()
2855 .expect("child Apps extension installs")
2856 .mcp_apps_ui_resource(resource)
2857 .expect("child Apps resource registers")
2858 .mcp_apps_tool(MountedAppsTool)
2859 .expect("child Apps tool registers")
2860 .build()
2861 }
2862
2863 #[cfg(feature = "apps")]
2864 fn apps_resource_bound_tool_mount_child() -> crate::Server {
2865 let resource = McpAppsUiResource::try_new(
2866 AbsoluteUri::parse("ui://mount/dashboard").expect("fixed Apps mount URI is valid"),
2867 "mounted-apps-dashboard",
2868 "<main>Apps mount fixture</main>",
2869 )
2870 .expect("fixed Apps mount resource is valid");
2871 ServerBuilder::new("apps-resource-child", "1.0")
2872 .mcp_apps()
2873 .expect("child Apps extension installs")
2874 .mcp_apps_ui_resource(resource)
2875 .expect("child Apps resource registers")
2876 .tool(TestTool)
2877 .build()
2878 }
2879
2880 struct TestResource;
2881 impl crate::ResourceHandler for TestResource {
2882 fn definition(&self) -> Resource {
2883 Resource {
2884 uri: "file:///test".to_string(),
2885 name: "test_res".to_string(),
2886 description: None,
2887 mime_type: None,
2888 icon: None,
2889 version: None,
2890 tags: vec![],
2891 }
2892 }
2893 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
2894 Ok(vec![ResourceContent {
2895 uri: "file:///test".to_string(),
2896 mime_type: None,
2897 text: Some("content".to_string()),
2898 blob: None,
2899 }])
2900 }
2901 }
2902
2903 struct TestPrompt;
2904 impl crate::PromptHandler for TestPrompt {
2905 fn definition(&self) -> Prompt {
2906 Prompt {
2907 name: "test_prompt".to_string(),
2908 description: None,
2909 arguments: vec![],
2910 icon: None,
2911 version: None,
2912 tags: vec![],
2913 }
2914 }
2915 fn get(
2916 &self,
2917 _ctx: &McpContext,
2918 _args: std::collections::HashMap<String, String>,
2919 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
2920 Ok(vec![])
2921 }
2922 }
2923
2924 struct TestCompletion;
2925
2926 impl crate::handler::CompletionHandler for TestCompletion {
2927 fn complete_legacy(
2928 &self,
2929 _ctx: &McpContext,
2930 _params: fastmcp_protocol::LegacyCompletionParams,
2931 ) -> McpResult<fastmcp_protocol::CompletionValues> {
2932 Ok(fastmcp_protocol::CompletionValues {
2933 values: vec!["staging".to_string()],
2934 total: Some(1),
2935 has_more: Some(false),
2936 })
2937 }
2938
2939 fn complete_final(
2940 &self,
2941 _ctx: &McpContext,
2942 _params: fastmcp_protocol::FinalCompletionParams,
2943 ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
2944 Ok(fastmcp_protocol::FinalCompletionValues {
2945 values: vec!["staging".to_string()],
2946 total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
2947 has_more: Some(false),
2948 })
2949 }
2950 }
2951
2952 struct CountingCompletion(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2953
2954 impl crate::handler::CompletionHandler for CountingCompletion {
2955 fn complete_legacy(
2956 &self,
2957 _ctx: &McpContext,
2958 _params: fastmcp_protocol::LegacyCompletionParams,
2959 ) -> McpResult<fastmcp_protocol::CompletionValues> {
2960 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2961 Ok(fastmcp_protocol::CompletionValues {
2962 values: vec!["staging".to_string()],
2963 total: Some(1),
2964 has_more: Some(false),
2965 })
2966 }
2967
2968 fn complete_final(
2969 &self,
2970 _ctx: &McpContext,
2971 _params: fastmcp_protocol::FinalCompletionParams,
2972 ) -> McpResult<fastmcp_protocol::FinalCompletionValues> {
2973 self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2974 Ok(fastmcp_protocol::FinalCompletionValues {
2975 values: vec!["staging".to_string()],
2976 total: Some(fastmcp_protocol::JsonInteger::from(1_i64)),
2977 has_more: Some(false),
2978 })
2979 }
2980 }
2981
2982 struct MarkedTool(&'static str);
2983
2984 impl crate::ToolHandler for MarkedTool {
2985 fn definition(&self) -> Tool {
2986 Tool {
2987 name: "duplicate_tool".to_string(),
2988 description: Some(self.0.to_string()),
2989 input_schema: serde_json::json!({"type": "object"}),
2990 output_schema: None,
2991 icon: None,
2992 version: None,
2993 tags: vec![self.0.to_string()],
2994 annotations: None,
2995 }
2996 }
2997
2998 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
2999 Ok(vec![Content::text(self.0)])
3000 }
3001 }
3002
3003 struct MarkedResource(&'static str);
3004
3005 impl crate::ResourceHandler for MarkedResource {
3006 fn definition(&self) -> Resource {
3007 Resource {
3008 uri: "duplicate://resource".to_string(),
3009 name: self.0.to_string(),
3010 description: Some(self.0.to_string()),
3011 mime_type: None,
3012 icon: None,
3013 version: None,
3014 tags: vec![self.0.to_string()],
3015 }
3016 }
3017
3018 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
3019 Ok(vec![])
3020 }
3021 }
3022
3023 struct MarkedPrompt(&'static str);
3024
3025 impl crate::PromptHandler for MarkedPrompt {
3026 fn definition(&self) -> Prompt {
3027 Prompt {
3028 name: "duplicate_prompt".to_string(),
3029 description: Some(self.0.to_string()),
3030 arguments: vec![],
3031 icon: None,
3032 version: None,
3033 tags: vec![self.0.to_string()],
3034 }
3035 }
3036
3037 fn get(
3038 &self,
3039 _ctx: &McpContext,
3040 _args: std::collections::HashMap<String, String>,
3041 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
3042 Ok(vec![])
3043 }
3044 }
3045
3046 fn marked_resource_template(marker: &str) -> ResourceTemplate {
3047 ResourceTemplate {
3048 uri_template: "duplicate://{item}".to_string(),
3049 name: marker.to_string(),
3050 description: Some(marker.to_string()),
3051 mime_type: None,
3052 icon: None,
3053 version: None,
3054 tags: vec![marker.to_string()],
3055 }
3056 }
3057
3058 fn marked_builder(marker: &'static str) -> ServerBuilder {
3059 ServerBuilder::new("marked", "1.0")
3060 .tool(MarkedTool(marker))
3061 .resource(MarkedResource(marker))
3062 .resource_template(marked_resource_template(marker))
3063 .prompt(MarkedPrompt(marker))
3064 }
3065
3066 fn assert_marked_server(server: &crate::Server, marker: &str) {
3067 assert_eq!(server.tools().len(), 1);
3068 assert_eq!(server.resources().len(), 1);
3069 assert_eq!(server.resource_templates().len(), 1);
3070 assert_eq!(server.prompts().len(), 1);
3071 assert_eq!(server.tools()[0].tags, vec![marker.to_string()]);
3072 assert_eq!(server.resources()[0].tags, vec![marker.to_string()]);
3073 assert_eq!(
3074 server.resource_templates()[0].tags,
3075 vec![marker.to_string()]
3076 );
3077 assert_eq!(server.prompts()[0].tags, vec![marker.to_string()]);
3078 }
3079
3080 #[cfg(feature = "proxy")]
3081 struct DuplicatePolicyProxyBackend;
3082
3083 #[cfg(feature = "proxy")]
3084 impl crate::proxy::ProxyBackend for DuplicatePolicyProxyBackend {
3085 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
3086 Ok(duplicate_policy_proxy_catalog().tools)
3087 }
3088
3089 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
3090 Ok(duplicate_policy_proxy_catalog().resources)
3091 }
3092
3093 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
3094 Ok(duplicate_policy_proxy_catalog().resource_templates)
3095 }
3096
3097 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
3098 Ok(duplicate_policy_proxy_catalog().prompts)
3099 }
3100
3101 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
3102 Ok(vec![])
3103 }
3104
3105 fn call_tool_with_progress(
3106 &mut self,
3107 _: &str,
3108 _: serde_json::Value,
3109 _: crate::proxy::ProgressCallback<'_>,
3110 ) -> McpResult<Vec<Content>> {
3111 Ok(vec![])
3112 }
3113
3114 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
3115 Ok(vec![])
3116 }
3117
3118 fn get_prompt(
3119 &mut self,
3120 _: &str,
3121 _: std::collections::HashMap<String, String>,
3122 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
3123 Ok(vec![])
3124 }
3125 }
3126
3127 #[cfg(all(feature = "proxy", feature = "tasks"))]
3128 struct OrdinaryProxyTasksListener {
3129 accepted: Option<SubscriptionFilter>,
3130 }
3131
3132 #[cfg(all(feature = "proxy", feature = "tasks"))]
3133 impl ProxyFinalTaskListener for OrdinaryProxyTasksListener {
3134 fn next(
3135 &mut self,
3136 _cx: &Cx,
3137 _request_cancellation: &fastmcp_core::McpRequestCancellation,
3138 ) -> McpResult<ProxyFinalTaskListenerEvent> {
3139 match self.accepted.take() {
3140 Some(accepted) => Ok(ProxyFinalTaskListenerEvent::Acknowledged(accepted)),
3141 None => Ok(ProxyFinalTaskListenerEvent::Terminal),
3142 }
3143 }
3144 }
3145
3146 #[cfg(all(feature = "proxy", feature = "tasks"))]
3150 struct OrdinaryProxyTasksBackend {
3151 calls: Arc<Mutex<Vec<String>>>,
3152 updates: Arc<Mutex<Vec<serde_json::Value>>>,
3153 task: CreateTaskResult,
3154 }
3155
3156 #[cfg(all(feature = "proxy", feature = "tasks"))]
3157 impl crate::proxy::ProxyBackend for OrdinaryProxyTasksBackend {
3158 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
3159 Ok(Vec::new())
3160 }
3161
3162 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
3163 Ok(Vec::new())
3164 }
3165
3166 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
3167 Ok(Vec::new())
3168 }
3169
3170 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
3171 Ok(Vec::new())
3172 }
3173
3174 fn call_tool(
3175 &mut self,
3176 _name: &str,
3177 _arguments: serde_json::Value,
3178 ) -> McpResult<Vec<Content>> {
3179 Err(fastmcp_core::McpError::internal_error(
3180 "ordinary Tasks proxy test must retain the final result algebra",
3181 ))
3182 }
3183
3184 fn call_tool_with_progress(
3185 &mut self,
3186 name: &str,
3187 arguments: serde_json::Value,
3188 _on_progress: crate::proxy::ProgressCallback<'_>,
3189 ) -> McpResult<Vec<Content>> {
3190 self.call_tool(name, arguments)
3191 }
3192
3193 fn read_resource(&mut self, _uri: &str) -> McpResult<Vec<ResourceContent>> {
3194 Err(fastmcp_core::McpError::internal_error("not used"))
3195 }
3196
3197 fn get_prompt(
3198 &mut self,
3199 _name: &str,
3200 _arguments: std::collections::HashMap<String, String>,
3201 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
3202 Err(fastmcp_core::McpError::internal_error("not used"))
3203 }
3204
3205 fn supports_final_tasks_relay(&mut self) -> McpResult<bool> {
3206 Ok(true)
3207 }
3208
3209 fn call_tool_final_outcome(
3210 &mut self,
3211 name: &str,
3212 arguments: serde_json::Value,
3213 ) -> McpResult<FinalToolCallOutcome> {
3214 self.calls
3215 .lock()
3216 .expect("ordinary proxy task call log is not poisoned")
3217 .push(format!("tools/call:{name}"));
3218 if arguments.get("outcome") == Some(&serde_json::json!("task")) {
3219 return Ok(FinalToolCallOutcome::Task(self.task.clone()));
3220 }
3221 let (decoded, diagnostic) = decode_peer_result(
3222 r#"{"resultType":"input_required","requestState":"upstream-forged-state"}"#,
3223 ResultPeerEra::Modern,
3224 &CoreResultDiscriminatorPolicy,
3225 )
3226 .map_err(|error| fastmcp_core::McpError::invalid_request(error.to_string()))?;
3227 assert!(diagnostic.is_none(), "the fixed final fixture is explicit");
3228 let DecodedResult::InputRequired(result) = decoded else {
3229 return Err(fastmcp_core::McpError::internal_error(
3230 "ordinary proxy test fixture must decode as input_required",
3231 ));
3232 };
3233 Ok(FinalToolCallOutcome::InputRequired(result))
3234 }
3235
3236 fn update_final_task(
3237 &mut self,
3238 task: &fastmcp_protocol::Task,
3239 input_responses: fastmcp_protocol::TaskInputResponses,
3240 ) -> McpResult<fastmcp_protocol::UpdateTaskResult> {
3241 self.calls
3242 .lock()
3243 .expect("ordinary proxy task call log is not poisoned")
3244 .push("tasks/update".to_owned());
3245 assert_eq!(
3246 task.base().task_id.as_str(),
3247 self.task.task.base().task_id.as_str(),
3248 "the relay supplies the retained upstream task rather than admitting an arbitrary id"
3249 );
3250 self.updates
3251 .lock()
3252 .expect("ordinary proxy task update receipt is not poisoned")
3253 .push(
3254 serde_json::to_value(&input_responses)
3255 .expect("the exact public Tasks input response map serializes"),
3256 );
3257 Ok(EmptyTaskResult::default())
3258 }
3259
3260 fn open_final_task_listener(
3261 &mut self,
3262 notifications: SubscriptionFilter,
3263 ) -> McpResult<Box<dyn ProxyFinalTaskListener>> {
3264 self.calls
3265 .lock()
3266 .expect("ordinary proxy task call log is not poisoned")
3267 .push("subscriptions/listen".to_owned());
3268 Ok(Box::new(OrdinaryProxyTasksListener {
3269 accepted: Some(notifications),
3270 }))
3271 }
3272 }
3273
3274 #[cfg(feature = "proxy")]
3275 fn duplicate_policy_proxy_catalog() -> ProxyCatalog {
3276 ProxyCatalog {
3277 tool_catalog_era: Some(ProtocolEra::Legacy2024),
3278 tools: vec![Tool {
3279 name: "test_tool".to_string(),
3280 description: Some("proxied tool".to_string()),
3281 input_schema: serde_json::json!({"type": "object"}),
3284 output_schema: None,
3285 icon: None,
3286 version: None,
3287 tags: vec![],
3288 annotations: None,
3289 }],
3290 resources: vec![Resource {
3291 uri: "file:///test".to_string(),
3292 name: "proxied resource".to_string(),
3293 description: Some("proxied resource".to_string()),
3294 mime_type: None,
3295 icon: None,
3296 version: None,
3297 tags: vec![],
3298 }],
3299 prompts: vec![Prompt {
3300 name: "test_prompt".to_string(),
3301 description: Some("proxied prompt".to_string()),
3302 arguments: vec![],
3303 icon: None,
3304 version: None,
3305 tags: vec![],
3306 }],
3307 ..ProxyCatalog::default()
3308 }
3309 }
3310
3311 #[test]
3314 fn builder_new_sets_info() {
3315 let builder = ServerBuilder::new("my-server", "2.0.0");
3316 let server = builder.build();
3317 assert_eq!(server.info().name, "my-server");
3318 assert_eq!(server.info().version, "2.0.0");
3319 }
3320
3321 #[test]
3322 fn builder_default_has_logging_capability() {
3323 let builder = ServerBuilder::new("srv", "1.0");
3324 let server = builder.build();
3325 assert!(server.capabilities().logging.is_some());
3326 }
3327
3328 #[test]
3329 fn builder_default_has_no_tool_resource_prompt_capabilities() {
3330 let builder = ServerBuilder::new("srv", "1.0");
3331 let server = builder.build();
3332 assert!(server.capabilities().tools.is_none());
3333 assert!(server.capabilities().resources.is_none());
3334 assert!(server.capabilities().prompts.is_none());
3335 }
3336
3337 #[cfg(feature = "apps")]
3338 #[test]
3339 fn builder_manual_apps_discovery_requires_the_exact_empty_marker_before_build() {
3340 let mut accepted_descriptors = fastmcp_protocol::ExtensionDescriptorRegistry::new();
3341 let accepted_id =
3342 fastmcp_protocol::register_official_mcp_apps_extension(&mut accepted_descriptors)
3343 .expect("the official Apps descriptor registers");
3344 let accepted = ServerBuilder::new("apps-marker", "1.0").extension_registry(
3345 crate::ExtensionHandlerRegistry::new(accepted_descriptors),
3346 ServerExtensionDiscovery {
3347 extensions: std::collections::BTreeMap::from([(
3348 accepted_id,
3349 fastmcp_protocol::official_mcp_apps_empty_server_settings(),
3350 )]),
3351 },
3352 |_descriptor: &fastmcp_protocol::ExtensionDescriptor,
3353 _client: &fastmcp_protocol::ExtensionSettings,
3354 _server: &fastmcp_protocol::ExtensionSettings|
3355 -> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
3356 Ok(fastmcp_protocol::official_mcp_apps_empty_server_settings())
3357 },
3358 );
3359 assert!(
3360 accepted.is_ok(),
3361 "the exact empty official Apps marker is accepted during builder configuration"
3362 );
3363
3364 let mut rejected_descriptors = fastmcp_protocol::ExtensionDescriptorRegistry::new();
3365 let rejected_id =
3366 fastmcp_protocol::register_official_mcp_apps_extension(&mut rejected_descriptors)
3367 .expect("the official Apps descriptor registers");
3368 let rejected = ServerBuilder::new("apps-marker", "1.0").extension_registry(
3369 crate::ExtensionHandlerRegistry::new(rejected_descriptors),
3370 ServerExtensionDiscovery {
3371 extensions: std::collections::BTreeMap::from([(
3372 rejected_id,
3373 fastmcp_protocol::ExtensionSettings::new(serde_json::json!({
3374 "unexpected": true,
3375 }))
3376 .expect("the one-field alternate is generic extension metadata"),
3377 )]),
3378 },
3379 |_descriptor: &fastmcp_protocol::ExtensionDescriptor,
3380 _client: &fastmcp_protocol::ExtensionSettings,
3381 _server: &fastmcp_protocol::ExtensionSettings|
3382 -> Result<fastmcp_protocol::ExtensionSettings, ExtensionNegotiationError> {
3383 Ok(fastmcp_protocol::official_mcp_apps_empty_server_settings())
3384 },
3385 );
3386 assert!(
3387 matches!(
3388 rejected,
3389 Err(crate::ServerExtensionConfigurationError::Registry(
3390 fastmcp_protocol::ExtensionRegistryError::OfficialMcpAppsServerSettingsNotEmpty
3391 ))
3392 ),
3393 "adding one discovery setting is rejected by extension_registry before build"
3394 );
3395 }
3396
3397 #[test]
3398 fn builder_completion_handler_activates_exact_discovery_capability() {
3399 let server = ServerBuilder::new("srv", "1.0")
3400 .completion_handler(TestCompletion)
3401 .build();
3402 let discovery = server
3403 .server_discovery()
3404 .expect("installed completion handler produces discovery");
3405 let wire = serde_json::to_value(discovery).expect("discovery serializes");
3406
3407 assert_eq!(wire["capabilities"]["completions"], serde_json::json!({}));
3408 }
3409
3410 #[test]
3411 fn builder_provider_specific_completion_requires_an_admitted_final_target() {
3412 let unmatched = ServerBuilder::new("srv", "1.0")
3413 .prompt_completion_handler("duplicate_prompt", TestCompletion)
3414 .build();
3415 let unmatched_discovery = unmatched
3416 .server_discovery()
3417 .expect("the unmatched provider-only server still discovers");
3418 let unmatched_wire =
3419 serde_json::to_value(unmatched_discovery).expect("discovery serializes");
3420 assert!(
3421 unmatched_wire["capabilities"].get("completions").is_none(),
3422 "an unbound provider-only registration must not advertise completion"
3423 );
3424
3425 let matched = ServerBuilder::new("srv", "1.0")
3426 .prompt(MarkedPrompt("provider-target"))
3427 .prompt_completion_handler("duplicate_prompt", TestCompletion)
3428 .build();
3429 let matched_discovery = matched
3430 .server_discovery()
3431 .expect("an admitted final prompt activates its provider route");
3432 let matched_wire = serde_json::to_value(matched_discovery).expect("discovery serializes");
3433
3434 assert_eq!(
3435 matched_wire["capabilities"]["completions"],
3436 serde_json::json!({}),
3437 "adding only the final prompt target makes the provider discoverable"
3438 );
3439 }
3440
3441 #[test]
3442 fn builder_without_completion_handler_omits_discovery_capability() {
3443 let server = ServerBuilder::new("srv", "1.0").build();
3444 let discovery = server
3445 .server_discovery()
3446 .expect("server without completion handler still discovers");
3447 let wire = serde_json::to_value(discovery).expect("discovery serializes");
3448
3449 assert!(
3450 wire["capabilities"].get("completions").is_none(),
3451 "absence of the handler must not advertise completion"
3452 );
3453 }
3454
3455 #[test]
3456 fn builder_completion_handler_rejects_final_metadata_before_handler_state_changes() {
3457 let invocations = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
3458 let server = ServerBuilder::new("srv", "1.0")
3459 .completion_handler(CountingCompletion(std::sync::Arc::clone(&invocations)))
3460 .build();
3461 let request_ctx = McpContext::new(asupersync::Cx::for_testing(), 93);
3462 let baseline = fastmcp_protocol::JsonRpcRequest::new(
3463 "completion/complete",
3464 Some(serde_json::json!({
3465 "ref": {"type": "ref/prompt", "name": "deploy"},
3466 "argument": {"name": "environment", "value": "sta"},
3467 })),
3468 93_i64,
3469 );
3470 let mut planted = baseline.clone();
3471 planted
3472 .params
3473 .as_mut()
3474 .and_then(serde_json::Value::as_object_mut)
3475 .expect("completion parameters are an object")
3476 .insert(
3477 "_meta".to_string(),
3478 serde_json::json!({
3479 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
3480 }),
3481 );
3482
3483 let baseline_result = server
3484 .router
3485 .dispatch_legacy_completion(&request_ctx, &baseline)
3486 .expect("baseline legacy completion reaches the builder-installed handler");
3487 assert_eq!(
3488 invocations.load(std::sync::atomic::Ordering::SeqCst),
3489 1,
3490 "the accepted request invokes the installed handler once"
3491 );
3492 let planted_before = serde_json::to_vec(&planted).expect("planted request serializes");
3493
3494 let error = server
3495 .router
3496 .dispatch_legacy_completion(&request_ctx, &planted)
3497 .expect_err("the sole final metadata field is refused in the exact legacy route");
3498 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
3499 assert_eq!(
3500 invocations.load(std::sync::atomic::Ordering::SeqCst),
3501 1,
3502 "cross-era rejection occurs before handler-owned state can change"
3503 );
3504 assert_eq!(
3505 serde_json::to_vec(&planted).expect("rejected request serializes"),
3506 planted_before,
3507 "the rejected request remains caller-owned and unchanged"
3508 );
3509 assert_eq!(
3510 server
3511 .router
3512 .dispatch_legacy_completion(&request_ctx, &baseline)
3513 .expect("baseline remains dispatchable after the rejection"),
3514 baseline_result,
3515 "the rejected one-field variant cannot alter the accepted completion result"
3516 );
3517 assert_eq!(
3518 invocations.load(std::sync::atomic::Ordering::SeqCst),
3519 2,
3520 "only accepted requests mutate handler-owned state"
3521 );
3522 }
3523
3524 #[test]
3525 fn builder_default_stats_enabled() {
3526 let server = ServerBuilder::new("srv", "1.0").build();
3527 assert!(server.stats().is_some());
3528 }
3529
3530 #[test]
3531 fn builder_default_request_timeout() {
3532 let builder = ServerBuilder::new("srv", "1.0");
3533 assert_eq!(builder.request_timeout_secs(), DEFAULT_REQUEST_TIMEOUT_SECS);
3534 }
3535
3536 #[test]
3537 fn builder_bidirectional_limit_has_exact_validated_boundaries() {
3538 let default = ServerBuilder::new("srv", "1.0");
3539 assert_eq!(
3540 default.max_bidirectional_requests_per_connection,
3541 crate::bidirectional::DEFAULT_MAX_IN_FLIGHT_REQUESTS
3542 );
3543
3544 for valid in [1, crate::bidirectional::HARD_MAX_IN_FLIGHT_REQUESTS] {
3545 let server = ServerBuilder::new("srv", "1.0")
3546 .max_bidirectional_requests_per_connection(valid)
3547 .expect("boundary must be valid")
3548 .build();
3549 assert_eq!(server.max_bidirectional_requests_per_connection, valid);
3550 assert_eq!(
3551 server.new_pending_requests_for_connection().max_in_flight(),
3552 valid
3553 );
3554 }
3555
3556 for invalid in [0, crate::bidirectional::HARD_MAX_IN_FLIGHT_REQUESTS + 1] {
3557 let Err(error) =
3558 ServerBuilder::new("srv", "1.0").max_bidirectional_requests_per_connection(invalid)
3559 else {
3560 panic!("out-of-range limit must fail closed");
3561 };
3562 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidParams);
3563 }
3564 }
3565
3566 #[test]
3567 fn builder_default_error_masking_disabled() {
3568 let builder = ServerBuilder::new("srv", "1.0");
3569 assert!(!builder.is_error_masking_enabled());
3570 }
3571
3572 #[test]
3573 fn builder_default_strict_validation_disabled() {
3574 let builder = ServerBuilder::new("srv", "1.0");
3575 assert!(!builder.is_strict_input_validation_enabled());
3576 }
3577
3578 #[test]
3579 fn explicit_protocol_policy_applies_without_reserved_launch_setting() {
3580 let server = ServerBuilder::from_launch_protocol_policy("srv", "1.0", Ok(None))
3581 .expect("unset launch policy must construct a builder")
3582 .protocol_policy(ProtocolPolicy::ModernOnly)
3583 .expect("ModernOnly must be available to this test build")
3584 .try_build()
3585 .expect("unset launch policy must permit a server");
3586
3587 assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
3588 }
3589
3590 #[test]
3591 fn launch_policy_unset_defaults_to_auto() {
3592 assert_eq!(protocol_policy_from_server_launch_value(None), Ok(None));
3593 }
3594
3595 #[cfg(not(feature = "legacy-2024-11-05"))]
3596 #[test]
3597 fn no_legacy_public_builder_defaults_to_modern_only() {
3598 let server = ServerBuilder::try_new("srv", "1.0")
3599 .expect("no-legacy construction must succeed")
3600 .try_build()
3601 .expect("the default no-legacy builder must build");
3602
3603 assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
3604 }
3605
3606 #[cfg(not(feature = "legacy-2024-11-05"))]
3607 #[test]
3608 fn no_legacy_source_exposes_a_modern_endpoint_builder_and_gates_the_dual_era_one() {
3609 let source = include_str!("builder.rs");
3610 assert!(source.contains(
3611 "/// Builds a live modern Streamable HTTP endpoint.\n #[cfg(not(any(feature = \"legacy-2024-11-05\", test)))]\n pub fn build_http_endpoint"
3612 ));
3613 assert!(
3614 source.contains(
3615 "/// Builds a live dual-era HTTP endpoint with an exact legacy SSE origin."
3616 )
3617 );
3618 assert!(source.contains("crate::ServerHttpEndpointError"));
3619 assert!(!source.contains("fastmcp_transport::http::DualEraHttpEndpointError"));
3620 }
3621
3622 #[cfg(not(feature = "legacy-2024-11-05"))]
3623 #[test]
3624 fn no_legacy_public_builder_rejects_legacy_policies_without_mutation() {
3625 for policy in [ProtocolPolicy::Auto, ProtocolPolicy::LegacyOnly] {
3626 let mut builder =
3627 ServerBuilder::try_new("srv", "1.0").expect("no-legacy construction must succeed");
3628
3629 assert_eq!(
3630 builder.try_set_protocol_policy(policy),
3631 Err(ServerLaunchPolicyError::FeatureUnavailable),
3632 "{policy:?} must reject before changing a no-legacy builder"
3633 );
3634
3635 let server = builder
3636 .try_build()
3637 .expect("a rejected policy must leave the builder buildable");
3638 assert_eq!(
3639 server.protocol_policy(),
3640 ProtocolPolicy::ModernOnly,
3641 "{policy:?} differs from the default only by requiring unavailable legacy behavior"
3642 );
3643 }
3644 }
3645
3646 #[cfg(feature = "legacy-2024-11-05")]
3647 #[test]
3648 fn legacy_enabled_public_builder_preserves_auto() {
3649 let mut builder =
3650 ServerBuilder::try_new("srv", "1.0").expect("legacy-enabled construction must succeed");
3651 builder
3652 .try_set_protocol_policy(ProtocolPolicy::Auto)
3653 .expect("Auto must remain available with the legacy adapter enabled");
3654
3655 let server = builder
3656 .try_build()
3657 .expect("legacy-enabled Auto builder must build");
3658 assert_eq!(server.protocol_policy(), ProtocolPolicy::Auto);
3659 }
3660
3661 #[test]
3662 fn launch_policy_accepts_exact_public_values() {
3663 assert_eq!(
3664 protocol_policy_from_server_launch_value(Some(OsStr::new("auto"))),
3665 Ok(Some(ProtocolPolicy::Auto))
3666 );
3667 assert_eq!(
3668 protocol_policy_from_server_launch_value(Some(OsStr::new("modern-only"))),
3669 Ok(Some(ProtocolPolicy::ModernOnly))
3670 );
3671 assert_eq!(
3672 protocol_policy_from_server_launch_value(Some(OsStr::new("legacy-only"))),
3673 Ok(Some(ProtocolPolicy::LegacyOnly))
3674 );
3675 }
3676
3677 #[cfg(feature = "legacy-2024-11-05")]
3678 #[test]
3679 fn valid_launch_policy_wins_over_explicit_builder_policy() {
3680 let server = ServerBuilder::from_launch_protocol_policy(
3681 "srv",
3682 "1.0",
3683 Ok(Some(ProtocolPolicy::ModernOnly)),
3684 )
3685 .expect("valid launch policy must construct a builder")
3686 .protocol_policy(ProtocolPolicy::LegacyOnly)
3687 .expect("the unit-test dual-era build supports LegacyOnly")
3688 .try_build()
3689 .expect("valid launch policy must build");
3690
3691 assert_eq!(server.protocol_policy(), ProtocolPolicy::ModernOnly);
3692 }
3693
3694 #[cfg(feature = "legacy-2024-11-05")]
3695 #[test]
3696 fn fixed_policy_constructor_reserves_policy_against_later_setter() {
3697 let server = ServerBuilder::try_new_with_fixed_protocol_policy(
3698 "srv",
3699 "1.0",
3700 ProtocolPolicy::ModernOnly,
3701 )
3702 .expect("ModernOnly is available in every feature profile")
3703 .protocol_policy(ProtocolPolicy::LegacyOnly)
3704 .expect("the later policy is valid in this dual-era test build")
3705 .try_build()
3706 .expect("the fixed-policy builder remains buildable");
3707
3708 assert_eq!(
3709 server.protocol_policy(),
3710 ProtocolPolicy::ModernOnly,
3711 "the fixed component policy must not be replaced by a later builder setter"
3712 );
3713 }
3714
3715 #[cfg(not(feature = "legacy-2024-11-05"))]
3716 #[test]
3717 fn fixed_policy_constructor_rejects_unavailable_policy_before_construction() {
3718 assert!(matches!(
3719 ServerBuilder::try_new_with_fixed_protocol_policy("srv", "1.0", ProtocolPolicy::Auto,),
3720 Err(ServerLaunchPolicyError::FeatureUnavailable)
3721 ));
3722 }
3723
3724 #[test]
3725 fn invalid_launch_policy_is_rejected_before_builder_construction() {
3726 let result = ServerBuilder::from_launch_protocol_policy(
3727 "srv",
3728 "1.0",
3729 Err(ServerLaunchPolicyError::InvalidValue),
3730 );
3731
3732 assert!(matches!(result, Err(ServerLaunchPolicyError::InvalidValue)));
3733 }
3734
3735 #[test]
3736 fn launch_policy_parser_rejects_unknown_value_without_auto_fallback() {
3737 assert_eq!(
3738 protocol_policy_from_server_launch_value(Some(OsStr::new("mcp-2025-11-25"))),
3739 Err(ServerLaunchPolicyError::InvalidValue)
3740 );
3741 }
3742
3743 #[cfg(unix)]
3744 #[test]
3745 fn launch_policy_parser_rejects_non_unicode_value_without_panic() {
3746 use std::os::unix::ffi::OsStrExt;
3747
3748 assert_eq!(
3749 protocol_policy_from_server_launch_value(Some(OsStr::from_bytes(b"modern-only\xff"))),
3750 Err(ServerLaunchPolicyError::NonUnicode)
3751 );
3752 }
3753
3754 #[test]
3757 fn builder_request_timeout() {
3758 let builder = ServerBuilder::new("srv", "1.0").request_timeout(60);
3759 assert_eq!(builder.request_timeout_secs(), 60);
3760 }
3761
3762 #[test]
3763 fn builder_request_timeout_zero_omits_server_ceiling() {
3764 let builder = ServerBuilder::new("srv", "1.0").request_timeout(0);
3765 assert_eq!(builder.request_timeout_secs(), 0);
3766 }
3767
3768 #[test]
3769 fn builder_without_stats() {
3770 let server = ServerBuilder::new("srv", "1.0").without_stats().build();
3771 assert!(server.stats().is_none());
3772 }
3773
3774 #[test]
3775 fn builder_mask_error_details() {
3776 let builder = ServerBuilder::new("srv", "1.0").mask_error_details(true);
3777 assert!(builder.is_error_masking_enabled());
3778 }
3779
3780 #[test]
3781 fn builder_strict_input_validation() {
3782 let builder = ServerBuilder::new("srv", "1.0").strict_input_validation(true);
3783 assert!(builder.is_strict_input_validation_enabled());
3784 }
3785
3786 #[test]
3787 fn builder_instructions() {
3788 let server = ServerBuilder::new("srv", "1.0")
3789 .instructions("Use this server wisely")
3790 .build();
3791 let _ = server;
3793 }
3794
3795 #[test]
3796 fn builder_log_level() {
3797 let builder = ServerBuilder::new("srv", "1.0").log_level(Level::Debug);
3798 assert_eq!(builder.logging.level, LevelFilter::Debug);
3799 assert_eq!(builder.console_config.log_level, LevelFilter::Debug);
3800 }
3801
3802 #[test]
3803 fn builder_log_level_filter() {
3804 let builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Warn);
3805 assert_eq!(builder.logging.level, LevelFilter::Warn);
3806 assert_eq!(builder.console_config.log_level, LevelFilter::Warn);
3807 }
3808
3809 #[test]
3810 fn builder_log_timestamps_and_targets() {
3811 let builder = ServerBuilder::new("srv", "1.0")
3812 .log_timestamps(false)
3813 .log_targets(false)
3814 .log_file_line(true);
3815 assert!(!builder.logging.timestamps);
3816 assert!(!builder.console_config.log_timestamps);
3817 assert!(!builder.logging.targets);
3818 assert!(!builder.console_config.log_targets);
3819 assert!(builder.logging.file_line);
3820 assert!(builder.console_config.log_file_line);
3821 }
3822
3823 #[test]
3826 fn builder_without_banner() {
3827 let builder = ServerBuilder::new("srv", "1.0").without_banner();
3828 let config = builder.console_config();
3829 assert_eq!(config.banner_style, BannerStyle::None);
3830 }
3831
3832 #[test]
3833 fn builder_with_banner_compact() {
3834 let builder = ServerBuilder::new("srv", "1.0").with_banner(BannerStyle::Compact);
3835 let config = builder.console_config();
3836 assert_eq!(config.banner_style, BannerStyle::Compact);
3837 }
3838
3839 #[test]
3840 fn builder_plain_mode() {
3841 let builder = ServerBuilder::new("srv", "1.0").plain_mode();
3842 let _config = builder.console_config();
3843 }
3844
3845 #[test]
3848 fn builder_tool_enables_capability() {
3849 let server = ServerBuilder::new("srv", "1.0").tool(TestTool).build();
3850 assert!(server.capabilities().tools.is_some());
3851 assert!(
3852 server
3853 .capabilities()
3854 .tools
3855 .as_ref()
3856 .is_some_and(|tools| tools.list_changed),
3857 "registering a tool must advertise tools.listChanged so clients can watch catalog mutations"
3858 );
3859 assert!(server.has_tools());
3860 }
3861
3862 #[cfg(feature = "apps")]
3863 #[test]
3864 fn builder_mcp_apps_tool_requires_apps_opt_in() {
3865 let Err(error) = ServerBuilder::new("srv", "1.0").mcp_apps_tool(TestTool) else {
3866 panic!("Apps tools must not register before Apps negotiation is configured");
3867 };
3868 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
3869 }
3870
3871 #[test]
3872 fn builder_legacy_tool_is_explicit_and_does_not_claim_modern_tools() {
3873 let server = ServerBuilder::new("srv", "1.0")
3874 .legacy_tool(ExactLegacyOnlyTool)
3875 .build();
3876 assert!(server.capabilities().tools.is_some());
3877 assert!(server.has_tools());
3878 let router = server.into_router();
3879 assert_eq!(router.tools_count(), 1);
3880 assert!(
3881 !router
3882 .server_discovery_behavior_registry()
3883 .contains(fastmcp_protocol::ServerBehavior::ToolsList)
3884 );
3885 }
3886
3887 #[test]
3888 fn builder_resource_enables_capability() {
3889 let server = ServerBuilder::new("srv", "1.0")
3890 .resource(TestResource)
3891 .build();
3892 assert!(server.capabilities().resources.is_some());
3893 assert!(
3894 server
3895 .capabilities()
3896 .resources
3897 .as_ref()
3898 .is_some_and(|resources| resources.subscribe && resources.list_changed),
3899 "registering a resource must advertise subscribe and listChanged"
3900 );
3901 assert!(server.has_resources());
3902 }
3903
3904 #[test]
3905 fn builder_prompt_enables_capability() {
3906 let server = ServerBuilder::new("srv", "1.0").prompt(TestPrompt).build();
3907 assert!(server.capabilities().prompts.is_some());
3908 assert!(
3909 server
3910 .capabilities()
3911 .prompts
3912 .as_ref()
3913 .is_some_and(|prompts| prompts.list_changed),
3914 "registering a prompt must advertise prompts.listChanged"
3915 );
3916 assert!(server.has_prompts());
3917 }
3918
3919 #[test]
3920 fn builder_all_handlers() {
3921 let server = ServerBuilder::new("srv", "1.0")
3922 .tool(TestTool)
3923 .resource(TestResource)
3924 .prompt(TestPrompt)
3925 .build();
3926 assert!(server.has_tools());
3927 assert!(server.has_resources());
3928 assert!(server.has_prompts());
3929 }
3930
3931 #[test]
3932 fn builder_no_handlers_means_no_capabilities() {
3933 let server = ServerBuilder::new("srv", "1.0").build();
3934 assert!(!server.has_tools());
3935 assert!(!server.has_resources());
3936 assert!(!server.has_prompts());
3937 }
3938
3939 #[test]
3942 fn builder_on_duplicate_default_is_warn() {
3943 let _builder = ServerBuilder::new("srv", "1.0");
3944 }
3946
3947 #[test]
3948 fn builder_on_duplicate_ignore() {
3949 let server = ServerBuilder::new("srv", "1.0")
3950 .on_duplicate(DuplicateBehavior::Ignore)
3951 .tool(TestTool)
3952 .build();
3953 assert!(server.has_tools());
3954 }
3955
3956 #[test]
3957 fn builder_on_duplicate_replace() {
3958 let server = ServerBuilder::new("srv", "1.0")
3959 .on_duplicate(DuplicateBehavior::Replace)
3960 .tool(TestTool)
3961 .build();
3962 assert!(server.has_tools());
3963 }
3964
3965 #[test]
3966 fn builder_resource_template_honors_duplicate_policy() {
3967 for behavior in [
3968 DuplicateBehavior::Warn,
3969 DuplicateBehavior::Ignore,
3970 DuplicateBehavior::Error,
3971 ] {
3972 let server = ServerBuilder::new("srv", "1.0")
3973 .on_duplicate(behavior)
3974 .resource_template(marked_resource_template("original"))
3975 .resource_template(marked_resource_template("incoming"))
3976 .build();
3977 assert_eq!(server.resource_templates().len(), 1);
3978 assert_eq!(server.resource_templates()[0].name, "original");
3979 }
3980
3981 let server = ServerBuilder::new("srv", "1.0")
3982 .on_duplicate(DuplicateBehavior::Replace)
3983 .resource_template(marked_resource_template("original"))
3984 .resource_template(marked_resource_template("incoming"))
3985 .build();
3986 assert_eq!(server.resource_templates().len(), 1);
3987 assert_eq!(server.resource_templates()[0].name, "incoming");
3988 }
3989
3990 #[test]
3993 fn builder_on_startup_builds() {
3994 let server = ServerBuilder::new("srv", "1.0")
3995 .on_startup(|| -> Result<(), std::io::Error> { Ok(()) })
3996 .build();
3997 let _ = server;
3998 }
3999
4000 #[test]
4001 fn builder_on_shutdown_builds() {
4002 let server = ServerBuilder::new("srv", "1.0").on_shutdown(|| {}).build();
4003 let _ = server;
4004 }
4005
4006 #[test]
4009 fn built_server_console_config_matches_builder() {
4010 let server = ServerBuilder::new("srv", "1.0").without_banner().build();
4011 assert_eq!(server.console_config().banner_style, BannerStyle::None);
4012 }
4013
4014 #[test]
4017 fn builder_chaining_fluent_api() {
4018 let server = ServerBuilder::new("chain", "3.0")
4019 .request_timeout(120)
4020 .mask_error_details(true)
4021 .strict_input_validation(true)
4022 .without_banner()
4023 .plain_mode()
4024 .tool(TestTool)
4025 .resource(TestResource)
4026 .prompt(TestPrompt)
4027 .on_shutdown(|| {})
4028 .build();
4029
4030 assert_eq!(server.info().name, "chain");
4031 assert_eq!(server.info().version, "3.0");
4032 assert!(server.has_tools());
4033 assert!(server.has_resources());
4034 assert!(server.has_prompts());
4035 }
4036
4037 #[test]
4040 fn builder_with_console_config() {
4041 let mut config = ConsoleConfig::new().with_banner(BannerStyle::None);
4042 config.log_level = LevelFilter::Trace;
4043 config.log_timestamps = false;
4044 config.log_targets = false;
4045 config.log_file_line = true;
4046 let builder = ServerBuilder::new("srv", "1.0").with_console_config(config);
4047 assert_eq!(builder.console_config().banner_style, BannerStyle::None);
4048 assert_eq!(builder.logging.level, LevelFilter::Trace);
4049 assert!(!builder.logging.timestamps);
4050 assert!(!builder.logging.targets);
4051 assert!(builder.logging.file_line);
4052 }
4053
4054 #[test]
4055 fn logging_and_console_setters_keep_one_effective_configuration() {
4056 let console = ConsoleConfig::new().with_log_level_filter(LevelFilter::Off);
4057 let builder = ServerBuilder::new("srv", "1.0")
4058 .log_level(Level::Debug)
4059 .with_console_config(console);
4060 assert_eq!(builder.logging.level, LevelFilter::Off);
4061 assert_eq!(builder.console_config.log_level, LevelFilter::Off);
4062
4063 let builder = builder.log_level_filter(LevelFilter::Warn);
4064 assert_eq!(builder.logging.level, LevelFilter::Warn);
4065 assert_eq!(builder.console_config.log_level, LevelFilter::Warn);
4066 }
4067
4068 #[test]
4069 fn builder_with_traffic_logging() {
4070 let builder = ServerBuilder::new("srv", "1.0").with_traffic_logging(TrafficVerbosity::Full);
4071 let config = builder.console_config();
4072 assert_eq!(config.traffic_verbosity, TrafficVerbosity::Full);
4073 }
4074
4075 #[test]
4076 fn builder_force_color() {
4077 let builder = ServerBuilder::new("srv", "1.0").force_color();
4078 let _config = builder.console_config();
4079 }
4081
4082 #[test]
4085 fn builder_logging_full_config() {
4086 let config = LoggingConfig {
4087 level: LevelFilter::Trace,
4088 timestamps: false,
4089 targets: false,
4090 file_line: true,
4091 };
4092 let _builder = ServerBuilder::new("srv", "1.0").logging(config);
4093 }
4094
4095 #[test]
4098 fn builder_list_page_size() {
4099 let server = ServerBuilder::new("srv", "1.0")
4100 .list_page_size(50)
4101 .tool(TestTool)
4102 .build();
4103 assert!(server.has_tools());
4104 }
4105
4106 #[test]
4109 fn builder_resource_template_enables_capability() {
4110 let template = ResourceTemplate {
4111 uri_template: "file://{path}".to_string(),
4112 name: "Template".to_string(),
4113 description: None,
4114 mime_type: None,
4115 icon: None,
4116 version: None,
4117 tags: vec![],
4118 };
4119 let server = ServerBuilder::new("srv", "1.0")
4120 .resource_template(template)
4121 .build();
4122 assert!(server.capabilities().resources.is_some());
4123 }
4124
4125 struct NoopMiddleware;
4128 impl crate::Middleware for NoopMiddleware {}
4129
4130 #[test]
4131 fn builder_middleware() {
4132 let server = ServerBuilder::new("srv", "1.0")
4133 .middleware(NoopMiddleware)
4134 .build();
4135 let _ = server;
4136 }
4137
4138 #[test]
4139 fn builder_multiple_middleware() {
4140 let server = ServerBuilder::new("srv", "1.0")
4141 .middleware(NoopMiddleware)
4142 .middleware(NoopMiddleware)
4143 .build();
4144 let _ = server;
4145 }
4146
4147 struct TestAuthProvider;
4150 impl crate::AuthProvider for TestAuthProvider {
4151 fn authenticate(
4152 &self,
4153 _ctx: &McpContext,
4154 _request: crate::auth::AuthRequest<'_>,
4155 ) -> McpResult<fastmcp_core::AuthContext> {
4156 Ok(fastmcp_core::AuthContext::with_subject("test-user"))
4157 }
4158 }
4159
4160 #[test]
4161 fn builder_auth_provider() {
4162 let server = ServerBuilder::new("srv", "1.0")
4163 .auth_provider(TestAuthProvider)
4164 .build();
4165 let _ = server;
4166 }
4167
4168 #[test]
4171 fn builder_auto_mask_errors() {
4172 let builder = ServerBuilder::new("srv", "1.0").auto_mask_errors();
4174 assert!(!builder.is_error_masking_enabled());
4176 }
4177
4178 struct DupTool(&'static str);
4181 impl crate::ToolHandler for DupTool {
4182 fn definition(&self) -> Tool {
4183 Tool {
4184 name: self.0.to_string(),
4185 description: None,
4186 input_schema: serde_json::json!({"type": "object"}),
4187 output_schema: None,
4188 icon: None,
4189 version: None,
4190 tags: vec![],
4191 annotations: None,
4192 }
4193 }
4194 fn call(&self, _ctx: &McpContext, _args: serde_json::Value) -> McpResult<Vec<Content>> {
4195 Ok(vec![Content::text("ok")])
4196 }
4197 }
4198
4199 #[test]
4200 fn builder_on_duplicate_error_logs_but_continues() {
4201 let server = ServerBuilder::new("srv", "1.0")
4204 .on_duplicate(DuplicateBehavior::Error)
4205 .tool(DupTool("dup"))
4206 .tool(DupTool("dup")) .build();
4208 assert!(server.has_tools());
4209 }
4210
4211 #[test]
4214 fn builder_mount_with_prefix() {
4215 let source = ServerBuilder::new("sub", "1.0")
4216 .tool(TestTool)
4217 .resource(TestResource)
4218 .prompt(TestPrompt)
4219 .build();
4220
4221 let main = ServerBuilder::new("main", "1.0")
4222 .mount(source, Some("sub"))
4223 .build();
4224
4225 assert!(main.has_tools());
4226 assert!(main.has_resources());
4227 assert!(main.has_prompts());
4228 }
4229
4230 #[test]
4231 fn builder_mount_without_prefix() {
4232 let source = ServerBuilder::new("sub", "1.0").tool(TestTool).build();
4233
4234 let main = ServerBuilder::new("main", "1.0")
4235 .mount(source, None)
4236 .build();
4237
4238 assert!(main.has_tools());
4239 }
4240
4241 #[cfg(feature = "apps")]
4242 #[test]
4243 fn builder_mount_rejects_apps_bound_child_without_apps_opt_in_atomically() {
4244 let main = ServerBuilder::new("main", "1.0")
4245 .tool(TestTool)
4246 .resource(TestResource)
4247 .mount(apps_mount_child(), None)
4248 .build();
4249
4250 assert!(main.has_tools());
4251 assert!(main.has_resources());
4252 assert!(
4253 main.extension_registry_receipt().is_none(),
4254 "rejecting a child must not adopt its Apps extension runtime"
4255 );
4256
4257 let router = main.into_router();
4258 assert_eq!(router.tools_count(), 1, "the rejected child adds no tools");
4259 assert_eq!(
4260 router.resources_count(),
4261 1,
4262 "the rejected child adds no resources"
4263 );
4264 }
4265
4266 #[cfg(feature = "apps")]
4267 #[test]
4268 fn builder_mount_accepts_apps_bound_child_with_active_apps_opt_in() {
4269 let main = ServerBuilder::new("main", "1.0")
4270 .mcp_apps()
4271 .expect("parent Apps extension installs")
4272 .mount(apps_mount_child(), None)
4273 .build();
4274
4275 assert!(main.has_tools());
4276 assert!(main.has_resources());
4277 assert_eq!(
4278 main.extension_registry_receipt()
4279 .expect("parent retains its Apps extension runtime")
4280 .descriptor_count(),
4281 1,
4282 "mounting does not transfer or merge the child extension runtime"
4283 );
4284
4285 let router = main.into_router();
4286 assert_eq!(router.tools_count(), 1);
4287 assert_eq!(router.resources_count(), 1);
4288 }
4289
4290 #[cfg(feature = "apps")]
4291 #[test]
4292 fn builder_mount_tools_rejects_apps_bound_child_without_apps_opt_in_atomically() {
4293 let main = ServerBuilder::new("main", "1.0")
4294 .resource(TestResource)
4295 .mount_tools(apps_resource_bound_tool_mount_child(), None)
4296 .build();
4297
4298 assert!(!main.has_tools());
4299 assert!(main.has_resources());
4300 assert!(
4301 main.extension_registry_receipt().is_none(),
4302 "rejecting a child must not adopt its Apps extension runtime"
4303 );
4304
4305 let router = main.into_router();
4306 assert_eq!(router.tools_count(), 0, "the rejected child adds no tools");
4307 assert_eq!(
4308 router.resources_count(),
4309 1,
4310 "the rejected child leaves existing destination resources unchanged"
4311 );
4312 }
4313
4314 #[cfg(feature = "apps")]
4315 #[test]
4316 fn builder_mount_tools_accepts_apps_bound_child_with_active_apps_opt_in() {
4317 let resource = McpAppsUiResource::try_new(
4318 AbsoluteUri::parse("ui://mount/dashboard").expect("fixed Apps mount URI is valid"),
4319 "mounted-apps-dashboard",
4320 "<main>Apps mount fixture</main>",
4321 )
4322 .expect("fixed Apps mount resource is valid");
4323 let main = ServerBuilder::new("main", "1.0")
4324 .mcp_apps()
4325 .expect("parent Apps extension installs")
4326 .mcp_apps_ui_resource(resource)
4327 .expect("parent Apps resource registers")
4328 .mount_tools(apps_mount_child(), None)
4329 .build();
4330
4331 assert!(main.has_tools());
4332 assert!(main.has_resources());
4333 assert_eq!(
4334 main.extension_registry_receipt()
4335 .expect("parent retains its Apps extension runtime")
4336 .descriptor_count(),
4337 1,
4338 "mounting does not transfer or merge the child extension runtime"
4339 );
4340
4341 let router = main.into_router();
4342 assert_eq!(router.tools_count(), 1);
4343 assert_eq!(router.resources_count(), 1);
4344 }
4345
4346 #[cfg(feature = "apps")]
4347 #[test]
4348 fn builder_mount_resources_rejects_apps_bound_child_without_apps_opt_in_atomically() {
4349 let main = ServerBuilder::new("main", "1.0")
4350 .tool(TestTool)
4351 .mount_resources(apps_mount_child(), None)
4352 .build();
4353
4354 assert!(main.has_tools());
4355 assert!(!main.has_resources());
4356 assert!(
4357 main.extension_registry_receipt().is_none(),
4358 "rejecting a child must not adopt its Apps extension runtime"
4359 );
4360
4361 let router = main.into_router();
4362 assert_eq!(
4363 router.tools_count(),
4364 1,
4365 "the rejected child leaves existing destination tools unchanged"
4366 );
4367 assert_eq!(
4368 router.resources_count(),
4369 0,
4370 "the rejected child adds no resources"
4371 );
4372 }
4373
4374 #[cfg(feature = "apps")]
4375 #[test]
4376 fn builder_mount_resources_accepts_apps_bound_child_with_active_apps_opt_in() {
4377 let main = ServerBuilder::new("main", "1.0")
4378 .mcp_apps()
4379 .expect("parent Apps extension installs")
4380 .mount_resources(apps_mount_child(), None)
4381 .build();
4382
4383 assert!(!main.has_tools());
4384 assert!(main.has_resources());
4385 assert_eq!(
4386 main.extension_registry_receipt()
4387 .expect("parent retains its Apps extension runtime")
4388 .descriptor_count(),
4389 1,
4390 "mounting does not transfer or merge the child extension runtime"
4391 );
4392
4393 let router = main.into_router();
4394 assert_eq!(router.tools_count(), 0);
4395 assert_eq!(router.resources_count(), 1);
4396 }
4397
4398 #[test]
4399 fn builder_mount_tools_only() {
4400 let source = ServerBuilder::new("sub", "1.0")
4401 .tool(TestTool)
4402 .resource(TestResource)
4403 .prompt(TestPrompt)
4404 .build();
4405
4406 let main = ServerBuilder::new("main", "1.0")
4407 .mount_tools(source, Some("sub"))
4408 .build();
4409
4410 assert!(main.has_tools());
4411 assert!(!main.has_resources());
4413 assert!(!main.has_prompts());
4414 }
4415
4416 #[test]
4417 fn builder_mount_resources_only() {
4418 let source = ServerBuilder::new("sub", "1.0")
4419 .tool(TestTool)
4420 .resource(TestResource)
4421 .prompt(TestPrompt)
4422 .build();
4423
4424 let main = ServerBuilder::new("main", "1.0")
4425 .mount_resources(source, Some("data"))
4426 .build();
4427
4428 assert!(!main.has_tools());
4429 assert!(main.has_resources());
4430 assert!(!main.has_prompts());
4431 }
4432
4433 #[test]
4434 fn builder_mount_prompts_only() {
4435 let source = ServerBuilder::new("sub", "1.0")
4436 .tool(TestTool)
4437 .resource(TestResource)
4438 .prompt(TestPrompt)
4439 .build();
4440
4441 let main = ServerBuilder::new("main", "1.0")
4442 .mount_prompts(source, Some("tmpl"))
4443 .build();
4444
4445 assert!(!main.has_tools());
4446 assert!(!main.has_resources());
4447 assert!(main.has_prompts());
4448 }
4449
4450 #[test]
4451 fn builder_mount_empty_server() {
4452 let source = ServerBuilder::new("empty", "1.0").build();
4453
4454 let main = ServerBuilder::new("main", "1.0")
4455 .mount(source, Some("empty"))
4456 .build();
4457
4458 assert!(!main.has_tools());
4459 assert!(!main.has_resources());
4460 assert!(!main.has_prompts());
4461 }
4462
4463 #[test]
4464 fn builder_full_mount_honors_duplicate_policy_for_all_component_kinds() {
4465 for behavior in [
4466 DuplicateBehavior::Warn,
4467 DuplicateBehavior::Ignore,
4468 DuplicateBehavior::Replace,
4469 DuplicateBehavior::Error,
4470 ] {
4471 let server = marked_builder("original")
4472 .on_duplicate(behavior)
4473 .mount(marked_builder("incoming").build(), None)
4474 .build();
4475 assert_marked_server(
4476 &server,
4477 if behavior == DuplicateBehavior::Replace {
4478 "incoming"
4479 } else {
4480 "original"
4481 },
4482 );
4483 }
4484 }
4485
4486 #[test]
4487 fn builder_partial_mounts_honor_duplicate_policy() {
4488 for behavior in [
4489 DuplicateBehavior::Warn,
4490 DuplicateBehavior::Ignore,
4491 DuplicateBehavior::Replace,
4492 DuplicateBehavior::Error,
4493 ] {
4494 let replacement_marker = if behavior == DuplicateBehavior::Replace {
4495 "incoming"
4496 } else {
4497 "original"
4498 };
4499
4500 let tools = marked_builder("original")
4501 .on_duplicate(behavior)
4502 .mount_tools(marked_builder("incoming").build(), None)
4503 .build();
4504 assert_eq!(tools.tools()[0].tags, vec![replacement_marker.to_string()]);
4505 assert_eq!(tools.resources()[0].tags, vec!["original".to_string()]);
4506 assert_eq!(
4507 tools.resource_templates()[0].tags,
4508 vec!["original".to_string()]
4509 );
4510 assert_eq!(tools.prompts()[0].tags, vec!["original".to_string()]);
4511
4512 let resources = marked_builder("original")
4513 .on_duplicate(behavior)
4514 .mount_resources(marked_builder("incoming").build(), None)
4515 .build();
4516 assert_eq!(resources.tools()[0].tags, vec!["original".to_string()]);
4517 assert_eq!(
4518 resources.resources()[0].tags,
4519 vec![replacement_marker.to_string()]
4520 );
4521 assert_eq!(
4522 resources.resource_templates()[0].tags,
4523 vec![replacement_marker.to_string()]
4524 );
4525 assert_eq!(resources.prompts()[0].tags, vec!["original".to_string()]);
4526
4527 let prompts = marked_builder("original")
4528 .on_duplicate(behavior)
4529 .mount_prompts(marked_builder("incoming").build(), None)
4530 .build();
4531 assert_eq!(prompts.tools()[0].tags, vec!["original".to_string()]);
4532 assert_eq!(prompts.resources()[0].tags, vec!["original".to_string()]);
4533 assert_eq!(
4534 prompts.resource_templates()[0].tags,
4535 vec!["original".to_string()]
4536 );
4537 assert_eq!(
4538 prompts.prompts()[0].tags,
4539 vec![replacement_marker.to_string()]
4540 );
4541 }
4542 }
4543
4544 #[test]
4545 fn builder_full_and_partial_mounts_reject_invalid_prefixes() {
4546 let full = marked_builder("original")
4547 .on_duplicate(DuplicateBehavior::Replace)
4548 .mount(marked_builder("incoming").build(), Some("peer/secret"))
4549 .build();
4550 assert_marked_server(&full, "original");
4551
4552 let tools = marked_builder("original")
4553 .on_duplicate(DuplicateBehavior::Replace)
4554 .mount_tools(marked_builder("incoming").build(), Some("peer/secret"))
4555 .build();
4556 assert_marked_server(&tools, "original");
4557
4558 let resources = marked_builder("original")
4559 .on_duplicate(DuplicateBehavior::Replace)
4560 .mount_resources(marked_builder("incoming").build(), Some("peer/secret"))
4561 .build();
4562 assert_marked_server(&resources, "original");
4563
4564 let prompts = marked_builder("original")
4565 .on_duplicate(DuplicateBehavior::Replace)
4566 .mount_prompts(marked_builder("incoming").build(), Some("peer/secret"))
4567 .build();
4568 assert_marked_server(&prompts, "original");
4569 }
4570
4571 #[cfg(feature = "proxy")]
4572 mod proxy_registration_tests {
4573 use super::*;
4574
4575 struct CompletionProxyBackend {
4578 supported: bool,
4579 result: CoreResult,
4580 calls: Arc<Mutex<Vec<fastmcp_client::CompletionParams>>>,
4581 }
4582
4583 struct LocalFinalCompletionPrompt;
4584
4585 impl crate::PromptHandler for LocalFinalCompletionPrompt {
4586 fn definition(&self) -> Prompt {
4587 Prompt {
4588 name: "final-deploy".to_owned(),
4589 description: Some("local completion target".to_owned()),
4590 arguments: vec![fastmcp_protocol::PromptArgument {
4591 name: "environment".to_owned(),
4592 description: None,
4593 required: false,
4594 }],
4595 icon: None,
4596 version: None,
4597 tags: Vec::new(),
4598 }
4599 }
4600
4601 fn final_definition(&self) -> Option<fastmcp_protocol::FinalPrompt> {
4602 Some(fastmcp_protocol::FinalPrompt {
4603 name: "final-deploy".to_owned(),
4604 title: Some("Local Final Deploy".to_owned()),
4605 description: Some("local completion target".to_owned()),
4606 icons: None,
4607 arguments: Some(vec![fastmcp_protocol::FinalPromptArgument {
4608 name: "environment".to_owned(),
4609 title: Some("Local Environment".to_owned()),
4610 description: None,
4611 required: Some(false),
4612 }]),
4613 meta: None,
4614 })
4615 }
4616
4617 fn get(
4618 &self,
4619 _ctx: &McpContext,
4620 _args: std::collections::HashMap<String, String>,
4621 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
4622 Ok(Vec::new())
4623 }
4624 }
4625
4626 impl crate::proxy::ProxyBackend for CompletionProxyBackend {
4627 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
4628 Ok(Vec::new())
4629 }
4630
4631 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
4632 Ok(Vec::new())
4633 }
4634
4635 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
4636 Ok(Vec::new())
4637 }
4638
4639 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
4640 Ok(Vec::new())
4641 }
4642
4643 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
4644 Ok(Vec::new())
4645 }
4646
4647 fn call_tool_with_progress(
4648 &mut self,
4649 _: &str,
4650 _: serde_json::Value,
4651 _: crate::proxy::ProgressCallback<'_>,
4652 ) -> McpResult<Vec<Content>> {
4653 Ok(Vec::new())
4654 }
4655
4656 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
4657 Ok(Vec::new())
4658 }
4659
4660 fn get_prompt(
4661 &mut self,
4662 _: &str,
4663 _: std::collections::HashMap<String, String>,
4664 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
4665 Ok(Vec::new())
4666 }
4667
4668 fn supports_completion(&mut self) -> McpResult<bool> {
4669 Ok(self.supported)
4670 }
4671
4672 fn complete_result(
4673 &mut self,
4674 params: fastmcp_client::CompletionParams,
4675 ) -> McpResult<CoreResult> {
4676 self.calls
4677 .lock()
4678 .expect("completion proxy call log is not poisoned")
4679 .push(params);
4680 Ok(self.result.clone())
4681 }
4682 }
4683
4684 fn legacy_completion_proxy_catalog() -> ProxyTypedCatalog {
4685 ProxyTypedCatalog {
4686 tools: ProxyToolCatalog::Legacy(Vec::new()),
4687 resources: ProxyResourceCatalog::Legacy(Vec::new()),
4688 resource_templates: ProxyResourceTemplateCatalog::Legacy(Vec::new()),
4689 prompts: ProxyPromptCatalog::Legacy(vec![Prompt {
4690 name: "legacy-deploy".to_owned(),
4691 description: None,
4692 arguments: Vec::new(),
4693 icon: None,
4694 version: None,
4695 tags: Vec::new(),
4696 }]),
4697 }
4698 }
4699
4700 fn final_completion_proxy_catalog() -> ProxyTypedCatalog {
4701 let prompt = serde_json::from_value(serde_json::json!({
4702 "name": "final-deploy",
4703 "title": "Upstream Final Deploy",
4704 "arguments": [{"name": "environment", "title": "Environment"}]
4705 }))
4706 .expect("the final completion prompt fixture is valid");
4707 ProxyTypedCatalog {
4708 tools: ProxyToolCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4709 resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4710 resource_templates: ProxyResourceTemplateCatalog::Final(ProxyFinalCatalog::new(
4711 Vec::new(),
4712 )),
4713 prompts: ProxyPromptCatalog::Final(ProxyFinalCatalog::new(vec![prompt])),
4714 }
4715 }
4716
4717 fn final_completion_proxy_template_catalog() -> ProxyTypedCatalog {
4718 let template = serde_json::from_value(serde_json::json!({
4719 "uriTemplate": "completion://{environment}",
4720 "name": "upstream-completion-template",
4721 "title": "Upstream Completion Template",
4722 }))
4723 .expect("the final completion resource-template fixture is valid");
4724 ProxyTypedCatalog {
4725 tools: ProxyToolCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4726 resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4727 resource_templates: ProxyResourceTemplateCatalog::Final(ProxyFinalCatalog::new(
4728 vec![template],
4729 )),
4730 prompts: ProxyPromptCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
4731 }
4732 }
4733
4734 fn legacy_completion_proxy_template_catalog() -> ProxyTypedCatalog {
4735 ProxyTypedCatalog {
4736 tools: ProxyToolCatalog::Legacy(Vec::new()),
4737 resources: ProxyResourceCatalog::Legacy(Vec::new()),
4738 resource_templates: ProxyResourceTemplateCatalog::Legacy(vec![ResourceTemplate {
4739 uri_template: "completion://{environment}".to_owned(),
4740 name: "upstream-completion-template".to_owned(),
4741 description: None,
4742 mime_type: None,
4743 icon: None,
4744 version: None,
4745 tags: Vec::new(),
4746 }]),
4747 prompts: ProxyPromptCatalog::Legacy(Vec::new()),
4748 }
4749 }
4750
4751 fn local_completion_template() -> ResourceTemplate {
4752 ResourceTemplate {
4753 uri_template: "completion://{environment}".to_owned(),
4754 name: "local-completion-template".to_owned(),
4755 description: Some("local completion target".to_owned()),
4756 mime_type: None,
4757 icon: None,
4758 version: None,
4759 tags: Vec::new(),
4760 }
4761 }
4762
4763 fn legacy_completion_proxy_result() -> CoreResult {
4764 CoreResult::Legacy(LegacyCoreResult::Completion(
4765 fastmcp_protocol::LegacyCompletionResult {
4766 completion: fastmcp_protocol::CompletionValues {
4767 values: vec!["legacy-staging".to_owned()],
4768 total: Some(1),
4769 has_more: Some(false),
4770 },
4771 meta: None,
4772 },
4773 ))
4774 }
4775
4776 fn final_completion_proxy_result() -> CoreResult {
4777 CoreResult::Final(FinalCoreResult::Completion {
4778 result: CompleteResult::new(
4779 fastmcp_protocol::FinalCompletionResult {
4780 completion: fastmcp_protocol::FinalCompletionValues {
4781 values: vec!["final-staging".to_owned()],
4782 total: Some(
4783 serde_json::from_str("92233720368547758081234567890")
4784 .expect("the fixed exact completion total is a JSON integer"),
4785 ),
4786 has_more: Some(false),
4787 },
4788 },
4789 ResultMeta::server_generated(
4790 Implementation::try_new("completion-upstream", "1.0")
4791 .expect("the fixed completion implementation is valid"),
4792 ),
4793 ),
4794 diagnostic: None,
4795 })
4796 }
4797
4798 fn final_completion_request(id: i64) -> JsonRpcRequest {
4799 JsonRpcRequest::new(
4800 "completion/complete",
4801 Some(serde_json::json!({
4802 "_meta": {
4803 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4804 "io.modelcontextprotocol/clientCapabilities": {},
4805 },
4806 "ref": {
4807 "type": "ref/prompt",
4808 "name": "final-deploy",
4809 "title": "Final Deploy",
4810 },
4811 "argument": {"name": "environment", "value": "sta"},
4812 "context": {"arguments": {"region": "us-east-1"}},
4813 })),
4814 id,
4815 )
4816 }
4817
4818 fn final_resource_template_completion_request(id: i64) -> JsonRpcRequest {
4819 JsonRpcRequest::new(
4820 "completion/complete",
4821 Some(serde_json::json!({
4822 "_meta": {
4823 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4824 "io.modelcontextprotocol/clientCapabilities": {},
4825 },
4826 "ref": {
4827 "type": "ref/resource",
4828 "uri": "completion://{environment}",
4829 },
4830 "argument": {"name": "environment", "value": "sta"},
4831 })),
4832 id,
4833 )
4834 }
4835
4836 fn legacy_completion_request(id: i64, reference: serde_json::Value) -> JsonRpcRequest {
4837 JsonRpcRequest::new(
4838 "completion/complete",
4839 Some(serde_json::json!({
4840 "ref": reference,
4841 "argument": {"name": "environment", "value": "sta"},
4842 })),
4843 id,
4844 )
4845 }
4846
4847 fn final_proxy_catalog() -> ProxyCatalog {
4848 ProxyCatalog {
4849 tool_catalog_era: Some(ProtocolEra::Modern2026),
4850 final_tools: vec![
4851 serde_json::from_value(serde_json::json!({
4852 "name": "weather",
4853 "title": "Weather Forecast",
4854 "description": "Returns a precise forecast.",
4855 "icons": [{
4856 "src": "https://example.test/icons/weather.svg",
4857 "mimeType": "image/svg+xml",
4858 "sizes": ["16x16", "32x32"],
4859 "theme": "light",
4860 "com.example/icon": {"retained": true}
4861 }],
4862 "inputSchema": {
4863 "type": "object",
4864 "properties": {"city": {"type": "string"}}
4865 },
4866 "outputSchema": {"type": "object"},
4867 "annotations": {
4868 "title": "Forecast",
4869 "destructiveHint": false,
4870 "idempotentHint": true,
4871 "readOnlyHint": true,
4872 "openWorldHint": false
4873 },
4874 "_meta": {"com.example/catalog": {"retained": true}}
4875 }))
4876 .expect("the exact final tool fixture is valid"),
4877 ],
4878 ..ProxyCatalog::default()
4879 }
4880 }
4881
4882 fn legacy_proxy_catalog() -> ProxyCatalog {
4883 ProxyCatalog {
4884 tool_catalog_era: Some(ProtocolEra::Legacy2024),
4885 tools: vec![Tool {
4886 name: "legacy-weather".to_owned(),
4887 description: Some("Exact legacy proxy fixture".to_owned()),
4888 input_schema: serde_json::json!({"type": "object"}),
4889 output_schema: None,
4890 icon: None,
4891 version: None,
4892 tags: Vec::new(),
4893 annotations: None,
4894 }],
4895 ..ProxyCatalog::default()
4896 }
4897 }
4898
4899 fn final_tools_list_request(id: i64) -> JsonRpcRequest {
4900 JsonRpcRequest::new(
4901 "tools/list",
4902 Some(serde_json::json!({
4903 "_meta": {
4904 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4905 "io.modelcontextprotocol/clientCapabilities": {},
4906 },
4907 })),
4908 id,
4909 )
4910 }
4911
4912 fn final_tools_call_request(
4913 name: &str,
4914 arguments: serde_json::Value,
4915 id: i64,
4916 ) -> JsonRpcRequest {
4917 JsonRpcRequest::new(
4918 "tools/call",
4919 Some(serde_json::json!({
4920 "_meta": {
4921 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
4922 "io.modelcontextprotocol/clientCapabilities": {},
4923 },
4924 "name": name,
4925 "arguments": arguments,
4926 })),
4927 id,
4928 )
4929 }
4930
4931 fn bound_proxy_client<B: crate::proxy::ProxyBackend + 'static>(
4932 backend: B,
4933 era: ProtocolEra,
4934 ) -> ProxyClient {
4935 let mut bindings = ProxyClient::upstream_binding_registry();
4936 let policy = match era {
4937 ProtocolEra::Modern2026 => ProtocolPolicy::ModernOnly,
4938 ProtocolEra::Legacy2024 => ProtocolPolicy::LegacyOnly,
4939 };
4940 let opening = match era {
4941 ProtocolEra::Modern2026 => StdioOpeningFrame::ModernRequest {
4942 protocol_version: era.version().as_str().to_owned(),
4943 },
4944 ProtocolEra::Legacy2024 => StdioOpeningFrame::LegacyInitialize,
4945 };
4946 let binding = bindings
4947 .bind_stdio(
4948 "weather-route",
4949 "stdio:weather",
4950 "final-catalog-receipt",
4951 1,
4952 policy,
4953 opening,
4954 )
4955 .expect("the route selects the requested exact era");
4956 let upstream_protocol_version = era.version().as_str().to_owned();
4957 ProxyClient::from_backend_with_upstream_binding(
4958 backend,
4959 binding,
4960 &upstream_protocol_version,
4961 )
4962 .expect("the selected upstream version matches its immutable binding")
4963 }
4964
4965 fn final_catalog_proxy_client(era: ProtocolEra) -> ProxyClient {
4966 bound_proxy_client(DuplicatePolicyProxyBackend, era)
4967 }
4968
4969 #[cfg(feature = "tasks")]
4970 fn ordinary_proxy_tasks_client(
4971 calls: Arc<Mutex<Vec<String>>>,
4972 updates: Arc<Mutex<Vec<serde_json::Value>>>,
4973 ) -> ProxyClient {
4974 let task = serde_json::from_value(serde_json::json!({
4975 "resultType": "task",
4976 "taskId": "ordinary-proxy-task-71",
4977 "status": "input_required",
4978 "createdAt": "2026-07-28T12:00:00Z",
4979 "lastUpdatedAt": "2026-07-28T12:00:00Z",
4980 "ttlMs": null,
4981 "inputRequests": {},
4982 }))
4983 .expect("the ordinary proxy Task fixture is exact");
4984 bound_proxy_client(
4985 OrdinaryProxyTasksBackend {
4986 calls,
4987 updates,
4988 task,
4989 },
4990 ProtocolEra::Modern2026,
4991 )
4992 }
4993
4994 fn discovered_duplicate_policy_proxy() -> (ProxyClient, ProxyCatalog) {
4995 let client = ProxyClient::from_backend(DuplicatePolicyProxyBackend);
4996 let catalog = client
4997 .catalog()
4998 .expect("backend discovery supplies the legacy era evidence");
4999 (client, catalog)
5000 }
5001
5002 #[derive(Clone, Copy)]
5003 enum DualEraProxyRoute {
5004 Legacy,
5005 Final,
5006 }
5007
5008 struct RecordingDualEraProxyBackend {
5009 route: DualEraProxyRoute,
5010 calls: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5011 }
5012
5013 impl RecordingDualEraProxyBackend {
5014 fn record(&self, name: &str, arguments: serde_json::Value) {
5015 self.calls
5016 .lock()
5017 .expect("the test call log lock is not poisoned")
5018 .push((name.to_owned(), arguments));
5019 }
5020 }
5021
5022 impl crate::proxy::ProxyBackend for RecordingDualEraProxyBackend {
5023 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
5024 Ok(Vec::new())
5025 }
5026
5027 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
5028 Ok(Vec::new())
5029 }
5030
5031 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
5032 Ok(Vec::new())
5033 }
5034
5035 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
5036 Ok(Vec::new())
5037 }
5038
5039 fn call_tool(
5040 &mut self,
5041 name: &str,
5042 arguments: serde_json::Value,
5043 ) -> McpResult<Vec<Content>> {
5044 self.record(name, arguments);
5045 Ok(vec![Content::text(match self.route {
5046 DualEraProxyRoute::Legacy => "bound legacy proxy",
5047 DualEraProxyRoute::Final => "bound final proxy",
5048 })])
5049 }
5050
5051 fn call_tool_with_progress(
5052 &mut self,
5053 name: &str,
5054 arguments: serde_json::Value,
5055 _: crate::proxy::ProgressCallback<'_>,
5056 ) -> McpResult<Vec<Content>> {
5057 self.call_tool(name, arguments)
5058 }
5059
5060 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
5061 Ok(Vec::new())
5062 }
5063
5064 fn get_prompt(
5065 &mut self,
5066 _: &str,
5067 _: std::collections::HashMap<String, String>,
5068 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
5069 Ok(Vec::new())
5070 }
5071
5072 fn call_tool_result(
5073 &mut self,
5074 name: &str,
5075 arguments: serde_json::Value,
5076 ) -> McpResult<CoreResult> {
5077 self.record(name, arguments);
5078 match self.route {
5079 DualEraProxyRoute::Legacy => Ok(CoreResult::Legacy(
5080 LegacyCoreResult::ToolsCall(CallToolResult {
5081 content: vec![LegacyContent::Text {
5082 text: "bound legacy proxy".to_owned(),
5083 annotations: None,
5084 additional: std::collections::BTreeMap::new(),
5085 }],
5086 is_error: false,
5087 meta: None,
5088 additional: std::collections::BTreeMap::new(),
5089 }),
5090 )),
5091 DualEraProxyRoute::Final => Ok(CoreResult::Final(FinalCoreResult::ToolsCall {
5092 result: CompleteResult::new(
5093 FinalCallToolResult {
5094 content: vec![ContentBlock::text("bound final proxy")],
5095 is_error: false,
5096 structured_content: Some(serde_json::json!({"route": "final"})),
5097 },
5098 ResultMeta::server_generated(
5099 Implementation::try_new("bound-final-upstream", "1.0")
5100 .expect("the fixed test implementation is valid"),
5101 ),
5102 ),
5103 diagnostic: None,
5104 })),
5105 }
5106 }
5107 }
5108
5109 fn dual_era_proxy_client(
5110 route: DualEraProxyRoute,
5111 calls: Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5112 ) -> ProxyClient {
5113 let era = match route {
5114 DualEraProxyRoute::Legacy => ProtocolEra::Legacy2024,
5115 DualEraProxyRoute::Final => ProtocolEra::Modern2026,
5116 };
5117 let (route_id, upstream_identity) = match route {
5118 DualEraProxyRoute::Legacy => ("dual-era-legacy-route", "stdio:dual-era-legacy"),
5119 DualEraProxyRoute::Final => ("dual-era-final-route", "stdio:dual-era-final"),
5120 };
5121 let policy = match era {
5122 ProtocolEra::Modern2026 => ProtocolPolicy::ModernOnly,
5123 ProtocolEra::Legacy2024 => ProtocolPolicy::LegacyOnly,
5124 };
5125 let opening = match era {
5126 ProtocolEra::Modern2026 => StdioOpeningFrame::ModernRequest {
5127 protocol_version: era.version().as_str().to_owned(),
5128 },
5129 ProtocolEra::Legacy2024 => StdioOpeningFrame::LegacyInitialize,
5130 };
5131 let mut bindings = ProxyClient::upstream_binding_registry();
5132 let binding = bindings
5133 .bind_stdio(
5134 route_id,
5135 upstream_identity,
5136 "dual-era-receipt",
5137 1,
5138 policy,
5139 opening,
5140 )
5141 .expect("the test route selects one immutable era");
5142 let upstream_protocol_version = era.version().as_str().to_owned();
5143 ProxyClient::from_backend_with_upstream_binding(
5144 RecordingDualEraProxyBackend { route, calls },
5145 binding,
5146 &upstream_protocol_version,
5147 )
5148 .expect("the selected upstream version matches its immutable binding")
5149 }
5150
5151 fn dual_era_proxy_server() -> (
5152 crate::Server,
5153 Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5154 Arc<Mutex<Vec<(String, serde_json::Value)>>>,
5155 ) {
5156 let legacy_calls = Arc::new(Mutex::new(Vec::new()));
5157 let final_calls = Arc::new(Mutex::new(Vec::new()));
5158 let server = ServerBuilder::new("srv", "1.0")
5159 .proxy(
5160 dual_era_proxy_client(DualEraProxyRoute::Legacy, Arc::clone(&legacy_calls)),
5161 legacy_proxy_catalog(),
5162 )
5163 .expect("the legacy proxy catalog agrees with its bound route")
5164 .proxy(
5165 dual_era_proxy_client(DualEraProxyRoute::Final, Arc::clone(&final_calls)),
5166 final_proxy_catalog(),
5167 )
5168 .expect("the final proxy catalog agrees with its bound route")
5169 .build();
5170 (server, legacy_calls, final_calls)
5171 }
5172
5173 fn initialized_legacy_proxy_session(server: &crate::Server) -> crate::Session {
5174 let mut session =
5175 crate::Session::new(server.info().clone(), server.capabilities().clone());
5176 session.initialize(
5177 fastmcp_protocol::ClientInfo {
5178 name: "dual-era-legacy-client".to_owned(),
5179 version: "1.0".to_owned(),
5180 },
5181 fastmcp_protocol::ClientCapabilities::default(),
5182 "2024-11-05".to_owned(),
5183 );
5184 session
5185 }
5186
5187 fn assert_rejected_proxy_catalog_returns_an_error(catalog: ProxyCatalog) {
5188 let error = match ServerBuilder::new("srv", "1.0").tool(TestTool).proxy(
5189 ProxyClient::from_backend(DuplicatePolicyProxyBackend),
5190 catalog,
5191 ) {
5192 Ok(_) => {
5193 panic!("the malformed or unbound proxy catalog is rejected before registration")
5194 }
5195 Err(error) => error,
5196 };
5197
5198 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
5199 }
5200
5201 #[test]
5202 fn builder_proxy_accepts_a_coherent_legacy_catalog() {
5203 let server = ServerBuilder::new("srv", "1.0")
5204 .proxy(
5205 final_catalog_proxy_client(ProtocolEra::Legacy2024),
5206 legacy_proxy_catalog(),
5207 )
5208 .expect("the coherent legacy catalog agrees with its route")
5209 .build();
5210
5211 assert!(server.has_tools());
5212 let tools = server.tools();
5213 assert_eq!(
5214 tools[0].name, "legacy-weather",
5215 "the public builder path registers a catalog whose marker and entries select legacy"
5216 );
5217 }
5218
5219 #[test]
5220 fn builder_proxy_rejects_an_unbound_caller_asserted_legacy_catalog() {
5221 let error = match ServerBuilder::new("srv", "1.0").proxy(
5222 ProxyClient::from_backend(DuplicatePolicyProxyBackend),
5223 legacy_proxy_catalog(),
5224 ) {
5225 Ok(_) => panic!("a caller-supplied catalog cannot bind an unbound proxy route"),
5226 Err(error) => error,
5227 };
5228
5229 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
5230 assert!(error.message.contains("cannot bind an unbound route"));
5231 }
5232
5233 #[test]
5234 fn builder_proxy_rejects_legacy_tools_when_only_the_marker_changes_to_modern() {
5235 let mut catalog = legacy_proxy_catalog();
5236 catalog.tool_catalog_era = Some(ProtocolEra::Modern2026);
5237
5238 assert_rejected_proxy_catalog_returns_an_error(catalog);
5239 }
5240
5241 #[test]
5242 fn builder_proxy_rejects_final_tools_when_only_the_marker_changes_to_legacy() {
5243 let mut catalog = final_proxy_catalog();
5244 catalog.tool_catalog_era = Some(ProtocolEra::Legacy2024);
5245
5246 assert_rejected_proxy_catalog_returns_an_error(catalog);
5247 }
5248
5249 #[test]
5250 fn builder_proxy_rejects_mixed_vectors_when_only_final_tools_are_added() {
5251 let mut catalog = legacy_proxy_catalog();
5252 catalog.final_tools = final_proxy_catalog().final_tools;
5253
5254 assert_rejected_proxy_catalog_returns_an_error(catalog);
5255 }
5256
5257 #[test]
5258 fn builder_proxy_rejects_a_missing_marker() {
5259 let mut catalog = legacy_proxy_catalog();
5260 catalog.tool_catalog_era = None;
5261
5262 assert_rejected_proxy_catalog_returns_an_error(catalog);
5263 }
5264
5265 #[test]
5266 fn public_builder_proxy_advertises_and_lists_the_exact_final_tool_catalog() {
5267 let catalog = final_proxy_catalog();
5268 let expected = serde_json::to_value(&catalog.final_tools[0])
5269 .expect("the final fixture serializes");
5270 let server = ServerBuilder::new("srv", "1.0")
5271 .proxy(final_catalog_proxy_client(ProtocolEra::Modern2026), catalog)
5272 .expect("the final catalog agrees with its bound route")
5273 .build();
5274
5275 assert!(server.has_tools());
5276 assert!(server.tools().is_empty());
5277 let discovery = serde_json::to_value(
5278 server
5279 .server_discovery()
5280 .expect("the public final proxy server is discoverable"),
5281 )
5282 .expect("final discovery serializes");
5283 assert_eq!(
5284 discovery.pointer("/capabilities/tools"),
5285 Some(&serde_json::json!({}))
5286 );
5287 let inbound = crate::InboundRequestContext::new(
5288 Cx::for_testing(),
5289 701,
5290 crate::InboundRequestTransport::Memory,
5291 );
5292 let response = server
5293 .dispatch_stateless(&inbound, &final_tools_list_request(701))
5294 .expect("the public server path responds to the modern tools/list request");
5295 assert!(response.error.is_none());
5296 let catalog = response
5297 .result
5298 .expect("the modern tools/list response has a result payload");
5299 assert_eq!(catalog["tools"][0], expected);
5300 }
5301
5302 #[test]
5303 fn public_builder_proxy_final_catalog_absence_changes_only_tool_advertisement_and_registry()
5304 {
5305 let mut catalog = final_proxy_catalog();
5306 let configured_tool_count = catalog.final_tools.len();
5307 catalog.final_tools.clear();
5308 assert_eq!(
5309 configured_tool_count, 1,
5310 "the planted negative removes only the final tool catalog entry"
5311 );
5312
5313 let server = ServerBuilder::new("srv", "1.0")
5314 .proxy(final_catalog_proxy_client(ProtocolEra::Modern2026), catalog)
5315 .expect("an otherwise identical empty final catalog remains admissible")
5316 .build();
5317
5318 assert!(!server.has_tools());
5319 let discovery = serde_json::to_value(
5320 server
5321 .server_discovery()
5322 .expect("the empty final proxy server remains discoverable"),
5323 )
5324 .expect("empty final discovery serializes");
5325 assert!(discovery.pointer("/capabilities/tools").is_none());
5326 let inbound = crate::InboundRequestContext::new(
5327 Cx::for_testing(),
5328 702,
5329 crate::InboundRequestTransport::Memory,
5330 );
5331 let response = server
5332 .dispatch_stateless(&inbound, &final_tools_list_request(702))
5333 .expect("the public final tools/list path responds for an empty catalog");
5334 assert_eq!(response.result, Some(serde_json::json!({"tools": []})));
5335 assert!(
5336 server.tools().is_empty(),
5337 "removing only the final catalog entry leaves no legacy registry mutation"
5338 );
5339 }
5340
5341 #[cfg(feature = "tasks")]
5342 #[test]
5343 fn public_builder_proxy_relays_modern_tasks_input_required_and_listener() {
5344 let calls = Arc::new(Mutex::new(Vec::new()));
5345 let updates = Arc::new(Mutex::new(Vec::new()));
5346 let proxy = ordinary_proxy_tasks_client(Arc::clone(&calls), Arc::clone(&updates));
5347 let server = Arc::new(
5348 ServerBuilder::new("ordinary-proxy-tasks", "1.0")
5349 .proxy(proxy.clone(), final_proxy_catalog())
5350 .expect("the ordinary public proxy path installs the admitted modern route")
5351 .build(),
5352 );
5353 let discovery = serde_json::to_value(
5354 server
5355 .server_discovery()
5356 .expect("the Tasks proxy is publicly discoverable"),
5357 )
5358 .expect("Tasks proxy discovery serializes");
5359 assert_eq!(
5360 discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
5361 Some(&serde_json::json!({})),
5362 "the ordinary proxy path advertises the same Tasks extension as the typed path"
5363 );
5364
5365 let cx = Cx::for_testing();
5366 let connection = crate::ModernConnection::new();
5367 let emitted_notifications = Arc::new(Mutex::new(Vec::<JsonRpcRequest>::new()));
5368 let notification_sender: crate::NotificationSender = {
5369 let emitted_notifications = Arc::clone(&emitted_notifications);
5370 Arc::new(move |notification| {
5371 emitted_notifications
5372 .lock()
5373 .expect("ordinary proxy emitted notification log is not poisoned")
5374 .push(notification);
5375 })
5376 };
5377 let dispatch_modern = |request_id, request| {
5378 let inbound = crate::InboundRequestContext::with_modern_connection(
5379 cx.clone(),
5380 request_id,
5381 crate::InboundRequestTransport::Memory,
5382 &connection,
5383 );
5384 block_on(Arc::clone(&server).dispatch_with_protocol_policy_owned(
5385 server.protocol_policy,
5386 &inbound,
5387 request,
5388 None,
5389 None,
5390 None,
5391 None,
5392 fastmcp_core::McpRequestCancellation::new(),
5393 None,
5394 Arc::clone(¬ification_sender),
5395 ))
5396 .expect("public ordinary proxy request receives a JSON-RPC response")
5397 };
5398 let task_parameters = serde_json::json!({
5399 "_meta": {
5400 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
5401 "io.modelcontextprotocol/clientCapabilities": {
5402 "extensions": {"io.modelcontextprotocol/tasks": {}}
5403 },
5404 },
5405 "name": "weather",
5406 "arguments": {"outcome": "task"},
5407 });
5408 let task = dispatch_modern(
5409 703,
5410 JsonRpcRequest::new("tools/call", Some(task_parameters.clone()), 703_i64),
5411 );
5412 assert_eq!(
5413 task.result
5414 .as_ref()
5415 .and_then(|result| result.get("resultType")),
5416 Some(&serde_json::json!("task"))
5417 );
5418 assert_eq!(
5419 task.result.as_ref().and_then(|result| result.get("taskId")),
5420 Some(&serde_json::json!("ordinary-proxy-task-71"))
5421 );
5422 let relay_after_task = proxy
5423 .final_task_registry_snapshot_for_test()
5424 .expect("the retained ordinary proxy exposes its route-local Task snapshot");
5425 assert_eq!(
5426 relay_after_task.pointer("/tasks/ordinary-proxy-task-71/status"),
5427 Some(&serde_json::json!("input_required")),
5428 "the public tools/call task result is retained by the ordinary route-local relay"
5429 );
5430
5431 let update = dispatch_modern(
5432 704,
5433 JsonRpcRequest::new(
5434 "tasks/update",
5435 Some(serde_json::json!({
5436 "taskId": "ordinary-proxy-task-71",
5437 "inputResponses": {},
5438 "_meta": task_parameters["_meta"].clone(),
5439 })),
5440 704_i64,
5441 ),
5442 );
5443 assert_eq!(
5444 update
5445 .result
5446 .as_ref()
5447 .and_then(|result| result.get("resultType")),
5448 Some(&serde_json::json!("complete")),
5449 "the public Tasks update preserves the upstream final result algebra"
5450 );
5451 assert_eq!(
5452 updates
5453 .lock()
5454 .expect("ordinary proxy task update receipt is not poisoned")
5455 .as_slice(),
5456 [serde_json::json!({})],
5457 "the public Tasks update reaches the upstream backend through the local relay"
5458 );
5459 assert_eq!(
5460 calls
5461 .lock()
5462 .expect("ordinary proxy task call log is not poisoned")
5463 .as_slice(),
5464 ["tools/call:weather".to_owned(), "tasks/update".to_owned()],
5465 "the public update reaches the selected upstream only after local relay admission"
5466 );
5467
5468 let mut input_required_parameters = task_parameters.clone();
5469 input_required_parameters["arguments"] = serde_json::json!({});
5470 let listened = dispatch_modern(
5471 705,
5472 JsonRpcRequest::new(
5473 "subscriptions/listen",
5474 Some(serde_json::json!({
5475 "notifications": {"taskIds": []},
5476 "_meta": task_parameters["_meta"].clone(),
5477 })),
5478 705_i64,
5479 ),
5480 );
5481 assert_eq!(
5482 listened
5483 .result
5484 .as_ref()
5485 .and_then(|result| result.get("resultType")),
5486 Some(&serde_json::json!("complete"))
5487 );
5488 let emitted_before_rejection = emitted_notifications
5489 .lock()
5490 .expect("ordinary proxy emitted notification log is not poisoned")
5491 .clone();
5492 assert_eq!(
5493 emitted_before_rejection.len(),
5494 1,
5495 "the public listener emits its acknowledgement before terminal completion"
5496 );
5497 assert_eq!(
5498 emitted_before_rejection
5499 .first()
5500 .map(|notification| notification.method.as_str()),
5501 Some("notifications/subscriptions/acknowledged"),
5502 "the captured local notification is the listener acknowledgement"
5503 );
5504 let calls_before_rejection = calls
5505 .lock()
5506 .expect("ordinary proxy task call log is not poisoned")
5507 .clone();
5508 let relay_before_rejection = proxy
5509 .final_task_registry_snapshot_for_test()
5510 .expect("the ordinary proxy keeps its local final Task registry");
5511 let subscriptions_before_rejection = server.final_subscription_snapshot_for_test();
5512 let mut missing_capability = task_parameters;
5513 assert!(
5514 missing_capability
5515 .pointer_mut("/_meta/io.modelcontextprotocol~1clientCapabilities/extensions")
5516 .and_then(serde_json::Value::as_object_mut)
5517 .expect("the admitted request has an extension map")
5518 .remove("io.modelcontextprotocol/tasks")
5519 .is_some(),
5520 "the RH-5 negative changes only the client Tasks capability"
5521 );
5522 let rejected = dispatch_modern(
5523 706,
5524 JsonRpcRequest::new("tools/call", Some(missing_capability), 706_i64),
5525 );
5526 assert!(rejected.error.is_some());
5527 assert_eq!(
5528 calls
5529 .lock()
5530 .expect("ordinary proxy task call log is not poisoned")
5531 .clone(),
5532 calls_before_rejection,
5533 "the one-field capability rejection must not invoke or mutate the upstream route"
5534 );
5535 assert_eq!(
5536 proxy
5537 .final_task_registry_snapshot_for_test()
5538 .expect("the ordinary proxy keeps its local final Task registry"),
5539 relay_before_rejection,
5540 "the one-field capability rejection must not mutate the local relay task registry"
5541 );
5542 assert_eq!(
5543 server.final_subscription_snapshot_for_test(),
5544 subscriptions_before_rejection,
5545 "the one-field capability rejection must not mutate local subscription delivery state"
5546 );
5547 assert_eq!(
5548 emitted_notifications
5549 .lock()
5550 .expect("ordinary proxy emitted notification log is not poisoned")
5551 .clone(),
5552 emitted_before_rejection,
5553 "the one-field capability rejection must not emit or alter local notification state"
5554 );
5555
5556 let input_required = dispatch_modern(
5557 707,
5558 JsonRpcRequest::new("tools/call", Some(input_required_parameters), 707_i64),
5559 );
5560 assert_eq!(
5561 input_required
5562 .result
5563 .as_ref()
5564 .and_then(|result| result.get("resultType")),
5565 Some(&serde_json::json!("input_required"))
5566 );
5567 assert_ne!(
5568 input_required
5569 .result
5570 .as_ref()
5571 .and_then(|result| result.get("requestState")),
5572 Some(&serde_json::json!("upstream-forged-state")),
5573 "the downstream router must mint rather than replay upstream MRTR state"
5574 );
5575
5576 assert_eq!(
5577 calls
5578 .lock()
5579 .expect("ordinary proxy task call log is not poisoned")
5580 .as_slice(),
5581 [
5582 "tools/call:weather".to_owned(),
5583 "tasks/update".to_owned(),
5584 "subscriptions/listen".to_owned(),
5585 "tools/call:weather".to_owned(),
5586 ],
5587 "public task update, listener, and input_required requests stay on the one admitted modern upstream"
5588 );
5589 }
5590
5591 #[test]
5592 fn builder_proxy_dual_era_preserves_final_catalog_and_routes_bound_calls() {
5593 let expected_final_tool = serde_json::to_value(&final_proxy_catalog().final_tools[0])
5594 .expect("the final fixture serializes");
5595 let (server, legacy_calls, final_calls) = dual_era_proxy_server();
5596 let mut legacy_session = initialized_legacy_proxy_session(&server);
5597 let notification_sender: crate::NotificationSender = Arc::new(|_| {});
5598 let request_sender = crate::RequestSender::new(
5599 Arc::new(crate::PendingRequests::new()),
5600 Arc::new(|message| {
5601 Err(format!("unexpected outbound message in test: {message:?}"))
5602 }),
5603 );
5604
5605 let legacy_catalog = server
5606 .dispatch_request(
5607 &Cx::for_testing(),
5608 &mut legacy_session,
5609 JsonRpcRequest::new("tools/list", Some(serde_json::json!({})), 801_i64),
5610 ¬ification_sender,
5611 &request_sender,
5612 )
5613 .expect("the legacy tools/list request receives a response")
5614 .result
5615 .expect("the legacy tools/list response has a result payload");
5616 assert_eq!(legacy_catalog["tools"].as_array().map(Vec::len), Some(1));
5617 assert_eq!(legacy_catalog["tools"][0]["name"], "legacy-weather");
5618
5619 let final_inbound = crate::InboundRequestContext::new(
5620 Cx::for_testing(),
5621 802,
5622 crate::InboundRequestTransport::Memory,
5623 );
5624 let final_catalog = server
5625 .dispatch_stateless(&final_inbound, &final_tools_list_request(802_i64))
5626 .expect("the final tools/list request receives a response")
5627 .result
5628 .expect("the final tools/list response has a result payload");
5629 assert_eq!(final_catalog["tools"].as_array().map(Vec::len), Some(1));
5630 assert_eq!(final_catalog["tools"][0]["name"], "weather");
5631 assert_eq!(
5632 serde_json::to_vec(&final_catalog["tools"][0])
5633 .expect("the emitted final tool normalizes to JSON"),
5634 serde_json::to_vec(&expected_final_tool)
5635 .expect("the exact final fixture normalizes to JSON"),
5636 "the final proxy path retains the full normalized FinalTool model"
5637 );
5638
5639 let legacy_call = server
5640 .dispatch_request(
5641 &Cx::for_testing(),
5642 &mut legacy_session,
5643 JsonRpcRequest::new(
5644 "tools/call",
5645 Some(serde_json::json!({
5646 "name": "legacy-weather",
5647 "arguments": {"city": "Portland"},
5648 })),
5649 803_i64,
5650 ),
5651 ¬ification_sender,
5652 &request_sender,
5653 )
5654 .expect("the legacy tools/call request receives a response")
5655 .result
5656 .expect("the legacy tools/call response has a result payload");
5657 assert_eq!(legacy_call["content"][0]["text"], "bound legacy proxy");
5658
5659 let final_call_inbound = crate::InboundRequestContext::new(
5660 Cx::for_testing(),
5661 804,
5662 crate::InboundRequestTransport::Memory,
5663 );
5664 let final_call = server
5665 .dispatch_stateless(
5666 &final_call_inbound,
5667 &final_tools_call_request(
5668 "weather",
5669 serde_json::json!({"city": "Boston"}),
5670 804,
5671 ),
5672 )
5673 .expect("the final tools/call request receives a response")
5674 .result
5675 .expect("the final tools/call response has a result payload");
5676 assert_eq!(final_call["resultType"], "complete");
5677 assert_eq!(final_call["content"][0]["text"], "bound final proxy");
5678 assert_eq!(
5679 final_call["structuredContent"],
5680 serde_json::json!({"route": "final"})
5681 );
5682
5683 assert_eq!(
5684 legacy_calls
5685 .lock()
5686 .expect("the test call log lock is not poisoned")
5687 .clone(),
5688 vec![(
5689 "legacy-weather".to_owned(),
5690 serde_json::json!({"city": "Portland"}),
5691 )],
5692 "the legacy request reaches only its bound legacy upstream"
5693 );
5694 assert_eq!(
5695 final_calls
5696 .lock()
5697 .expect("the test call log lock is not poisoned")
5698 .clone(),
5699 vec![("weather".to_owned(), serde_json::json!({"city": "Boston"}))],
5700 "the final request reaches only its bound final upstream"
5701 );
5702 }
5703
5704 #[test]
5705 fn builder_proxy_dual_era_rejects_cross_era_names_without_upstream_calls() {
5706 let (server, legacy_calls, final_calls) = dual_era_proxy_server();
5707 let mut legacy_session = initialized_legacy_proxy_session(&server);
5708 let notification_sender: crate::NotificationSender = Arc::new(|_| {});
5709 let request_sender = crate::RequestSender::new(
5710 Arc::new(crate::PendingRequests::new()),
5711 Arc::new(|message| {
5712 Err(format!("unexpected outbound message in test: {message:?}"))
5713 }),
5714 );
5715
5716 let legacy_rejected = server
5717 .dispatch_request(
5718 &Cx::for_testing(),
5719 &mut legacy_session,
5720 JsonRpcRequest::new(
5721 "tools/call",
5722 Some(serde_json::json!({
5723 "name": "weather",
5724 "arguments": {},
5725 })),
5726 805_i64,
5727 ),
5728 ¬ification_sender,
5729 &request_sender,
5730 )
5731 .expect("the legacy cross-era request receives a JSON-RPC error");
5732 assert!(legacy_rejected.result.is_none());
5733 assert_eq!(
5734 legacy_rejected.error.and_then(|error| error.code.as_i32()),
5735 Some(-32601),
5736 "the final-only name is absent from the legacy call route"
5737 );
5738
5739 let final_inbound = crate::InboundRequestContext::new(
5740 Cx::for_testing(),
5741 806,
5742 crate::InboundRequestTransport::Memory,
5743 );
5744 let final_rejected = server
5745 .dispatch_stateless(
5746 &final_inbound,
5747 &final_tools_call_request("legacy-weather", serde_json::json!({}), 806),
5748 )
5749 .expect("the final cross-era request receives a JSON-RPC error");
5750 assert!(final_rejected.result.is_none());
5751 assert_eq!(
5752 final_rejected.error.and_then(|error| error.code.as_i32()),
5753 Some(-32602),
5754 "the legacy-only name is absent from the final call route"
5755 );
5756
5757 assert!(
5758 legacy_calls
5759 .lock()
5760 .expect("the test call log lock is not poisoned")
5761 .is_empty(),
5762 "the final-only name must be rejected before the legacy upstream is called"
5763 );
5764 assert!(
5765 final_calls
5766 .lock()
5767 .expect("the test call log lock is not poisoned")
5768 .is_empty(),
5769 "the legacy-only name must be rejected before the final upstream is called"
5770 );
5771 }
5772
5773 #[test]
5774 fn public_proxy_completion_forwards_exact_legacy_and_final_results() {
5775 let legacy_calls = Arc::new(Mutex::new(Vec::new()));
5776 let final_calls = Arc::new(Mutex::new(Vec::new()));
5777 let server = ServerBuilder::new("completion-proxy", "1.0")
5778 .proxy_typed(
5779 bound_proxy_client(
5780 CompletionProxyBackend {
5781 supported: true,
5782 result: legacy_completion_proxy_result(),
5783 calls: Arc::clone(&legacy_calls),
5784 },
5785 ProtocolEra::Legacy2024,
5786 ),
5787 legacy_completion_proxy_catalog(),
5788 )
5789 .expect("the exact legacy completion proxy installs")
5790 .proxy_typed(
5791 bound_proxy_client(
5792 CompletionProxyBackend {
5793 supported: true,
5794 result: final_completion_proxy_result(),
5795 calls: Arc::clone(&final_calls),
5796 },
5797 ProtocolEra::Modern2026,
5798 ),
5799 final_completion_proxy_catalog(),
5800 )
5801 .expect("the exact final completion proxy installs")
5802 .build();
5803 assert!(
5804 server.capabilities().completions.is_some(),
5805 "installing a proxied completion provider must advertise initialize completions"
5806 );
5807
5808 let mut legacy_session = initialized_legacy_proxy_session(&server);
5809 let notification_sender: crate::NotificationSender = Arc::new(|_| {});
5810 let request_sender = crate::RequestSender::new(
5811 Arc::new(crate::PendingRequests::new()),
5812 Arc::new(|message| {
5813 Err(format!("unexpected outbound message in test: {message:?}"))
5814 }),
5815 );
5816 let legacy = server
5817 .dispatch_request(
5818 &Cx::for_testing(),
5819 &mut legacy_session,
5820 JsonRpcRequest::new(
5821 "completion/complete",
5822 Some(serde_json::json!({
5823 "ref": {"type": "ref/prompt", "name": "legacy-deploy"},
5824 "argument": {"name": "environment", "value": "sta"},
5825 })),
5826 821_i64,
5827 ),
5828 ¬ification_sender,
5829 &request_sender,
5830 )
5831 .expect("the public exact-2024 completion path returns a response")
5832 .result
5833 .expect("the exact-2024 completion response has a result payload");
5834 assert_eq!(
5835 legacy["completion"]["values"],
5836 serde_json::json!(["legacy-staging"])
5837 );
5838
5839 let final_inbound = crate::InboundRequestContext::new(
5840 Cx::for_testing(),
5841 822,
5842 crate::InboundRequestTransport::Memory,
5843 );
5844 let final_response = server
5845 .dispatch_stateless(&final_inbound, &final_completion_request(822))
5846 .expect("the public final completion path returns a response")
5847 .result
5848 .expect("the final completion response has a result payload");
5849 assert_eq!(final_response["resultType"], "complete");
5850 assert_eq!(
5851 final_response["completion"]["values"],
5852 serde_json::json!(["final-staging"])
5853 );
5854 assert_eq!(
5855 final_response["completion"]["total"],
5856 serde_json::json!(92233720368547758081234567890_u128),
5857 "the proxy retains the final arbitrary-precision completion total"
5858 );
5859
5860 let legacy_calls = legacy_calls
5861 .lock()
5862 .expect("legacy completion call log is not poisoned");
5863 assert_eq!(legacy_calls.len(), 1);
5864 assert!(matches!(
5865 &legacy_calls[0].reference,
5866 fastmcp_client::CompletionReference::Prompt { name } if name == "legacy-deploy"
5867 ));
5868 assert!(legacy_calls[0].context.is_none());
5869 let final_calls = final_calls
5870 .lock()
5871 .expect("final completion call log is not poisoned");
5872 assert_eq!(final_calls.len(), 1);
5873 assert!(matches!(
5874 &final_calls[0].reference,
5875 fastmcp_client::CompletionReference::PromptWithTitle { name, title }
5876 if name == "final-deploy" && title == "Final Deploy"
5877 ));
5878 assert_eq!(
5879 final_calls[0]
5880 .context
5881 .as_ref()
5882 .and_then(|context| context.arguments.as_ref())
5883 .and_then(|arguments| arguments.get("region")),
5884 Some(&"us-east-1".to_owned()),
5885 "the proxy forwards final completion context unchanged"
5886 );
5887 }
5888
5889 #[test]
5890 fn public_proxy_completion_unsupported_upstream_rejects_without_downstream_mutation() {
5891 let calls = Arc::new(Mutex::new(Vec::new()));
5892 let server = ServerBuilder::new("completion-proxy", "1.0")
5893 .proxy_typed(
5894 bound_proxy_client(
5895 CompletionProxyBackend {
5896 supported: false,
5897 result: final_completion_proxy_result(),
5898 calls: Arc::clone(&calls),
5899 },
5900 ProtocolEra::Modern2026,
5901 ),
5902 final_completion_proxy_catalog(),
5903 )
5904 .expect("changing only upstream completion support preserves catalog registration")
5905 .build();
5906 assert!(
5907 server.capabilities().completions.is_none(),
5908 "an unsupported upstream must not advertise initialize completions"
5909 );
5910 let discovery = serde_json::to_value(
5911 server
5912 .server_discovery()
5913 .expect("the final proxy server remains discoverable"),
5914 )
5915 .expect("discovery serializes");
5916 assert!(
5917 discovery["capabilities"].get("completions").is_none(),
5918 "an unsupported upstream must not create a downstream completion claim"
5919 );
5920
5921 let before_inbound = crate::InboundRequestContext::new(
5922 Cx::for_testing(),
5923 823,
5924 crate::InboundRequestTransport::Memory,
5925 );
5926 let before = server
5927 .dispatch_stateless(
5928 &before_inbound,
5929 &JsonRpcRequest::new(
5930 "prompts/list",
5931 Some(serde_json::json!({
5932 "_meta": {
5933 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
5934 "io.modelcontextprotocol/clientCapabilities": {},
5935 },
5936 })),
5937 823_i64,
5938 ),
5939 )
5940 .expect("the proxied final prompt catalog is public before rejection")
5941 .result
5942 .expect("the proxied final prompt catalog has a result payload");
5943 let rejected_inbound = crate::InboundRequestContext::new(
5944 Cx::for_testing(),
5945 824,
5946 crate::InboundRequestTransport::Memory,
5947 );
5948 let rejected = server
5949 .dispatch_stateless(&rejected_inbound, &final_completion_request(824))
5950 .expect("the unsupported completion request receives a JSON-RPC error");
5951 assert_eq!(
5952 rejected.error.and_then(|error| error.code.as_i32()),
5953 Some(-32601),
5954 "the absent local completion handler rejects before an upstream call"
5955 );
5956 assert!(rejected.result.is_none());
5957 let after_inbound = crate::InboundRequestContext::new(
5958 Cx::for_testing(),
5959 825,
5960 crate::InboundRequestTransport::Memory,
5961 );
5962 let after = server
5963 .dispatch_stateless(
5964 &after_inbound,
5965 &JsonRpcRequest::new(
5966 "prompts/list",
5967 Some(serde_json::json!({
5968 "_meta": {
5969 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
5970 "io.modelcontextprotocol/clientCapabilities": {},
5971 },
5972 })),
5973 825_i64,
5974 ),
5975 )
5976 .expect("the proxied final prompt catalog remains public after rejection")
5977 .result
5978 .expect("the proxied final prompt catalog still has a result payload");
5979 assert_eq!(
5980 before, after,
5981 "the rejected request cannot mutate downstream catalog state"
5982 );
5983 assert!(
5984 calls
5985 .lock()
5986 .expect("completion proxy call log is not poisoned")
5987 .is_empty(),
5988 "the unsupported upstream is never invoked"
5989 );
5990 }
5991
5992 #[test]
5993 fn public_proxy_completion_duplicate_local_target_keeps_no_upstream_mapping() {
5994 for duplicate_behavior in [DuplicateBehavior::Warn, DuplicateBehavior::Ignore] {
5995 let calls = Arc::new(Mutex::new(Vec::new()));
5996 let server = ServerBuilder::new("completion-proxy", "1.0")
5997 .prompt(LocalFinalCompletionPrompt)
5998 .on_duplicate(duplicate_behavior)
5999 .proxy_typed(
6000 bound_proxy_client(
6001 CompletionProxyBackend {
6002 supported: true,
6003 result: final_completion_proxy_result(),
6004 calls: Arc::clone(&calls),
6005 },
6006 ProtocolEra::Modern2026,
6007 ),
6008 final_completion_proxy_catalog(),
6009 )
6010 .expect("retaining the local final prompt is a successful duplicate admission")
6011 .build();
6012
6013 let discovery = serde_json::to_value(
6014 server
6015 .server_discovery()
6016 .expect("the retained local prompt server remains discoverable"),
6017 )
6018 .expect("discovery serializes");
6019 assert!(
6020 discovery["capabilities"].get("completions").is_none(),
6021 "{duplicate_behavior:?} must not advertise an upstream provider for a retained local target"
6022 );
6023
6024 let before_inbound = crate::InboundRequestContext::new(
6025 Cx::for_testing(),
6026 826,
6027 crate::InboundRequestTransport::Memory,
6028 );
6029 let before = server
6030 .dispatch_stateless(
6031 &before_inbound,
6032 &JsonRpcRequest::new(
6033 "prompts/list",
6034 Some(serde_json::json!({
6035 "_meta": {
6036 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6037 "io.modelcontextprotocol/clientCapabilities": {},
6038 },
6039 })),
6040 826_i64,
6041 ),
6042 )
6043 .expect("the retained local prompt is visible through the public final list")
6044 .result
6045 .expect("the retained local prompt list has a result payload");
6046 assert_eq!(
6047 before["prompts"][0]["title"], "Local Final Deploy",
6048 "{duplicate_behavior:?} retains the pre-existing local prompt target"
6049 );
6050
6051 let rejected_inbound = crate::InboundRequestContext::new(
6052 Cx::for_testing(),
6053 827,
6054 crate::InboundRequestTransport::Memory,
6055 );
6056 let rejected = server
6057 .dispatch_stateless(&rejected_inbound, &final_completion_request(827))
6058 .expect("the absent completion mapping returns a JSON-RPC error");
6059 assert_eq!(
6060 rejected.error.and_then(|error| error.code.as_i32()),
6061 Some(-32601),
6062 "{duplicate_behavior:?} rejects before invocation because no upstream mapping was installed"
6063 );
6064 assert!(rejected.result.is_none());
6065
6066 let after_inbound = crate::InboundRequestContext::new(
6067 Cx::for_testing(),
6068 828,
6069 crate::InboundRequestTransport::Memory,
6070 );
6071 let after = server
6072 .dispatch_stateless(
6073 &after_inbound,
6074 &JsonRpcRequest::new(
6075 "prompts/list",
6076 Some(serde_json::json!({
6077 "_meta": {
6078 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6079 "io.modelcontextprotocol/clientCapabilities": {},
6080 },
6081 })),
6082 828_i64,
6083 ),
6084 )
6085 .expect("the rejected request leaves the retained local target public")
6086 .result
6087 .expect("the retained local prompt list still has a result payload");
6088 assert_eq!(
6089 before, after,
6090 "{duplicate_behavior:?} completion rejection cannot mutate the retained local target"
6091 );
6092 assert!(
6093 calls
6094 .lock()
6095 .expect("completion proxy call log is not poisoned")
6096 .is_empty(),
6097 "{duplicate_behavior:?} must not invoke the upstream completion backend"
6098 );
6099 }
6100 }
6101
6102 #[test]
6103 fn public_proxy_completion_replace_installs_upstream_mapping_for_replaced_target() {
6104 let calls = Arc::new(Mutex::new(Vec::new()));
6105 let server = ServerBuilder::new("completion-proxy", "1.0")
6106 .prompt(LocalFinalCompletionPrompt)
6107 .on_duplicate(DuplicateBehavior::Replace)
6108 .proxy_typed(
6109 bound_proxy_client(
6110 CompletionProxyBackend {
6111 supported: true,
6112 result: final_completion_proxy_result(),
6113 calls: Arc::clone(&calls),
6114 },
6115 ProtocolEra::Modern2026,
6116 ),
6117 final_completion_proxy_catalog(),
6118 )
6119 .expect("Replace admits the exact upstream final prompt target")
6120 .build();
6121
6122 let discovery = serde_json::to_value(
6123 server
6124 .server_discovery()
6125 .expect("the replacement proxy server remains discoverable"),
6126 )
6127 .expect("discovery serializes");
6128 assert_eq!(
6129 discovery["capabilities"]["completions"],
6130 serde_json::json!({}),
6131 "the real upstream mapping advertises final completion support"
6132 );
6133
6134 let list_inbound = crate::InboundRequestContext::new(
6135 Cx::for_testing(),
6136 829,
6137 crate::InboundRequestTransport::Memory,
6138 );
6139 let prompts = server
6140 .dispatch_stateless(
6141 &list_inbound,
6142 &JsonRpcRequest::new(
6143 "prompts/list",
6144 Some(serde_json::json!({
6145 "_meta": {
6146 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6147 "io.modelcontextprotocol/clientCapabilities": {},
6148 },
6149 })),
6150 829_i64,
6151 ),
6152 )
6153 .expect("the replaced prompt is public through the final list")
6154 .result
6155 .expect("the replaced prompt list has a result payload");
6156 assert_eq!(
6157 prompts["prompts"][0]["title"], "Upstream Final Deploy",
6158 "Replace exposes the admitted upstream prompt instead of the local target"
6159 );
6160
6161 let completion_inbound = crate::InboundRequestContext::new(
6162 Cx::for_testing(),
6163 830,
6164 crate::InboundRequestTransport::Memory,
6165 );
6166 let completion = server
6167 .dispatch_stateless(&completion_inbound, &final_completion_request(830))
6168 .expect("the replaced target forwards through the public final completion route")
6169 .result
6170 .expect("the replaced target completion has a result payload");
6171 assert_eq!(
6172 completion["completion"]["values"],
6173 serde_json::json!(["final-staging"])
6174 );
6175 assert_eq!(
6176 calls
6177 .lock()
6178 .expect("completion proxy call log is not poisoned")
6179 .len(),
6180 1,
6181 "Replace installs exactly one upstream completion invocation path"
6182 );
6183 }
6184
6185 #[test]
6186 fn public_final_proxy_completion_replacement_evicts_proxy_targets_and_restores_local_providers()
6187 {
6188 let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6189 let template_calls = Arc::new(Mutex::new(Vec::new()));
6190 let evicted = ServerBuilder::new("completion-proxy", "1.0")
6191 .proxy_typed(
6192 bound_proxy_client(
6193 CompletionProxyBackend {
6194 supported: true,
6195 result: final_completion_proxy_result(),
6196 calls: Arc::clone(&prompt_calls),
6197 },
6198 ProtocolEra::Modern2026,
6199 ),
6200 final_completion_proxy_catalog(),
6201 )
6202 .expect("the final prompt proxy installs")
6203 .proxy_typed(
6204 bound_proxy_client(
6205 CompletionProxyBackend {
6206 supported: true,
6207 result: final_completion_proxy_result(),
6208 calls: Arc::clone(&template_calls),
6209 },
6210 ProtocolEra::Modern2026,
6211 ),
6212 final_completion_proxy_template_catalog(),
6213 )
6214 .expect("the final resource-template proxy installs")
6215 .on_duplicate(DuplicateBehavior::Replace)
6216 .prompt(LocalFinalCompletionPrompt)
6217 .resource_template(local_completion_template())
6218 .build();
6219
6220 let discovery = serde_json::to_value(
6221 evicted
6222 .server_discovery()
6223 .expect("the locally replaced catalog remains discoverable"),
6224 )
6225 .expect("discovery serializes");
6226 assert!(
6227 discovery["capabilities"].get("completions").is_none(),
6228 "replacing every proxied final target removes proxy completion advertisement"
6229 );
6230 for (id, request) in [
6231 (831, final_completion_request(831)),
6232 (832, final_resource_template_completion_request(832)),
6233 ] {
6234 let inbound = crate::InboundRequestContext::new(
6235 Cx::for_testing(),
6236 id,
6237 crate::InboundRequestTransport::Memory,
6238 );
6239 let rejected = evicted
6240 .dispatch_stateless(&inbound, &request)
6241 .expect("the evicted completion mapping returns a JSON-RPC error");
6242 assert_eq!(
6243 rejected.error.and_then(|error| error.code.as_i32()),
6244 Some(-32601),
6245 "the local replacement has no retained upstream completion provider"
6246 );
6247 assert!(rejected.result.is_none());
6248 }
6249 assert!(
6250 prompt_calls
6251 .lock()
6252 .expect("prompt completion proxy call log is not poisoned")
6253 .is_empty()
6254 );
6255 assert!(
6256 template_calls
6257 .lock()
6258 .expect("template completion proxy call log is not poisoned")
6259 .is_empty()
6260 );
6261
6262 let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6263 let template_calls = Arc::new(Mutex::new(Vec::new()));
6264 let restored = ServerBuilder::new("completion-proxy", "1.0")
6265 .proxy_typed(
6266 bound_proxy_client(
6267 CompletionProxyBackend {
6268 supported: true,
6269 result: final_completion_proxy_result(),
6270 calls: Arc::clone(&prompt_calls),
6271 },
6272 ProtocolEra::Modern2026,
6273 ),
6274 final_completion_proxy_catalog(),
6275 )
6276 .expect("the final prompt proxy installs")
6277 .proxy_typed(
6278 bound_proxy_client(
6279 CompletionProxyBackend {
6280 supported: true,
6281 result: final_completion_proxy_result(),
6282 calls: Arc::clone(&template_calls),
6283 },
6284 ProtocolEra::Modern2026,
6285 ),
6286 final_completion_proxy_template_catalog(),
6287 )
6288 .expect("the final resource-template proxy installs")
6289 .on_duplicate(DuplicateBehavior::Replace)
6290 .prompt(LocalFinalCompletionPrompt)
6291 .resource_template(local_completion_template())
6292 .prompt_completion_handler("final-deploy", TestCompletion)
6293 .resource_template_completion_handler("completion://{environment}", TestCompletion)
6294 .build();
6295 let discovery = serde_json::to_value(
6296 restored
6297 .server_discovery()
6298 .expect("the local completion providers make final discovery available"),
6299 )
6300 .expect("discovery serializes");
6301 assert_eq!(
6302 discovery["capabilities"]["completions"],
6303 serde_json::json!({}),
6304 "only the restored local final providers advertise completion support"
6305 );
6306 for (id, request) in [
6307 (833, final_completion_request(833)),
6308 (834, final_resource_template_completion_request(834)),
6309 ] {
6310 let inbound = crate::InboundRequestContext::new(
6311 Cx::for_testing(),
6312 id,
6313 crate::InboundRequestTransport::Memory,
6314 );
6315 let response = restored
6316 .dispatch_stateless(&inbound, &request)
6317 .expect("the local replacement provider handles the public final request")
6318 .result
6319 .expect("the local replacement provider returns a result payload");
6320 assert_eq!(
6321 response["completion"]["values"],
6322 serde_json::json!(["staging"])
6323 );
6324 }
6325 assert!(
6326 prompt_calls
6327 .lock()
6328 .expect("prompt completion proxy call log is not poisoned")
6329 .is_empty(),
6330 "local prompt providers must not invoke the displaced proxy"
6331 );
6332 assert!(
6333 template_calls
6334 .lock()
6335 .expect("template completion proxy call log is not poisoned")
6336 .is_empty(),
6337 "local template providers must not invoke the displaced proxy"
6338 );
6339 }
6340
6341 #[test]
6342 fn public_legacy_proxy_completion_replacement_evicts_proxy_targets_and_restores_local_provider()
6343 {
6344 let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6345 let template_calls = Arc::new(Mutex::new(Vec::new()));
6346 let evicted = ServerBuilder::new("completion-proxy", "1.0")
6347 .proxy_typed(
6348 bound_proxy_client(
6349 CompletionProxyBackend {
6350 supported: true,
6351 result: legacy_completion_proxy_result(),
6352 calls: Arc::clone(&prompt_calls),
6353 },
6354 ProtocolEra::Legacy2024,
6355 ),
6356 legacy_completion_proxy_catalog(),
6357 )
6358 .expect("the legacy prompt proxy installs")
6359 .proxy_typed(
6360 bound_proxy_client(
6361 CompletionProxyBackend {
6362 supported: true,
6363 result: legacy_completion_proxy_result(),
6364 calls: Arc::clone(&template_calls),
6365 },
6366 ProtocolEra::Legacy2024,
6367 ),
6368 legacy_completion_proxy_template_catalog(),
6369 )
6370 .expect("the legacy resource-template proxy installs")
6371 .on_duplicate(DuplicateBehavior::Replace)
6372 .legacy_prompt(LocalFinalCompletionPrompt)
6373 .legacy_resource_template(local_completion_template())
6374 .build();
6375 let mut legacy_session = initialized_legacy_proxy_session(&evicted);
6376 let notification_sender: crate::NotificationSender = Arc::new(|_| {});
6377 let request_sender = crate::RequestSender::new(
6378 Arc::new(crate::PendingRequests::new()),
6379 Arc::new(|message| {
6380 Err(format!("unexpected outbound message in test: {message:?}"))
6381 }),
6382 );
6383 for request in [
6384 legacy_completion_request(
6385 835,
6386 serde_json::json!({"type": "ref/prompt", "name": "legacy-deploy"}),
6387 ),
6388 legacy_completion_request(
6389 836,
6390 serde_json::json!({"type": "ref/resource", "uri": "completion://{environment}"}),
6391 ),
6392 ] {
6393 let rejected = evicted
6394 .dispatch_request(
6395 &Cx::for_testing(),
6396 &mut legacy_session,
6397 request,
6398 ¬ification_sender,
6399 &request_sender,
6400 )
6401 .expect("the public replaced legacy target returns a JSON-RPC error");
6402 assert_eq!(
6403 rejected.error.and_then(|error| error.code.as_i32()),
6404 Some(-32601),
6405 "the local replacement has no retained upstream completion provider"
6406 );
6407 assert!(rejected.result.is_none());
6408 }
6409 assert!(
6410 prompt_calls
6411 .lock()
6412 .expect("prompt completion proxy call log is not poisoned")
6413 .is_empty()
6414 );
6415 assert!(
6416 template_calls
6417 .lock()
6418 .expect("template completion proxy call log is not poisoned")
6419 .is_empty()
6420 );
6421
6422 let prompt_calls = Arc::new(Mutex::new(Vec::new()));
6423 let template_calls = Arc::new(Mutex::new(Vec::new()));
6424 let restored = ServerBuilder::new("completion-proxy", "1.0")
6425 .proxy_typed(
6426 bound_proxy_client(
6427 CompletionProxyBackend {
6428 supported: true,
6429 result: legacy_completion_proxy_result(),
6430 calls: Arc::clone(&prompt_calls),
6431 },
6432 ProtocolEra::Legacy2024,
6433 ),
6434 legacy_completion_proxy_catalog(),
6435 )
6436 .expect("the legacy prompt proxy installs")
6437 .proxy_typed(
6438 bound_proxy_client(
6439 CompletionProxyBackend {
6440 supported: true,
6441 result: legacy_completion_proxy_result(),
6442 calls: Arc::clone(&template_calls),
6443 },
6444 ProtocolEra::Legacy2024,
6445 ),
6446 legacy_completion_proxy_template_catalog(),
6447 )
6448 .expect("the legacy resource-template proxy installs")
6449 .on_duplicate(DuplicateBehavior::Replace)
6450 .legacy_prompt(LocalFinalCompletionPrompt)
6451 .legacy_resource_template(local_completion_template())
6452 .legacy_completion_handler(TestCompletion)
6453 .build();
6454 let mut legacy_session = initialized_legacy_proxy_session(&restored);
6455 let notification_sender: crate::NotificationSender = Arc::new(|_| {});
6456 let request_sender = crate::RequestSender::new(
6457 Arc::new(crate::PendingRequests::new()),
6458 Arc::new(|message| {
6459 Err(format!("unexpected outbound message in test: {message:?}"))
6460 }),
6461 );
6462 for request in [
6463 legacy_completion_request(
6464 837,
6465 serde_json::json!({"type": "ref/prompt", "name": "legacy-deploy"}),
6466 ),
6467 legacy_completion_request(
6468 838,
6469 serde_json::json!({"type": "ref/resource", "uri": "completion://{environment}"}),
6470 ),
6471 ] {
6472 let response = restored
6473 .dispatch_request(
6474 &Cx::for_testing(),
6475 &mut legacy_session,
6476 request,
6477 ¬ification_sender,
6478 &request_sender,
6479 )
6480 .expect("the public local legacy provider returns a response")
6481 .result
6482 .expect("the local legacy provider returns a result payload");
6483 assert_eq!(
6484 response["completion"]["values"],
6485 serde_json::json!(["staging"])
6486 );
6487 }
6488 assert!(
6489 prompt_calls
6490 .lock()
6491 .expect("prompt completion proxy call log is not poisoned")
6492 .is_empty()
6493 );
6494 assert!(
6495 template_calls
6496 .lock()
6497 .expect("template completion proxy call log is not poisoned")
6498 .is_empty()
6499 );
6500 }
6501
6502 #[test]
6503 fn builder_proxy_rejects_final_tools_with_one_legacy_resource_vector() {
6504 let mut catalog = final_proxy_catalog();
6505 catalog.resources.push(Resource {
6506 uri: "file:///must-not-project".to_owned(),
6507 name: "must-not-project".to_owned(),
6508 description: None,
6509 mime_type: None,
6510 icon: None,
6511 version: None,
6512 tags: Vec::new(),
6513 });
6514
6515 assert_rejected_proxy_catalog_returns_an_error(catalog);
6516 }
6517
6518 #[test]
6519 fn public_legacy_and_stateless_ping_answer_empty_object_without_entering_the_final_request_union()
6520 {
6521 let server = ServerBuilder::new("srv", "1.0").build();
6522 let mut legacy_session =
6523 crate::Session::new(server.info().clone(), server.capabilities().clone());
6524 legacy_session.initialize(
6525 fastmcp_protocol::ClientInfo {
6526 name: "exact-2024-ping-client".to_owned(),
6527 version: "1.0".to_owned(),
6528 },
6529 fastmcp_protocol::ClientCapabilities::default(),
6530 "2024-11-05".to_owned(),
6531 );
6532 let state_before = legacy_session.state().len();
6533 let legacy_ping = JsonRpcRequest::new("ping", Some(serde_json::json!({})), 714_i64);
6534 let mut final_ping = legacy_ping.clone();
6535 final_ping
6536 .params
6537 .as_mut()
6538 .expect("the cloned ping request retains its object parameters")
6539 .as_object_mut()
6540 .expect("the cloned ping parameters remain an object")
6541 .insert(
6542 "_meta".to_owned(),
6543 serde_json::json!({
6544 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6545 "io.modelcontextprotocol/clientCapabilities": {},
6546 }),
6547 );
6548 let mut final_without_metadata = final_ping
6549 .params
6550 .clone()
6551 .expect("the final ping parameters are retained for the negative-control check");
6552 final_without_metadata
6553 .as_object_mut()
6554 .expect("the final ping parameters remain an object")
6555 .remove("_meta");
6556 assert_eq!(final_ping.method, legacy_ping.method);
6557 assert_eq!(final_ping.id, legacy_ping.id);
6558 assert_eq!(Some(final_without_metadata), legacy_ping.params);
6559
6560 let notification_sender: crate::NotificationSender = Arc::new(|_| {});
6561 let request_sender = crate::RequestSender::new(
6562 Arc::new(crate::PendingRequests::new()),
6563 Arc::new(|message| {
6564 Err(format!("unexpected outbound message in test: {message:?}"))
6565 }),
6566 );
6567 let legacy = server
6568 .dispatch_request(
6569 &Cx::for_testing(),
6570 &mut legacy_session,
6571 legacy_ping,
6572 ¬ification_sender,
6573 &request_sender,
6574 )
6575 .expect("the exact-2024 public dispatch path responds to ping");
6576 assert_eq!(legacy.result, Some(serde_json::json!({})));
6577 assert!(legacy.error.is_none());
6578
6579 let inbound = crate::InboundRequestContext::new(
6580 Cx::for_testing(),
6581 714,
6582 crate::InboundRequestTransport::Memory,
6583 );
6584 let answered = server
6585 .dispatch_stateless(&inbound, &final_ping)
6586 .expect("stateless modern ping is a connection health-check");
6587 assert_eq!(answered.result, Some(serde_json::json!({})));
6588 assert!(answered.error.is_none());
6589 assert_eq!(
6590 legacy_session.state().len(),
6591 state_before,
6592 "ping cannot mutate session state"
6593 );
6594
6595 assert!(
6596 !fastmcp_protocol::methods::FINAL_2026_07_28_METHODS
6597 .iter()
6598 .any(|method| method.name == "ping"),
6599 "ping must remain outside the official 2026 client-request union"
6600 );
6601 let decode = fastmcp_protocol::CoreRequest::decode(
6602 ProtocolEra::Modern2026,
6603 "ping",
6604 Some(&serde_json::json!({})),
6605 )
6606 .expect_err("FinalCoreRequest must not grow a Ping variant");
6607 assert!(matches!(
6608 decode,
6609 fastmcp_protocol::CoreDispatchError::UnsupportedMethod {
6610 era: ProtocolEra::Modern2026,
6611 method,
6612 } if method == "ping"
6613 ));
6614 }
6615
6616 #[test]
6617 fn typed_proxy_registration_retains_final_resource_template_and_prompt_metadata() {
6618 let resource = serde_json::json!({
6619 "uri": "mcp://upstream/resource",
6620 "name": "upstream-resource",
6621 "size": 4096,
6622 "_meta": {"com.example/resource": {"retained": true}}
6623 });
6624 let template = serde_json::json!({
6625 "uriTemplate": "mcp://upstream/{name}",
6626 "name": "upstream-template",
6627 "_meta": {"com.example/template": {"retained": true}}
6628 });
6629 let prompt = serde_json::json!({
6630 "name": "upstream-prompt",
6631 "arguments": [{"name": "region", "title": "Region"}],
6632 "_meta": {"com.example/prompt": {"retained": true}}
6633 });
6634 let typed = ProxyTypedCatalog {
6635 tools: ProxyToolCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
6636 resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(vec![
6637 serde_json::from_value(resource.clone())
6638 .expect("the final resource fixture is valid"),
6639 ])),
6640 resource_templates: ProxyResourceTemplateCatalog::Final(ProxyFinalCatalog::new(
6641 vec![
6642 serde_json::from_value(template.clone())
6643 .expect("the final resource-template fixture is valid"),
6644 ],
6645 )),
6646 prompts: ProxyPromptCatalog::Final(ProxyFinalCatalog::new(vec![
6647 serde_json::from_value(prompt.clone())
6648 .expect("the final prompt fixture is valid"),
6649 ])),
6650 };
6651 let server = ServerBuilder::new("srv", "1.0")
6652 .proxy_typed(
6653 bound_proxy_client(DuplicatePolicyProxyBackend, ProtocolEra::Modern2026),
6654 typed,
6655 )
6656 .expect("coherent typed catalog registers")
6657 .build();
6658 for (method, member, expected, id) in [
6659 ("resources/list", "resources", resource, 711_i64),
6660 (
6661 "resources/templates/list",
6662 "resourceTemplates",
6663 template,
6664 712_i64,
6665 ),
6666 ("prompts/list", "prompts", prompt, 713_i64),
6667 ] {
6668 let inbound = crate::InboundRequestContext::new(
6669 Cx::for_testing(),
6670 u64::try_from(id).expect("test request IDs are non-negative"),
6671 crate::InboundRequestTransport::Memory,
6672 );
6673 let response = server
6674 .dispatch_stateless(
6675 &inbound,
6676 &JsonRpcRequest::new(
6677 method,
6678 Some(serde_json::json!({
6679 "_meta": {
6680 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
6681 "io.modelcontextprotocol/clientCapabilities": {},
6682 },
6683 })),
6684 id,
6685 ),
6686 )
6687 .expect("final discovery dispatch succeeds");
6688 assert_eq!(
6689 response.result.expect("final discovery has a result")[member][0],
6690 expected,
6691 "final proxy registration must retain exact {member} metadata"
6692 );
6693 }
6694
6695 assert!(server.resources().is_empty());
6696 assert!(server.resource_templates().is_empty());
6697 assert!(server.prompts().is_empty());
6698 let router = server.into_router();
6699 let state = fastmcp_core::SessionState::new();
6700 let request_ctx =
6701 fastmcp_core::McpContext::with_state(Cx::for_testing(), 714, state.clone());
6702 let legacy_resource = router
6703 .handle_resources_read(
6704 &request_ctx,
6705 &fastmcp_protocol::ReadResourceParams {
6706 uri: "mcp://upstream/resource".to_owned(),
6707 meta: None,
6708 },
6709 state.clone(),
6710 None,
6711 None,
6712 )
6713 .expect_err("typed-final resource registration is not legacy-visible");
6714 assert_eq!(
6715 legacy_resource.code,
6716 fastmcp_core::McpErrorCode::ResourceNotFound
6717 );
6718 let legacy_prompt = router
6719 .handle_prompts_get(
6720 &request_ctx,
6721 fastmcp_protocol::GetPromptParams {
6722 name: "upstream-prompt".to_owned(),
6723 arguments: None,
6724 meta: None,
6725 },
6726 state,
6727 None,
6728 None,
6729 )
6730 .expect_err("typed-final prompt registration is not legacy-visible");
6731 assert_eq!(
6732 legacy_prompt.code,
6733 fastmcp_core::McpErrorCode::PromptNotFound
6734 );
6735 }
6736
6737 #[test]
6738 fn typed_proxy_registration_rejects_an_unbound_caller_asserted_final_catalog() {
6739 let error = match ServerBuilder::new("srv", "1.0").proxy_typed(
6740 ProxyClient::from_backend(DuplicatePolicyProxyBackend),
6741 final_completion_proxy_catalog(),
6742 ) {
6743 Ok(_) => {
6744 panic!("a caller-supplied typed catalog cannot bind an unbound proxy route")
6745 }
6746 Err(error) => error,
6747 };
6748
6749 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6750 assert!(error.message.contains("cannot bind an unbound route"));
6751 }
6752
6753 #[test]
6754 fn typed_proxy_registration_rejects_one_mixed_era_component_vector() {
6755 let typed = ProxyTypedCatalog {
6756 tools: ProxyToolCatalog::Legacy(Vec::new()),
6757 resources: ProxyResourceCatalog::Final(ProxyFinalCatalog::new(Vec::new())),
6758 resource_templates: ProxyResourceTemplateCatalog::Legacy(Vec::new()),
6759 prompts: ProxyPromptCatalog::Legacy(Vec::new()),
6760 };
6761 let error = match ServerBuilder::new("srv", "1.0").register_raw_typed_proxy_catalog(
6762 bound_proxy_client(DuplicatePolicyProxyBackend, ProtocolEra::Legacy2024),
6763 typed,
6764 ) {
6765 Err(error) => error,
6766 Ok(_) => {
6767 panic!("changing only resources to final rejects a mixed-era proxy catalog")
6768 }
6769 };
6770 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6771 }
6772
6773 #[test]
6774 fn builder_proxy_rejects_the_same_final_catalog_for_a_legacy_binding() {
6775 let error = match ServerBuilder::new("srv", "1.0").proxy(
6776 final_catalog_proxy_client(ProtocolEra::Legacy2024),
6777 final_proxy_catalog(),
6778 ) {
6779 Ok(_) => panic!("the catalog era cannot contradict the immutable route binding"),
6780 Err(error) => error,
6781 };
6782
6783 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6784 }
6785
6786 #[test]
6787 fn builder_proxy_with_catalog() {
6788 use crate::proxy::ProxyClient;
6789
6790 struct DummyBackend;
6791 impl crate::proxy::ProxyBackend for DummyBackend {
6792 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
6793 Ok(vec![Tool {
6794 name: "proxy-tool".to_owned(),
6795 description: None,
6796 input_schema: serde_json::json!({}),
6797 output_schema: None,
6798 icon: None,
6799 version: None,
6800 tags: Vec::new(),
6801 annotations: None,
6802 }])
6803 }
6804 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
6805 Ok(vec![])
6806 }
6807 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
6808 Ok(vec![])
6809 }
6810 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
6811 Ok(vec![])
6812 }
6813 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
6814 Ok(vec![])
6815 }
6816 fn call_tool_with_progress(
6817 &mut self,
6818 _: &str,
6819 _: serde_json::Value,
6820 _: crate::proxy::ProgressCallback<'_>,
6821 ) -> McpResult<Vec<Content>> {
6822 Ok(vec![])
6823 }
6824 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
6825 Ok(vec![])
6826 }
6827 fn get_prompt(
6828 &mut self,
6829 _: &str,
6830 _: std::collections::HashMap<String, String>,
6831 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
6832 Ok(vec![])
6833 }
6834 }
6835
6836 let client = ProxyClient::from_backend(DummyBackend);
6837 let catalog = client
6838 .catalog()
6839 .expect("backend discovery supplies the legacy era evidence");
6840
6841 let server = ServerBuilder::new("srv", "1.0")
6842 .proxy(client, catalog)
6843 .expect("the backend-observed catalog is admitted")
6844 .build();
6845 assert!(server.has_tools());
6846 }
6847
6848 #[test]
6849 fn builder_proxy_honors_duplicate_policy_for_every_component_kind() {
6850 for behavior in [
6851 DuplicateBehavior::Warn,
6852 DuplicateBehavior::Ignore,
6853 DuplicateBehavior::Error,
6854 ] {
6855 let (client, catalog) = discovered_duplicate_policy_proxy();
6856 let server = ServerBuilder::new("srv", "1.0")
6857 .on_duplicate(behavior)
6858 .tool(TestTool)
6859 .resource(TestResource)
6860 .prompt(TestPrompt)
6861 .proxy(client, catalog)
6862 .expect("the backend-observed catalog is admitted")
6863 .build();
6864 let router = server.into_router();
6865
6866 assert_eq!(
6867 router.tools()[0].description.as_deref(),
6868 Some("a test tool")
6869 );
6870 assert_eq!(router.resources()[0].name, "test_res");
6871 assert_eq!(router.prompts()[0].description, None);
6872 }
6873
6874 let (client, catalog) = discovered_duplicate_policy_proxy();
6875 let replaced = ServerBuilder::new("srv", "1.0")
6876 .on_duplicate(DuplicateBehavior::Replace)
6877 .tool(TestTool)
6878 .resource(TestResource)
6879 .prompt(TestPrompt)
6880 .proxy(client, catalog)
6881 .expect("the backend-observed catalog is admitted")
6882 .build()
6883 .into_router();
6884
6885 assert_eq!(
6886 replaced.tools()[0].description.as_deref(),
6887 Some("proxied tool")
6888 );
6889 assert_eq!(replaced.resources()[0].name, "proxied resource");
6890 assert_eq!(
6891 replaced.prompts()[0].description.as_deref(),
6892 Some("proxied prompt")
6893 );
6894 }
6895
6896 #[cfg(feature = "tasks")]
6897 #[test]
6898 fn as_proxy_typed_installs_route_bound_final_tasks_relay() {
6899 let calls = Arc::new(Mutex::new(Vec::new()));
6900 let updates = Arc::new(Mutex::new(Vec::new()));
6901 let server = ServerBuilder::new("prefixed-proxy-tasks", "1.0")
6902 .as_proxy_typed(
6903 "ext",
6904 ordinary_proxy_tasks_client(calls, updates),
6905 final_completion_proxy_catalog(),
6906 )
6907 .expect("as_proxy_typed admits a modern catalog with a Tasks-capable route")
6908 .build();
6909 assert!(
6910 server.final_task_runtime().is_none(),
6911 "as_proxy_typed must install the route-bound Tasks relay instead of the default in-memory store"
6912 );
6913 let discovery = serde_json::to_value(
6914 server
6915 .server_discovery()
6916 .expect("the prefixed Tasks proxy remains discoverable"),
6917 )
6918 .expect("prefixed Tasks proxy discovery serializes");
6919 assert_eq!(
6920 discovery.pointer("/capabilities/extensions/io.modelcontextprotocol~1tasks"),
6921 Some(&serde_json::json!({})),
6922 "as_proxy_typed must advertise the same Tasks extension as proxy_typed"
6923 );
6924 }
6925
6926 #[test]
6927 fn as_proxy_raw_propagates_duplicate_registration_errors() {
6928 let result = ServerBuilder::new("srv", "1.0")
6929 .on_duplicate(DuplicateBehavior::Error)
6930 .tool(TestTool)
6931 .as_proxy_raw_with_proxy_client(ProxyClient::from_backend(
6932 DuplicatePolicyProxyBackend,
6933 ));
6934
6935 let error = match result {
6936 Ok(_) => panic!("raw proxy registration unexpectedly accepted a duplicate tool"),
6937 Err(error) => error,
6938 };
6939 assert_eq!(error.code, fastmcp_core::McpErrorCode::InvalidRequest);
6940 assert!(error.message.starts_with("Tool already exists"));
6941 }
6942 }
6943
6944 #[test]
6947 fn default_request_timeout_constant() {
6948 assert_eq!(DEFAULT_REQUEST_TIMEOUT_SECS, 30);
6949 }
6950
6951 #[test]
6954 fn builder_mask_error_details_toggle() {
6955 let builder = ServerBuilder::new("srv", "1.0")
6956 .mask_error_details(true)
6957 .mask_error_details(false);
6958 assert!(!builder.is_error_masking_enabled());
6959 }
6960
6961 #[test]
6964 fn builder_strict_validation_toggle() {
6965 let builder = ServerBuilder::new("srv", "1.0")
6966 .strict_input_validation(true)
6967 .strict_input_validation(false);
6968 assert!(!builder.is_strict_input_validation_enabled());
6969 }
6970
6971 #[cfg(feature = "tasks")]
6972 mod task_manager_tests {
6973 use super::*;
6974
6975 #[test]
6978 fn default_build_installs_in_memory_official_tasks() {
6979 let server = ServerBuilder::new("srv", "1.0").build();
6980 assert!(
6981 server.final_task_runtime().is_some(),
6982 "default build must install official in-memory Tasks"
6983 );
6984 }
6985
6986 #[test]
6987 fn builder_with_task_manager_retains_manager_without_advertising_capability() {
6988 use crate::tasks::TaskManager;
6989 let tm = TaskManager::new().into_shared();
6990 let server = ServerBuilder::new("srv", "1.0")
6991 .with_task_manager(tm)
6992 .build();
6993 assert!(server.task_manager().is_some());
6994 assert!(server.capabilities().tasks.is_none());
6995 assert!(
6996 server.final_task_runtime().is_none(),
6997 "quarantined task manager must not receive official Tasks"
6998 );
6999 }
7000
7001 #[test]
7002 fn builder_with_notifying_task_manager_keeps_capability_quarantined() {
7003 use crate::tasks::TaskManager;
7004 let tm = TaskManager::with_list_changed_notifications().into_shared();
7005 let server = ServerBuilder::new("srv", "1.0")
7006 .with_task_manager(tm)
7007 .build();
7008 assert!(server.task_manager().is_some());
7009 assert!(server.capabilities().tasks.is_none());
7010 }
7011
7012 #[test]
7013 fn builder_with_non_notifying_task_manager_keeps_capability_quarantined() {
7014 use crate::tasks::TaskManager;
7015 let tm = TaskManager::new().into_shared();
7016 let server = ServerBuilder::new("srv", "1.0")
7017 .with_task_manager(tm)
7018 .build();
7019 assert!(server.task_manager().is_some());
7020 assert!(server.capabilities().tasks.is_none());
7021 }
7022 }
7023
7024 struct DupResource(&'static str);
7027 impl crate::ResourceHandler for DupResource {
7028 fn definition(&self) -> Resource {
7029 Resource {
7030 uri: format!("file:///{}", self.0),
7031 name: self.0.to_string(),
7032 description: None,
7033 mime_type: None,
7034 icon: None,
7035 version: None,
7036 tags: vec![],
7037 }
7038 }
7039 fn read(&self, _ctx: &McpContext) -> McpResult<Vec<ResourceContent>> {
7040 Ok(vec![])
7041 }
7042 }
7043
7044 struct DupPrompt(&'static str);
7045 impl crate::PromptHandler for DupPrompt {
7046 fn definition(&self) -> Prompt {
7047 Prompt {
7048 name: self.0.to_string(),
7049 description: None,
7050 arguments: vec![],
7051 icon: None,
7052 version: None,
7053 tags: vec![],
7054 }
7055 }
7056 fn get(
7057 &self,
7058 _ctx: &McpContext,
7059 _args: std::collections::HashMap<String, String>,
7060 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
7061 Ok(vec![])
7062 }
7063 }
7064
7065 #[test]
7066 fn builder_on_duplicate_error_resource_logs_but_continues() {
7067 let server = ServerBuilder::new("srv", "1.0")
7068 .on_duplicate(DuplicateBehavior::Error)
7069 .resource(DupResource("dup"))
7070 .resource(DupResource("dup"))
7071 .build();
7072 assert!(server.has_resources());
7073 }
7074
7075 #[test]
7076 fn builder_on_duplicate_error_prompt_logs_but_continues() {
7077 let server = ServerBuilder::new("srv", "1.0")
7078 .on_duplicate(DuplicateBehavior::Error)
7079 .prompt(DupPrompt("dup"))
7080 .prompt(DupPrompt("dup"))
7081 .build();
7082 assert!(server.has_prompts());
7083 }
7084
7085 #[cfg(feature = "proxy")]
7088 #[test]
7089 fn builder_proxy_with_resources_and_prompts() {
7090 use crate::proxy::ProxyClient;
7091
7092 struct DummyBackend2;
7093 impl crate::proxy::ProxyBackend for DummyBackend2 {
7094 fn list_tools(&mut self) -> McpResult<Vec<Tool>> {
7095 Ok(vec![])
7096 }
7097 fn list_resources(&mut self) -> McpResult<Vec<Resource>> {
7098 Ok(vec![Resource {
7099 uri: "file:///proxy-res".to_owned(),
7100 name: "proxy-res".to_owned(),
7101 description: None,
7102 mime_type: None,
7103 icon: None,
7104 version: None,
7105 tags: Vec::new(),
7106 }])
7107 }
7108 fn list_resource_templates(&mut self) -> McpResult<Vec<ResourceTemplate>> {
7109 Ok(vec![ResourceTemplate {
7110 uri_template: "db://{table}".to_owned(),
7111 name: "db".to_owned(),
7112 description: None,
7113 mime_type: None,
7114 icon: None,
7115 version: None,
7116 tags: Vec::new(),
7117 }])
7118 }
7119 fn list_prompts(&mut self) -> McpResult<Vec<Prompt>> {
7120 Ok(vec![Prompt {
7121 name: "proxy-prompt".to_owned(),
7122 description: None,
7123 arguments: Vec::new(),
7124 icon: None,
7125 version: None,
7126 tags: Vec::new(),
7127 }])
7128 }
7129 fn call_tool(&mut self, _: &str, _: serde_json::Value) -> McpResult<Vec<Content>> {
7130 Ok(vec![])
7131 }
7132 fn call_tool_with_progress(
7133 &mut self,
7134 _: &str,
7135 _: serde_json::Value,
7136 _: crate::proxy::ProgressCallback<'_>,
7137 ) -> McpResult<Vec<Content>> {
7138 Ok(vec![])
7139 }
7140 fn read_resource(&mut self, _: &str) -> McpResult<Vec<ResourceContent>> {
7141 Ok(vec![])
7142 }
7143 fn get_prompt(
7144 &mut self,
7145 _: &str,
7146 _: std::collections::HashMap<String, String>,
7147 ) -> McpResult<Vec<fastmcp_protocol::PromptMessage>> {
7148 Ok(vec![])
7149 }
7150 }
7151
7152 let client = ProxyClient::from_backend(DummyBackend2);
7153 let catalog = client
7154 .catalog()
7155 .expect("backend discovery supplies the legacy era evidence");
7156
7157 let server = ServerBuilder::new("srv", "1.0")
7158 .proxy(client, catalog)
7159 .expect("the backend-observed catalog is admitted")
7160 .build();
7161 assert!(server.has_resources());
7162 assert!(server.has_prompts());
7163 assert!(!server.has_tools());
7164 }
7165
7166 #[test]
7169 fn build_propagates_strict_validation_to_router() {
7170 let server = ServerBuilder::new("srv", "1.0")
7171 .strict_input_validation(true)
7172 .build();
7173 let router = server.into_router();
7174 assert!(router.strict_input_validation());
7175 }
7176
7177 #[test]
7178 fn build_propagates_strict_validation_false_to_router() {
7179 let server = ServerBuilder::new("srv", "1.0")
7180 .strict_input_validation(false)
7181 .build();
7182 let router = server.into_router();
7183 assert!(!router.strict_input_validation());
7184 }
7185
7186 #[test]
7189 fn builder_log_level_filter_off() {
7190 let builder = ServerBuilder::new("srv", "1.0").log_level_filter(LevelFilter::Off);
7191 assert_eq!(builder.logging.level, LevelFilter::Off);
7192 assert_eq!(builder.console_config.log_level, LevelFilter::Off);
7193 }
7194
7195 #[test]
7198 fn builder_mount_no_op_leaves_capabilities_unchanged() {
7199 let source = ServerBuilder::new("sub", "1.0").build();
7200 let main = ServerBuilder::new("main", "1.0")
7201 .mount(source, Some("ns"))
7202 .build();
7203 assert!(!main.has_tools());
7204 assert!(!main.has_resources());
7205 assert!(!main.has_prompts());
7206 }
7207}