1use std::collections::BTreeMap;
10use std::fmt;
11use std::marker::PhantomData;
12
13use fastmcp_core::{McpContext, McpError, McpResult};
14use fastmcp_protocol::extensions::{
15 ExtensionDispatchError, ExtensionRegistryError, MAX_EXTENSION_MEMBER_NAME_BYTES,
16 NegotiatedExtensionSet, ServerExtensionDiscovery,
17};
18#[cfg(feature = "apps")]
19use fastmcp_protocol::extensions::{
20 official_mcp_apps_empty_server_settings, register_official_mcp_apps_extension,
21 validate_official_mcp_apps_server_settings,
22};
23use fastmcp_protocol::protocol_policy::ProtocolEra;
24use fastmcp_protocol::{
25 ExtensionDescriptorRegistry, ExtensionDirection, ExtensionId, ExtensionRegistryReceipt,
26 ExtensionSettings, JsonRpcRequest,
27};
28use serde::Serialize;
29use serde::de::DeserializeOwned;
30use serde_json::Value;
31
32pub trait ExtensionHandler<Request, Response>: Send + Sync {
38 fn handle(&self, context: &McpContext, request: Request) -> McpResult<Response>;
40}
41
42#[cfg(all(test, feature = "apps"))]
43mod apps_only_tests {
44 use fastmcp_core::{McpContext, McpResult};
45 use fastmcp_protocol::extensions::official_mcp_apps_empty_server_settings;
46 use fastmcp_protocol::{
47 ExtensionDescriptorRegistry, ExtensionRegistryError, ExtensionSettings,
48 };
49 use serde_json::json;
50
51 use super::{ExtensionHandlerKey, ExtensionHandlerRegistrationError, ExtensionHandlerRegistry};
52
53 #[test]
54 fn official_apps_rejects_unowned_client_to_server_handler_without_mutation() {
55 let mut handlers = ExtensionHandlerRegistry::new(ExtensionDescriptorRegistry::new());
56 let apps_id = handlers
57 .install_official_mcp_apps()
58 .expect("the official Apps descriptor and marker install");
59 let key = ExtensionHandlerKey::new(
60 apps_id.clone(),
61 fastmcp_protocol::extensions::MCP_APPS_INITIALIZE_METHOD,
62 );
63
64 assert_eq!(
65 handlers.register(
66 apps_id.clone(),
67 fastmcp_protocol::extensions::MCP_APPS_INITIALIZE_METHOD,
68 |_context: &McpContext,
69 _params: serde_json::Value|
70 -> McpResult<serde_json::Value> { Ok(json!({})) },
71 ),
72 Err(ExtensionHandlerRegistrationError::MethodNotOwned(key)),
73 "MCP Apps owns no client-to-server extension method on this server registry"
74 );
75 assert_eq!(
76 handlers.len(),
77 0,
78 "the rejected Apps handler cannot create a dead dispatch entry"
79 );
80 assert_eq!(
81 handlers.server_metadata_len(),
82 1,
83 "the rejection preserves the installed Apps discovery marker"
84 );
85 assert_eq!(
86 handlers.descriptor_registry().descriptor(&apps_id),
87 Some(&fastmcp_protocol::official_mcp_apps_descriptor()),
88 "the rejected handler cannot alter the official Apps descriptor"
89 );
90 }
91
92 #[test]
93 fn official_apps_installation_duplicate_is_rejected_without_mutating_metadata() {
94 let mut handlers = ExtensionHandlerRegistry::new(ExtensionDescriptorRegistry::new());
95 let apps_id = handlers
96 .install_official_mcp_apps()
97 .expect("baseline official Apps installation succeeds");
98
99 assert_eq!(
100 handlers
101 .install_official_mcp_apps()
102 .expect_err("only the duplicate installation is rejected"),
103 ExtensionHandlerRegistrationError::OfficialMcpAppsAlreadyInstalled
104 );
105 assert_eq!(handlers.server_metadata_len(), 1);
106 assert_eq!(
107 handlers.descriptor_registry().descriptor(&apps_id),
108 Some(&fastmcp_protocol::official_mcp_apps_descriptor())
109 );
110 }
111
112 #[test]
113 fn manually_registered_official_apps_metadata_requires_the_empty_marker() {
114 let mut descriptors = ExtensionDescriptorRegistry::new();
115 let apps_id = fastmcp_protocol::register_official_mcp_apps_extension(&mut descriptors)
116 .expect("the official Apps descriptor registers");
117 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
118 let rejected = ExtensionSettings::new(json!({ "unexpected": true }))
119 .expect("the one-field alternate is generic extension metadata");
120
121 assert_eq!(
122 handlers.register_server_metadata(apps_id.clone(), rejected),
123 Err(ExtensionHandlerRegistrationError::Registry(
124 ExtensionRegistryError::OfficialMcpAppsServerSettingsNotEmpty
125 )),
126 "only the non-empty official Apps marker is rejected"
127 );
128 assert_eq!(
129 handlers.server_metadata_len(),
130 0,
131 "the rejected marker cannot be retained for later builder composition"
132 );
133
134 handlers
135 .register_server_metadata(apps_id, official_mcp_apps_empty_server_settings())
136 .expect("the exact empty official Apps marker remains accepted");
137 assert_eq!(handlers.server_metadata_len(), 1);
138 }
139}
140
141impl<Request, Response, Handler> ExtensionHandler<Request, Response> for Handler
142where
143 Handler: Fn(&McpContext, Request) -> McpResult<Response> + Send + Sync,
144{
145 fn handle(&self, context: &McpContext, request: Request) -> McpResult<Response> {
146 self(context, request)
147 }
148}
149
150trait ErasedExtensionHandler: Send + Sync {
152 fn invoke(&self, context: &McpContext, parameters: Value) -> McpResult<Value>;
153}
154
155struct SerdeExtensionHandler<Request, Response, Handler> {
157 handler: Handler,
158 marker: PhantomData<fn(Request) -> Response>,
159}
160
161impl<Request, Response, Handler> ErasedExtensionHandler
162 for SerdeExtensionHandler<Request, Response, Handler>
163where
164 Request: DeserializeOwned,
165 Response: Serialize,
166 Handler: ExtensionHandler<Request, Response>,
167{
168 fn invoke(&self, context: &McpContext, parameters: Value) -> McpResult<Value> {
169 let request = serde_json::from_value(parameters)
170 .map_err(|error| McpError::invalid_params(error.to_string()))?;
171 let response = self.handler.handle(context, request)?;
172 serde_json::to_value(response).map_err(|_| {
173 McpError::internal_error("typed extension handler response serialization failed")
174 })
175 }
176}
177
178#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
180pub struct ExtensionHandlerKey {
181 extension_id: ExtensionId,
182 method: String,
183}
184
185impl ExtensionHandlerKey {
186 #[must_use]
188 pub fn new(extension_id: ExtensionId, method: impl Into<String>) -> Self {
189 Self {
190 extension_id,
191 method: method.into(),
192 }
193 }
194
195 #[must_use]
197 pub const fn extension_id(&self) -> &ExtensionId {
198 &self.extension_id
199 }
200
201 #[must_use]
203 pub fn method(&self) -> &str {
204 &self.method
205 }
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
210pub enum ExtensionHandlerRegistrationError {
211 Frozen,
213 UnregisteredExtension(String),
215 EmptyMethodName,
217 MethodNameTooLong(String),
219 MethodNotOwned(ExtensionHandlerKey),
221 MethodNotClientToServer(ExtensionHandlerKey),
223 DuplicateHandler(ExtensionHandlerKey),
225 DuplicateServerMetadata(ExtensionId),
227 #[cfg(feature = "apps")]
229 OfficialMcpAppsAlreadyInstalled,
230 Registry(ExtensionRegistryError),
232}
233
234impl fmt::Display for ExtensionHandlerRegistrationError {
235 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
236 match self {
237 Self::Frozen => formatter.write_str("extension handler registry is frozen"),
238 Self::UnregisteredExtension(id) => {
239 write!(formatter, "extension handler has no descriptor: {id}")
240 }
241 Self::EmptyMethodName => formatter.write_str("extension handler method is empty"),
242 Self::MethodNameTooLong(method) => {
243 write!(
244 formatter,
245 "extension handler method exceeds its byte limit: {method}"
246 )
247 }
248 Self::MethodNotOwned(key) => write!(
249 formatter,
250 "extension handler method is not owned by its descriptor: {}/{}",
251 key.extension_id(),
252 key.method()
253 ),
254 Self::MethodNotClientToServer(key) => write!(
255 formatter,
256 "extension handler method is not client-to-server: {}/{}",
257 key.extension_id(),
258 key.method()
259 ),
260 Self::DuplicateHandler(key) => write!(
261 formatter,
262 "extension handler is already registered: {}/{}",
263 key.extension_id(),
264 key.method()
265 ),
266 Self::DuplicateServerMetadata(id) => {
267 write!(
268 formatter,
269 "extension server metadata is already registered: {id}"
270 )
271 }
272 #[cfg(feature = "apps")]
273 Self::OfficialMcpAppsAlreadyInstalled => {
274 formatter.write_str("official MCP Apps extension is already installed")
275 }
276 Self::Registry(error) => {
277 write!(formatter, "extension registry rejected install: {error}")
278 }
279 }
280 }
281}
282
283impl std::error::Error for ExtensionHandlerRegistrationError {
284 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
285 match self {
286 Self::Registry(error) => Some(error),
287 Self::Frozen
288 | Self::UnregisteredExtension(_)
289 | Self::EmptyMethodName
290 | Self::MethodNameTooLong(_)
291 | Self::MethodNotOwned(_)
292 | Self::MethodNotClientToServer(_)
293 | Self::DuplicateHandler(_)
294 | Self::DuplicateServerMetadata(_) => None,
295 #[cfg(feature = "apps")]
296 Self::OfficialMcpAppsAlreadyInstalled => None,
297 }
298 }
299}
300
301#[derive(Clone, Debug, Eq, PartialEq)]
303pub enum ExtensionHandlerLookupError {
304 RegistryNotFrozen,
306 HandlerNotFound(ExtensionHandlerKey),
308}
309
310impl fmt::Display for ExtensionHandlerLookupError {
311 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312 match self {
313 Self::RegistryNotFrozen => {
314 formatter.write_str("extension handler registry must be frozen before lookup")
315 }
316 Self::HandlerNotFound(key) => write!(
317 formatter,
318 "no extension handler is registered: {}/{}",
319 key.extension_id(),
320 key.method()
321 ),
322 }
323 }
324}
325
326impl std::error::Error for ExtensionHandlerLookupError {}
327
328#[derive(Debug)]
330pub enum ExtensionHandlerInvocationError {
331 RegistryNotFrozen,
333 Protocol(ExtensionDispatchError),
335 HandlerNotFound(ExtensionHandlerKey),
337 RequestEnvelopeRequired(ExtensionHandlerKey),
339 Handler(McpError),
341}
342
343impl fmt::Display for ExtensionHandlerInvocationError {
344 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
345 match self {
346 Self::RegistryNotFrozen => {
347 formatter.write_str("extension handler registry must be frozen before invocation")
348 }
349 Self::Protocol(error) => {
350 write!(formatter, "extension request was not admitted: {error}")
351 }
352 Self::HandlerNotFound(key) => write!(
353 formatter,
354 "no extension handler is registered: {}/{}",
355 key.extension_id(),
356 key.method()
357 ),
358 Self::RequestEnvelopeRequired(key) => write!(
359 formatter,
360 "extension request requires a JSON-RPC request id: {}/{}",
361 key.extension_id(),
362 key.method()
363 ),
364 Self::Handler(error) => write!(formatter, "extension handler failed: {error}"),
365 }
366 }
367}
368
369impl std::error::Error for ExtensionHandlerInvocationError {
370 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
371 match self {
372 Self::Protocol(error) => Some(error),
373 Self::Handler(error) => Some(error),
374 Self::RegistryNotFrozen
375 | Self::HandlerNotFound(_)
376 | Self::RequestEnvelopeRequired(_) => None,
377 }
378 }
379}
380
381pub struct ExtensionHandlerRegistry {
390 descriptor_registry: ExtensionDescriptorRegistry,
391 handlers: BTreeMap<ExtensionHandlerKey, Box<dyn ErasedExtensionHandler>>,
392 server_metadata: BTreeMap<ExtensionId, ExtensionSettings>,
393 frozen: bool,
394}
395
396impl ExtensionHandlerRegistry {
397 #[must_use]
399 pub fn new(descriptor_registry: ExtensionDescriptorRegistry) -> Self {
400 Self {
401 descriptor_registry,
402 handlers: BTreeMap::new(),
403 server_metadata: BTreeMap::new(),
404 frozen: false,
405 }
406 }
407
408 #[must_use]
410 pub const fn descriptor_registry(&self) -> &ExtensionDescriptorRegistry {
411 &self.descriptor_registry
412 }
413
414 pub(crate) fn descriptor_registry_mut(
420 &mut self,
421 ) -> Result<&mut ExtensionDescriptorRegistry, ExtensionHandlerRegistrationError> {
422 if self.frozen {
423 return Err(ExtensionHandlerRegistrationError::Frozen);
424 }
425 Ok(&mut self.descriptor_registry)
426 }
427
428 #[must_use]
430 pub const fn is_frozen(&self) -> bool {
431 self.frozen
432 }
433
434 #[must_use]
436 pub fn len(&self) -> usize {
437 self.handlers.len()
438 }
439
440 #[must_use]
442 pub fn is_empty(&self) -> bool {
443 self.handlers.is_empty()
444 }
445
446 #[must_use]
448 pub fn server_metadata_len(&self) -> usize {
449 self.server_metadata.len()
450 }
451
452 pub(crate) fn configured_server_discovery(&self) -> ServerExtensionDiscovery {
458 ServerExtensionDiscovery {
459 extensions: self.server_metadata.clone(),
460 }
461 }
462
463 pub fn register_server_metadata(
471 &mut self,
472 extension_id: ExtensionId,
473 settings: ExtensionSettings,
474 ) -> Result<(), ExtensionHandlerRegistrationError> {
475 if self.frozen {
476 return Err(ExtensionHandlerRegistrationError::Frozen);
477 }
478 if self.descriptor_registry.descriptor(&extension_id).is_none() {
479 return Err(ExtensionHandlerRegistrationError::UnregisteredExtension(
480 extension_id.to_string(),
481 ));
482 }
483 #[cfg(feature = "apps")]
484 if extension_id == fastmcp_protocol::official_mcp_apps_extension_id() {
485 validate_official_mcp_apps_server_settings(&settings)
486 .map_err(ExtensionHandlerRegistrationError::Registry)?;
487 }
488 if self.server_metadata.contains_key(&extension_id) {
489 return Err(ExtensionHandlerRegistrationError::DuplicateServerMetadata(
490 extension_id,
491 ));
492 }
493
494 self.server_metadata.insert(extension_id, settings);
495 Ok(())
496 }
497
498 #[cfg(feature = "apps")]
506 pub fn install_official_mcp_apps(
507 &mut self,
508 ) -> Result<ExtensionId, ExtensionHandlerRegistrationError> {
509 if self.frozen {
510 return Err(ExtensionHandlerRegistrationError::Frozen);
511 }
512 let id = fastmcp_protocol::official_mcp_apps_extension_id();
513 if self.descriptor_registry.descriptor(&id).is_some()
514 || self.server_metadata.contains_key(&id)
515 {
516 return Err(ExtensionHandlerRegistrationError::OfficialMcpAppsAlreadyInstalled);
517 }
518 register_official_mcp_apps_extension(&mut self.descriptor_registry)
519 .map_err(ExtensionHandlerRegistrationError::Registry)?;
520 self.register_server_metadata(id.clone(), official_mcp_apps_empty_server_settings())?;
521 Ok(id)
522 }
523
524 pub fn register<Request, Response, Handler>(
531 &mut self,
532 extension_id: ExtensionId,
533 method: impl Into<String>,
534 handler: Handler,
535 ) -> Result<(), ExtensionHandlerRegistrationError>
536 where
537 Request: DeserializeOwned + 'static,
538 Response: Serialize + 'static,
539 Handler: ExtensionHandler<Request, Response> + 'static,
540 {
541 if self.frozen {
542 return Err(ExtensionHandlerRegistrationError::Frozen);
543 }
544 if self.descriptor_registry.descriptor(&extension_id).is_none() {
545 return Err(ExtensionHandlerRegistrationError::UnregisteredExtension(
546 extension_id.to_string(),
547 ));
548 }
549
550 let key = ExtensionHandlerKey::new(extension_id, method);
551 if key.method().is_empty() {
552 return Err(ExtensionHandlerRegistrationError::EmptyMethodName);
553 }
554 if key.method().len() > MAX_EXTENSION_MEMBER_NAME_BYTES {
555 return Err(ExtensionHandlerRegistrationError::MethodNameTooLong(
556 key.method().to_owned(),
557 ));
558 }
559 let Some(method_descriptor) = self
560 .descriptor_registry
561 .method_descriptor(key.extension_id(), key.method())
562 else {
563 return Err(ExtensionHandlerRegistrationError::MethodNotOwned(key));
564 };
565 if method_descriptor.direction != ExtensionDirection::ClientToServer {
566 return Err(ExtensionHandlerRegistrationError::MethodNotClientToServer(
567 key,
568 ));
569 }
570 if self.handlers.contains_key(&key) {
571 return Err(ExtensionHandlerRegistrationError::DuplicateHandler(key));
572 }
573
574 self.handlers.insert(
575 key,
576 Box::new(SerdeExtensionHandler::<Request, Response, Handler> {
577 handler,
578 marker: PhantomData,
579 }),
580 );
581 Ok(())
582 }
583
584 pub fn freeze(&mut self) -> Result<ExtensionRegistryReceipt, ExtensionRegistryError> {
586 let receipt = self.descriptor_registry.freeze()?;
587 self.frozen = true;
588 Ok(receipt)
589 }
590
591 pub fn freeze_with_server_discovery(
599 &mut self,
600 ) -> Result<(ExtensionRegistryReceipt, ServerExtensionDiscovery), ExtensionRegistryError> {
601 let receipt = self.freeze()?;
602 let discovery = self
603 .server_discovery()
604 .expect("registry is frozen immediately after a successful freeze");
605 Ok((receipt, discovery))
606 }
607
608 pub fn server_discovery(
613 &self,
614 ) -> Result<ServerExtensionDiscovery, ExtensionHandlerLookupError> {
615 if !self.frozen {
616 return Err(ExtensionHandlerLookupError::RegistryNotFrozen);
617 }
618 Ok(ServerExtensionDiscovery {
619 extensions: self.server_metadata.clone(),
620 })
621 }
622
623 pub fn lookup(
625 &self,
626 extension_id: &ExtensionId,
627 method: &str,
628 ) -> Result<&ExtensionHandlerKey, ExtensionHandlerLookupError> {
629 if !self.frozen {
630 return Err(ExtensionHandlerLookupError::RegistryNotFrozen);
631 }
632 let key = ExtensionHandlerKey::new(extension_id.clone(), method);
633 self.handlers
634 .get_key_value(&key)
635 .map(|(registered_key, _)| registered_key)
636 .ok_or(ExtensionHandlerLookupError::HandlerNotFound(key))
637 }
638
639 pub fn invoke(
648 &self,
649 negotiated: &NegotiatedExtensionSet,
650 protocol_era: ProtocolEra,
651 extension_id: &ExtensionId,
652 request: &JsonRpcRequest,
653 context: &McpContext,
654 ) -> Result<Value, ExtensionHandlerInvocationError> {
655 if !self.frozen {
656 return Err(ExtensionHandlerInvocationError::RegistryNotFrozen);
657 }
658
659 negotiated
660 .admit_method(
661 &self.descriptor_registry,
662 protocol_era,
663 extension_id,
664 &request.method,
665 ExtensionDirection::ClientToServer,
666 )
667 .map_err(ExtensionHandlerInvocationError::Protocol)?;
668 let key = ExtensionHandlerKey::new(extension_id.clone(), &request.method);
669 if request.is_notification() {
670 return Err(ExtensionHandlerInvocationError::RequestEnvelopeRequired(
671 key,
672 ));
673 }
674 let key = self
675 .lookup(extension_id, &request.method)
676 .map_err(|error| match error {
677 ExtensionHandlerLookupError::RegistryNotFrozen => {
678 ExtensionHandlerInvocationError::RegistryNotFrozen
679 }
680 ExtensionHandlerLookupError::HandlerNotFound(key) => {
681 ExtensionHandlerInvocationError::HandlerNotFound(key)
682 }
683 })?;
684 let handler = self
685 .handlers
686 .get(key)
687 .ok_or_else(|| ExtensionHandlerInvocationError::HandlerNotFound(key.clone()))?;
688 handler
689 .invoke(
690 context,
691 request
692 .params
693 .clone()
694 .unwrap_or_else(|| serde_json::json!({})),
695 )
696 .map_err(ExtensionHandlerInvocationError::Handler)
697 }
698}
699
700#[cfg(all(test, feature = "tasks"))]
701mod tests {
702 use std::collections::BTreeMap;
703 use std::sync::Arc;
704 use std::sync::atomic::{AtomicUsize, Ordering};
705
706 use asupersync::Cx;
707 use fastmcp_core::{McpContext, McpErrorCode, McpResult};
708 #[cfg(feature = "apps")]
709 use fastmcp_protocol::extensions::official_mcp_apps_empty_server_settings;
710 use fastmcp_protocol::extensions::{
711 ClientExtensionDiscovery, ExtensionLocalEnablement, ExtensionSettings,
712 ServerExtensionDiscovery, official_tasks_descriptor, official_tasks_empty_settings,
713 register_official_tasks_extension,
714 };
715 use fastmcp_protocol::protocol_policy::ProtocolEra;
716 use fastmcp_protocol::{
717 ExtensionDescriptorRegistry, ExtensionDirection, ExtensionId, JsonRpcRequest,
718 };
719 use serde::{Deserialize, Serialize};
720 use serde_json::json;
721
722 use super::{
723 ExtensionHandlerInvocationError, ExtensionHandlerKey, ExtensionHandlerRegistrationError,
724 ExtensionHandlerRegistry,
725 };
726
727 fn tasks_descriptors() -> (ExtensionDescriptorRegistry, ExtensionId) {
728 let mut descriptors = ExtensionDescriptorRegistry::new();
729 let id = register_official_tasks_extension(&mut descriptors)
730 .expect("official Tasks descriptor registers");
731 (descriptors, id)
732 }
733
734 fn primary_tasks_descriptor_with_direction(
735 direction: ExtensionDirection,
736 ) -> (ExtensionDescriptorRegistry, ExtensionId) {
737 let id = fastmcp_protocol::official_tasks_extension_id();
738 let mut descriptor = official_tasks_descriptor();
739 let method = descriptor
740 .method
741 .as_mut()
742 .expect("official Tasks descriptor owns a primary request method");
743 method.direction = direction;
744 if direction == ExtensionDirection::ServerToClient {
745 method.http_era_disposition = None;
746 }
747 let mut descriptors = ExtensionDescriptorRegistry::new();
748 descriptors
749 .register(descriptor)
750 .expect("direction-adjusted Tasks descriptor remains structurally valid");
751 (descriptors, id)
752 }
753
754 #[derive(Deserialize)]
755 struct GetTaskRequest {
756 value: u32,
757 }
758
759 #[derive(Debug, PartialEq, Serialize)]
760 struct GetTaskResponse {
761 next: u32,
762 }
763
764 #[derive(Deserialize)]
765 struct UpdateTaskRequest {
766 title: String,
767 }
768
769 #[derive(Debug, PartialEq, Serialize)]
770 struct UpdateTaskResponse {
771 updated_title: String,
772 }
773
774 #[allow(
775 clippy::unnecessary_wraps,
776 reason = "the ExtensionHandler callback contract requires McpResult"
777 )]
778 fn get_task(context: &McpContext, request: GetTaskRequest) -> McpResult<GetTaskResponse> {
779 assert_eq!(context.request_id(), 71);
780 Ok(GetTaskResponse {
781 next: request.value + 1,
782 })
783 }
784
785 #[allow(
786 clippy::unnecessary_wraps,
787 reason = "the ExtensionHandler callback contract requires McpResult"
788 )]
789 fn alternate_get_task(
790 context: &McpContext,
791 request: GetTaskRequest,
792 ) -> McpResult<GetTaskResponse> {
793 assert_eq!(context.request_id(), 71);
794 Ok(GetTaskResponse {
795 next: request.value + 2,
796 })
797 }
798
799 #[allow(
800 clippy::unnecessary_wraps,
801 reason = "the ExtensionHandler callback contract requires McpResult"
802 )]
803 fn update_task(
804 context: &McpContext,
805 request: UpdateTaskRequest,
806 ) -> McpResult<UpdateTaskResponse> {
807 assert_eq!(context.request_id(), 71);
808 Ok(UpdateTaskResponse {
809 updated_title: request.title.to_uppercase(),
810 })
811 }
812
813 fn negotiated_tasks(
814 descriptors: &ExtensionDescriptorRegistry,
815 id: &ExtensionId,
816 ) -> fastmcp_protocol::extensions::NegotiatedExtensionSet {
817 let client = ClientExtensionDiscovery {
818 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
819 };
820 let server = ServerExtensionDiscovery {
821 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
822 };
823 let mut local = ExtensionLocalEnablement::default();
824 local.enable(id.clone());
825 let mut resolver =
826 |_descriptor: &fastmcp_protocol::ExtensionDescriptor,
827 _client: &ExtensionSettings,
828 _server: &ExtensionSettings| { Ok(official_tasks_empty_settings()) };
829
830 descriptors
831 .negotiate(
832 ProtocolEra::Modern2026,
833 &local,
834 &client,
835 &server,
836 &mut resolver,
837 )
838 .expect("bilaterally advertised official Tasks negotiates")
839 }
840
841 #[test]
842 fn extension_handler_registry_freezes_and_looks_up_registered_handler() {
843 let (descriptors, id) = tasks_descriptors();
844 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
845 handlers
846 .register(id.clone(), "tasks/get", get_task)
847 .expect("typed Tasks handler registers");
848
849 let receipt = handlers.freeze().expect("handler registry freezes");
850
851 assert!(handlers.is_frozen());
852 assert_eq!(handlers.len(), 1);
853 assert_eq!(handlers.descriptor_registry().receipt(), Some(&receipt));
854 let registered = handlers
855 .lookup(&id, "tasks/get")
856 .expect("frozen registry finds its handler registration");
857 assert_eq!(registered.extension_id(), &id);
858 assert_eq!(registered.method(), "tasks/get");
859 }
860
861 #[test]
862 fn extension_handler_registry_requires_client_to_server_ownership_without_mutation() {
863 let (valid_descriptors, valid_id) =
864 primary_tasks_descriptor_with_direction(ExtensionDirection::ClientToServer);
865 let mut valid_handlers = ExtensionHandlerRegistry::new(valid_descriptors);
866 valid_handlers
867 .register(valid_id, "tasks/get", get_task)
868 .expect("a descriptor-owned client-to-server method registers");
869 assert_eq!(valid_handlers.len(), 1);
870
871 let (rejected_descriptors, rejected_id) =
872 primary_tasks_descriptor_with_direction(ExtensionDirection::ServerToClient);
873 let mut rejected_handlers = ExtensionHandlerRegistry::new(rejected_descriptors);
874 let key = ExtensionHandlerKey::new(rejected_id.clone(), "tasks/get");
875
876 assert_eq!(
877 rejected_handlers.register(rejected_id.clone(), "tasks/get", get_task),
878 Err(ExtensionHandlerRegistrationError::MethodNotClientToServer(
879 key
880 )),
881 "the structurally valid server-to-client direction must reject before insertion"
882 );
883 assert_eq!(
884 rejected_handlers.len(),
885 0,
886 "a server-to-client method cannot create a dead server handler entry"
887 );
888 assert_eq!(rejected_handlers.server_metadata_len(), 0);
889 assert_eq!(
890 rejected_handlers
891 .descriptor_registry()
892 .method_descriptor(&rejected_id, "tasks/get")
893 .map(|method| method.direction),
894 Some(ExtensionDirection::ServerToClient),
895 "rejection must not alter the owned descriptor"
896 );
897 }
898
899 #[test]
900 #[cfg(feature = "apps")]
901 fn official_apps_server_metadata_is_emitted_from_the_frozen_registry() {
902 let (descriptors, tasks_id) = tasks_descriptors();
903 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
904 let apps_id = handlers
905 .install_official_mcp_apps()
906 .expect("the official Apps descriptor and marker install alongside Tasks");
907 let (receipt, discovery) = handlers
908 .freeze_with_server_discovery()
909 .expect("Apps metadata registry freezes with matching discovery");
910
911 assert_eq!(handlers.descriptor_registry().receipt(), Some(&receipt));
912 assert_eq!(handlers.server_metadata_len(), 1);
913 assert!(
914 handlers
915 .descriptor_registry()
916 .descriptor(&tasks_id)
917 .is_some(),
918 "Apps installation preserves the already-registered Tasks descriptor"
919 );
920 assert_eq!(
921 handlers.descriptor_registry().descriptor(&apps_id),
922 Some(&fastmcp_protocol::official_mcp_apps_descriptor()),
923 "server discovery must use the protocol's official Apps descriptor"
924 );
925 assert_eq!(
926 discovery
927 .extensions
928 .get(&apps_id)
929 .map(|settings| serde_json::Value::Object(settings.as_object().clone())),
930 Some(json!({})),
931 "the official Apps server marker is emitted only through the frozen descriptor registry"
932 );
933 }
934
935 #[test]
936 fn unnegotiated_extension_metadata_is_rejected_without_mutating_the_registry() {
937 let (descriptors, _) = tasks_descriptors();
938 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
939 let unnegotiated = fastmcp_protocol::official_mcp_apps_extension_id();
940 let marker = ExtensionSettings::new(json!({}))
941 .expect("the unchanged official Apps server marker is an object");
942
943 assert_eq!(
944 handlers.register_server_metadata(unnegotiated.clone(), marker),
945 Err(ExtensionHandlerRegistrationError::UnregisteredExtension(
946 unnegotiated.to_string()
947 )),
948 "only the extension identifier differs from the registered Apps metadata path"
949 );
950 assert_eq!(handlers.server_metadata_len(), 0);
951 assert!(
952 handlers
953 .descriptor_registry()
954 .descriptor(&unnegotiated)
955 .is_none()
956 );
957
958 handlers.freeze().expect("unchanged registry still freezes");
959 assert!(
960 handlers
961 .server_discovery()
962 .expect("unchanged frozen registry still exports discovery")
963 .extensions
964 .is_empty(),
965 "the rejected unnegotiated extension cannot alter advertised metadata"
966 );
967 }
968
969 #[test]
970 fn extension_handler_registry_rejects_duplicate_key_one_variable_negative() {
971 let (descriptors, id) = tasks_descriptors();
972 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
973 handlers
974 .register(id.clone(), "tasks/get", get_task)
975 .expect("baseline handler registers");
976
977 assert_eq!(
978 handlers.register(id.clone(), "tasks/get", alternate_get_task),
979 Err(ExtensionHandlerRegistrationError::DuplicateHandler(
980 ExtensionHandlerKey::new(id, "tasks/get")
981 )),
982 "only the handler implementation changes from the registered request location"
983 );
984 }
985
986 #[test]
987 fn extension_handler_registry_erases_heterogeneous_types_and_rejects_malformed_input() {
988 let (descriptors, id) = tasks_descriptors();
989 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
990 let get_calls = Arc::new(AtomicUsize::new(0));
991 let counted_get_calls = Arc::clone(&get_calls);
992 handlers
993 .register(
994 id.clone(),
995 "tasks/get",
996 move |context: &McpContext, request: GetTaskRequest| {
997 counted_get_calls.fetch_add(1, Ordering::Relaxed);
998 get_task(context, request)
999 },
1000 )
1001 .expect("typed Tasks get handler registers");
1002 handlers
1003 .register(id.clone(), "tasks/update", update_task)
1004 .expect("differently typed Tasks update handler registers");
1005 handlers.freeze().expect("handler registry freezes");
1006 let negotiated = negotiated_tasks(handlers.descriptor_registry(), &id);
1007 let context = McpContext::new(Cx::for_testing(), 71);
1008
1009 assert_eq!(
1010 handlers
1011 .invoke(
1012 &negotiated,
1013 ProtocolEra::Modern2026,
1014 &id,
1015 &JsonRpcRequest::new("tasks/get", Some(json!({"value": 41})), 1_i64),
1016 &context,
1017 )
1018 .expect("negotiated protocol admission invokes the typed get handler"),
1019 json!({"next": 42})
1020 );
1021 assert_eq!(
1022 handlers
1023 .invoke(
1024 &negotiated,
1025 ProtocolEra::Modern2026,
1026 &id,
1027 &JsonRpcRequest::new("tasks/update", Some(json!({"title": "review"})), 2_i64),
1028 &context,
1029 )
1030 .expect("the same registry invokes the differently typed update handler"),
1031 json!({"updated_title": "REVIEW"})
1032 );
1033 assert_eq!(get_calls.load(Ordering::Relaxed), 1);
1034
1035 let error = handlers
1036 .invoke(
1037 &negotiated,
1038 ProtocolEra::Modern2026,
1039 &id,
1040 &JsonRpcRequest::new("tasks/get", Some(json!({"unexpected": true})), 3_i64),
1041 &context,
1042 )
1043 .expect_err("one malformed request field shape must reject before the handler runs");
1044 let ExtensionHandlerInvocationError::Handler(error) = error else {
1045 panic!("malformed parameters must reach the typed serde admission boundary");
1046 };
1047 assert_eq!(error.code, McpErrorCode::InvalidParams);
1048 assert_eq!(
1049 get_calls.load(Ordering::Relaxed),
1050 1,
1051 "failed typed decoding cannot invoke the registered handler"
1052 );
1053 }
1054
1055 #[test]
1056 fn extension_request_descriptor_rejects_an_idless_near_match_before_handler_invocation() {
1057 let (descriptors, id) = tasks_descriptors();
1058 let mut handlers = ExtensionHandlerRegistry::new(descriptors);
1059 let calls = Arc::new(AtomicUsize::new(0));
1060 let counted_calls = Arc::clone(&calls);
1061 handlers
1062 .register(
1063 id.clone(),
1064 "tasks/update",
1065 move |_context: &McpContext, _request: UpdateTaskRequest| {
1066 counted_calls.fetch_add(1, Ordering::Relaxed);
1067 Ok(serde_json::json!({"resultType": "complete"}))
1068 },
1069 )
1070 .expect("typed Tasks update handler registers");
1071 handlers.freeze().expect("handler registry freezes");
1072 let negotiated = negotiated_tasks(handlers.descriptor_registry(), &id);
1073 let context = McpContext::new(Cx::for_testing(), 72);
1074 let parameters = json!({"title": "review"});
1075
1076 assert_eq!(
1077 handlers
1078 .invoke(
1079 &negotiated,
1080 ProtocolEra::Modern2026,
1081 &id,
1082 &JsonRpcRequest::new("tasks/update", Some(parameters.clone()), 4_i64),
1083 &context,
1084 )
1085 .expect("the request-shaped extension frame is admitted"),
1086 json!({"resultType": "complete"})
1087 );
1088 assert_eq!(calls.load(Ordering::Relaxed), 1);
1089
1090 let error = handlers
1091 .invoke(
1092 &negotiated,
1093 ProtocolEra::Modern2026,
1094 &id,
1095 &JsonRpcRequest::notification("tasks/update", Some(parameters)),
1096 &context,
1097 )
1098 .expect_err("removing only the request id must reject before handler invocation");
1099 assert!(matches!(
1100 error,
1101 ExtensionHandlerInvocationError::RequestEnvelopeRequired(_)
1102 ));
1103 assert_eq!(
1104 calls.load(Ordering::Relaxed),
1105 1,
1106 "the rejected id-less near-match must leave handler-owned state unchanged"
1107 );
1108 }
1109}