1use std::collections::{BTreeMap, BTreeSet};
8use std::fmt;
9
10use fastmcp_core::sha256_bounded;
11use serde::Deserialize;
12use serde_json::{Map, Value};
13
14use crate::methods::{final_2026_07_28_method, legacy_2024_11_05_method};
15use crate::protocol_policy::ProtocolEra;
16
17pub const MAX_EXTENSION_DESCRIPTORS: usize = 128;
19pub const MAX_EXTENSION_ID_BYTES: usize = 512;
21pub const MAX_EXTENSION_REGISTRY_CANONICAL_BYTES: usize = 256 * 1024;
23pub const MAX_EXTENSION_SETTINGS_ENTRIES: usize = 128;
25pub const MAX_EXTENSION_SETTINGS_KEY_BYTES: usize = 512;
27pub const MAX_EXTENSION_SETTINGS_VALUE_BYTES: usize = 16 * 1024;
29pub const MAX_EXTENSION_SETTINGS_NESTING: usize = 32;
31pub const MAX_EXTENSION_MEMBER_NAME_BYTES: usize = 512;
33pub const MAX_EXTENSION_ROUTING_HEADERS: usize = 32;
35pub const MAX_EXTENSION_ROUTING_HEADER_BYTES: usize = 256;
37pub const MAX_STDIO_CORRELATION_METHODS: usize = 32;
39
40#[cfg(feature = "tasks")]
42pub const OFFICIAL_TASKS_EXTENSION_ID: &str = "io.modelcontextprotocol/tasks";
43#[cfg(feature = "tasks")]
45pub const OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID: &str = "tasks-2026-07-28-empty-object-v1";
46#[cfg(feature = "tasks")]
48pub const OFFICIAL_TASKS_EMPTY_SETTINGS_CODEC_ID: &str = "tasks-2026-07-28-empty-object-v1";
49#[cfg(feature = "tasks")]
51pub const OFFICIAL_TASKS_METHODS: [&str; 3] = ["tasks/get", "tasks/update", "tasks/cancel"];
52#[cfg(feature = "tasks")]
54pub const OFFICIAL_TASKS_NOTIFICATION: &str = "notifications/tasks";
55#[cfg(feature = "tasks")]
57pub const OFFICIAL_TASKS_RESULT_DISCRIMINATOR: &str = "task";
58
59pub const OFFICIAL_MCP_APPS_EXTENSION_ID: &str = "io.modelcontextprotocol/ui";
61pub const MCP_APPS_PROTOCOL_VERSION: &str = "2026-01-26";
66pub const MCP_APPS_HTML_MIME_TYPE: &str = "text/html;profile=mcp-app";
68pub const MCP_APPS_CLIENT_SETTINGS_SCHEMA_ID: &str = "apps-2026-01-26-client-mime-types-v1";
70pub const MCP_APPS_SERVER_SETTINGS_SCHEMA_ID: &str =
72 "fastmcp-2026-07-28-apps-empty-server-marker-v1";
73pub const MCP_APPS_NEGOTIATION_RESOLVER_ID: &str = "fastmcp-apps-bilateral-resolver-v1";
75pub const MCP_APPS_NEGOTIATION_RESOLVER_VERSION: u32 = 1;
77pub const MCP_APPS_ACTIVATION_PREDICATE_ID: &str = "fastmcp-2026-07-28-apps-bilateral-mime-v1";
79pub const MAX_MCP_APPS_MIME_TYPES: usize = 128;
81pub const MAX_MCP_APPS_MIME_TYPE_BYTES: usize = 512;
83
84pub const MCP_APPS_OPEN_LINK_METHOD: &str = "ui/open-link";
86pub const MCP_APPS_DOWNLOAD_FILE_METHOD: &str = "ui/download-file";
88pub const MCP_APPS_MESSAGE_METHOD: &str = "ui/message";
90pub const MCP_APPS_UPDATE_MODEL_CONTEXT_METHOD: &str = "ui/update-model-context";
92pub const MCP_APPS_RESOURCE_TEARDOWN_METHOD: &str = "ui/resource-teardown";
94pub const MCP_APPS_INITIALIZE_METHOD: &str = "ui/initialize";
96pub const MCP_APPS_REQUEST_DISPLAY_MODE_METHOD: &str = "ui/request-display-mode";
98
99pub const MCP_APPS_SANDBOX_PROXY_READY_NOTIFICATION: &str = "ui/notifications/sandbox-proxy-ready";
101pub const MCP_APPS_SANDBOX_RESOURCE_READY_NOTIFICATION: &str =
103 "ui/notifications/sandbox-resource-ready";
104pub const MCP_APPS_SIZE_CHANGED_NOTIFICATION: &str = "ui/notifications/size-changed";
106pub const MCP_APPS_TOOL_INPUT_NOTIFICATION: &str = "ui/notifications/tool-input";
108pub const MCP_APPS_TOOL_INPUT_PARTIAL_NOTIFICATION: &str = "ui/notifications/tool-input-partial";
110pub const MCP_APPS_TOOL_RESULT_NOTIFICATION: &str = "ui/notifications/tool-result";
112pub const MCP_APPS_TOOL_CANCELLED_NOTIFICATION: &str = "ui/notifications/tool-cancelled";
114pub const MCP_APPS_HOST_CONTEXT_CHANGED_NOTIFICATION: &str =
116 "ui/notifications/host-context-changed";
117pub const MCP_APPS_REQUEST_TEARDOWN_NOTIFICATION: &str = "ui/notifications/request-teardown";
119pub const MCP_APPS_INITIALIZED_NOTIFICATION: &str = "ui/notifications/initialized";
121
122#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub struct ExtensionId(String);
125
126impl ExtensionId {
127 pub fn parse(value: impl Into<String>) -> Result<Self, ExtensionRegistryError> {
129 let value = value.into();
130 if value.is_empty() || value.len() > MAX_EXTENSION_ID_BYTES {
131 return Err(ExtensionRegistryError::InvalidIdentifier(value));
132 }
133 let Some((prefix, name)) = value.split_once('/') else {
134 return Err(ExtensionRegistryError::InvalidIdentifier(value));
135 };
136 if value.matches('/').count() != 1 || !valid_prefix(prefix) || !valid_name(name) {
137 return Err(ExtensionRegistryError::InvalidIdentifier(value));
138 }
139 if let Some(second_label) = prefix.split('.').nth(1) {
144 let reserved = second_label.eq_ignore_ascii_case("mcp")
145 || second_label.eq_ignore_ascii_case("modelcontextprotocol");
146 if reserved && prefix != "io.modelcontextprotocol" {
147 return Err(ExtensionRegistryError::ReservedNamespace(value));
148 }
149 }
150 Ok(Self(value))
151 }
152
153 #[must_use]
155 pub fn as_str(&self) -> &str {
156 &self.0
157 }
158}
159
160impl fmt::Display for ExtensionId {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 formatter.write_str(&self.0)
163 }
164}
165
166fn valid_prefix(prefix: &str) -> bool {
167 prefix.split('.').all(|label| {
168 !label.is_empty()
169 && label.as_bytes()[0].is_ascii_alphabetic()
170 && label
171 .as_bytes()
172 .last()
173 .is_some_and(|byte| byte.is_ascii_alphanumeric())
174 && label
175 .bytes()
176 .all(|byte| byte.is_ascii_alphabetic() || byte.is_ascii_digit() || byte == b'-')
177 })
178}
179
180fn valid_name(name: &str) -> bool {
181 name.is_empty()
182 || (name.as_bytes()[0].is_ascii_alphanumeric()
183 && name
184 .as_bytes()
185 .last()
186 .is_some_and(|byte| byte.is_ascii_alphanumeric())
187 && name
188 .bytes()
189 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')))
190}
191
192#[derive(Clone, Debug, PartialEq)]
194pub struct ExtensionSettings(Map<String, Value>);
195
196impl ExtensionSettings {
197 pub fn new(value: Value) -> Result<Self, ExtensionRegistryError> {
199 let Value::Object(map) = value else {
200 return Err(ExtensionRegistryError::SettingsNotObject);
201 };
202 validate_settings_map(&map)?;
203 Ok(Self(map))
204 }
205
206 #[must_use]
208 pub const fn as_object(&self) -> &Map<String, Value> {
209 &self.0
210 }
211
212 #[must_use]
214 pub fn into_value(self) -> Value {
215 Value::Object(self.0)
216 }
217
218 pub fn decode<T>(&self) -> Result<T, ExtensionRegistryError>
220 where
221 T: serde::de::DeserializeOwned,
222 {
223 serde_json::from_value(Value::Object(self.0.clone()))
224 .map_err(|_| ExtensionRegistryError::SettingsCodecRejected)
225 }
226}
227
228#[must_use]
230#[cfg(feature = "tasks")]
231pub fn official_tasks_empty_settings() -> ExtensionSettings {
232 ExtensionSettings(Map::new())
233}
234
235#[derive(Deserialize)]
236#[serde(rename_all = "camelCase", deny_unknown_fields)]
237struct McpAppsClientSettingsWire {
238 mime_types: Vec<String>,
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
247pub struct McpAppsClientSettings {
248 mime_types: Vec<String>,
249}
250
251impl McpAppsClientSettings {
252 pub fn new(mime_types: Vec<String>) -> Result<Self, ExtensionRegistryError> {
254 if mime_types.len() > MAX_MCP_APPS_MIME_TYPES
255 || mime_types
256 .iter()
257 .any(|mime_type| mime_type.len() > MAX_MCP_APPS_MIME_TYPE_BYTES)
258 {
259 return Err(ExtensionRegistryError::SettingsTooLarge);
260 }
261
262 validate_settings_map(&mcp_apps_client_settings_map(&mime_types))?;
266 Ok(Self { mime_types })
267 }
268
269 pub fn from_extension_settings(
271 settings: &ExtensionSettings,
272 ) -> Result<Self, ExtensionRegistryError> {
273 let wire = serde_json::from_value::<McpAppsClientSettingsWire>(Value::Object(
274 settings.as_object().clone(),
275 ))
276 .map_err(|_| ExtensionRegistryError::SettingsCodecRejected)?;
277 Self::new(wire.mime_types)
278 }
279
280 #[must_use]
282 pub fn mime_types(&self) -> &[String] {
283 &self.mime_types
284 }
285
286 #[must_use]
288 pub fn supports_mcp_apps_html(&self) -> bool {
289 self.mime_types
290 .iter()
291 .any(|mime_type| mime_type == MCP_APPS_HTML_MIME_TYPE)
292 }
293
294 pub fn to_extension_settings(&self) -> ExtensionSettings {
296 ExtensionSettings(mcp_apps_client_settings_map(&self.mime_types))
297 }
298}
299
300fn mcp_apps_client_settings_map(mime_types: &[String]) -> Map<String, Value> {
301 let mut map = Map::new();
302 map.insert(
303 "mimeTypes".to_owned(),
304 Value::Array(mime_types.iter().cloned().map(Value::String).collect()),
305 );
306 map
307}
308
309#[must_use]
311pub fn official_mcp_apps_empty_server_settings() -> ExtensionSettings {
312 ExtensionSettings(Map::new())
313}
314
315pub fn validate_official_mcp_apps_server_settings(
322 settings: &ExtensionSettings,
323) -> Result<(), ExtensionRegistryError> {
324 if settings.as_object().is_empty() {
325 Ok(())
326 } else {
327 Err(ExtensionRegistryError::OfficialMcpAppsServerSettingsNotEmpty)
328 }
329}
330
331#[must_use]
333pub fn official_mcp_apps_extension_id() -> ExtensionId {
334 ExtensionId::parse(OFFICIAL_MCP_APPS_EXTENSION_ID)
335 .expect("the fixed official MCP Apps identifier satisfies the extension grammar")
336}
337
338#[must_use]
344pub fn official_mcp_apps_descriptor() -> ExtensionDescriptor {
345 ExtensionDescriptor {
346 id: official_mcp_apps_extension_id(),
347 client_settings: ExtensionSettingsSchema {
348 schema_id: MCP_APPS_CLIENT_SETTINGS_SCHEMA_ID.to_owned(),
349 codec_id: MCP_APPS_CLIENT_SETTINGS_SCHEMA_ID.to_owned(),
350 },
351 server_settings: ExtensionSettingsSchema {
352 schema_id: MCP_APPS_SERVER_SETTINGS_SCHEMA_ID.to_owned(),
353 codec_id: MCP_APPS_SERVER_SETTINGS_SCHEMA_ID.to_owned(),
354 },
355 resolver: ExtensionNegotiationResolver {
356 id: MCP_APPS_NEGOTIATION_RESOLVER_ID.to_owned(),
357 version: MCP_APPS_NEGOTIATION_RESOLVER_VERSION,
358 fallback: ExtensionFallbackPolicy::InactiveOnEitherPeer,
359 },
360 method: None,
361 notification: None,
362 result_discriminator: None,
363 routing_headers: Vec::new(),
364 stdio_correlation: None,
365 }
366}
367
368pub fn validate_official_mcp_apps_descriptor(
375 descriptor: &ExtensionDescriptor,
376) -> Result<(), ExtensionRegistryError> {
377 if descriptor == &official_mcp_apps_descriptor() {
378 Ok(())
379 } else {
380 Err(ExtensionRegistryError::OfficialMcpAppsDescriptorMismatch)
381 }
382}
383
384pub fn register_official_mcp_apps_extension(
390 registry: &mut ExtensionDescriptorRegistry,
391) -> Result<ExtensionId, ExtensionRegistryError> {
392 let id = official_mcp_apps_extension_id();
393 let descriptor = official_mcp_apps_descriptor();
394 validate_official_mcp_apps_descriptor(&descriptor)?;
395 registry.register(descriptor)?;
396 Ok(id)
397}
398
399#[derive(Clone, Debug)]
406pub struct McpAppsNegotiationResolver<R = RejectingExtensionNegotiationResolver> {
407 fallback: R,
408}
409
410#[derive(Clone, Copy, Debug, Default)]
416pub struct RejectingExtensionNegotiationResolver;
417
418impl<R> McpAppsNegotiationResolver<R> {
419 #[must_use]
421 pub const fn with_fallback(fallback: R) -> Self {
422 Self { fallback }
423 }
424}
425
426#[derive(Clone, Copy, Debug, Default)]
428#[cfg(feature = "tasks")]
429pub struct OfficialTasksNegotiationResolver;
430
431#[derive(Clone, Debug)]
437#[cfg(feature = "tasks")]
438pub struct TasksNegotiationResolver<R> {
439 fallback: R,
440}
441
442#[cfg(feature = "tasks")]
443impl<R> TasksNegotiationResolver<R> {
444 #[must_use]
446 pub const fn with_fallback(fallback: R) -> Self {
447 Self { fallback }
448 }
449}
450
451#[must_use]
457#[cfg(feature = "tasks")]
458pub const fn official_mcp_apps_negotiation_resolver()
459-> McpAppsNegotiationResolver<OfficialTasksNegotiationResolver> {
460 McpAppsNegotiationResolver::with_fallback(OfficialTasksNegotiationResolver)
461}
462
463#[must_use]
465#[cfg(not(feature = "tasks"))]
466pub const fn official_mcp_apps_negotiation_resolver() -> McpAppsNegotiationResolver {
467 McpAppsNegotiationResolver::with_fallback(RejectingExtensionNegotiationResolver)
468}
469
470pub fn resolve_official_mcp_apps_settings(
476 descriptor: &ExtensionDescriptor,
477 client: &ExtensionSettings,
478 server: &ExtensionSettings,
479) -> Result<ExtensionSettingsResolution, ExtensionNegotiationError> {
480 if validate_official_mcp_apps_descriptor(descriptor).is_err() {
481 return Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
482 descriptor.id.to_string(),
483 ));
484 }
485 validate_official_mcp_apps_server_settings(server).map_err(|_| {
486 ExtensionNegotiationError::SettingsCompatibilityRejected(descriptor.id.to_string())
487 })?;
488 let client = McpAppsClientSettings::from_extension_settings(client).map_err(|_| {
489 ExtensionNegotiationError::SettingsCompatibilityRejected(descriptor.id.to_string())
490 })?;
491 if !client.supports_mcp_apps_html() {
492 return Ok(ExtensionSettingsResolution::Inactive);
493 }
494 Ok(ExtensionSettingsResolution::Active(
495 client.to_extension_settings(),
496 ))
497}
498
499#[cfg(feature = "tasks")]
500fn resolve_official_tasks_settings(
501 descriptor: &ExtensionDescriptor,
502 client: &ExtensionSettings,
503 server: &ExtensionSettings,
504) -> Result<ExtensionSettings, ExtensionNegotiationError> {
505 if descriptor.id.as_str() != OFFICIAL_TASKS_EXTENSION_ID
506 || descriptor.client_settings.schema_id != OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID
507 || descriptor.client_settings.codec_id != OFFICIAL_TASKS_EMPTY_SETTINGS_CODEC_ID
508 || descriptor.server_settings.schema_id != OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID
509 || descriptor.server_settings.codec_id != OFFICIAL_TASKS_EMPTY_SETTINGS_CODEC_ID
510 || descriptor.resolver.id != OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID
511 || descriptor.resolver.version != 1
512 || descriptor.resolver.fallback != ExtensionFallbackPolicy::RejectOneSided
513 {
514 return Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
515 descriptor.id.to_string(),
516 ));
517 }
518 enforce_official_tasks_empty_settings(&descriptor.id, client)?;
519 enforce_official_tasks_empty_settings(&descriptor.id, server)?;
520 Ok(official_tasks_empty_settings())
521}
522
523fn validate_settings_map(map: &Map<String, Value>) -> Result<(), ExtensionRegistryError> {
524 if map.len() > MAX_EXTENSION_SETTINGS_ENTRIES {
525 return Err(ExtensionRegistryError::SettingsTooManyEntries);
526 }
527 for (key, value) in map {
528 if key.len() > MAX_EXTENSION_SETTINGS_KEY_BYTES {
529 return Err(ExtensionRegistryError::SettingsKeyTooLong);
530 }
531 validate_settings_value(value, 0)?;
532 let encoded =
533 serde_json::to_vec(value).map_err(|_| ExtensionRegistryError::SettingsTooLarge)?;
534 if encoded.len() > MAX_EXTENSION_SETTINGS_VALUE_BYTES {
535 return Err(ExtensionRegistryError::SettingsTooLarge);
536 }
537 }
538 Ok(())
539}
540
541fn validate_settings_value(value: &Value, depth: usize) -> Result<(), ExtensionRegistryError> {
542 if depth > MAX_EXTENSION_SETTINGS_NESTING {
543 return Err(ExtensionRegistryError::SettingsTooDeep);
544 }
545 match value {
546 Value::Array(values) => {
547 for value in values {
548 validate_settings_value(value, depth + 1)?;
549 }
550 }
551 Value::Object(values) => {
552 if values.len() > MAX_EXTENSION_SETTINGS_ENTRIES {
553 return Err(ExtensionRegistryError::SettingsTooManyEntries);
554 }
555 for (key, value) in values {
556 if key.len() > MAX_EXTENSION_SETTINGS_KEY_BYTES {
557 return Err(ExtensionRegistryError::SettingsKeyTooLong);
558 }
559 validate_settings_value(value, depth + 1)?;
560 }
561 }
562 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
563 }
564 Ok(())
565}
566
567#[derive(Clone, Debug, PartialEq)]
569pub struct ExtensionDiscovery {
570 pub id: ExtensionId,
572 pub settings: ExtensionSettings,
574}
575
576#[derive(Clone, Debug, Default, PartialEq)]
578pub struct ClientExtensionDiscovery {
579 pub extensions: BTreeMap<ExtensionId, ExtensionSettings>,
581}
582
583#[derive(Clone, Debug, Default, PartialEq)]
585pub struct ServerExtensionDiscovery {
586 pub extensions: BTreeMap<ExtensionId, ExtensionSettings>,
588}
589
590#[derive(Clone, Copy, Debug, Eq, PartialEq)]
592pub enum ExtensionDirection {
593 ClientToServer,
595 ServerToClient,
597}
598
599#[derive(Clone, Copy, Debug, Eq, PartialEq)]
601pub enum ExtensionHttpEraDisposition {
602 ModernExclusive,
604 EraAmbiguous,
606}
607
608#[derive(Clone, Copy, Debug, Eq, PartialEq)]
610pub enum ExtensionFallbackPolicy {
611 RejectOneSided,
613 ServerInactiveFallback,
615 ClientInactiveFallback,
617 InactiveOnEitherPeer,
619}
620
621#[derive(Clone, Debug, Eq, PartialEq)]
623pub struct ExtensionNegotiationResolver {
624 pub id: String,
626 pub version: u32,
628 pub fallback: ExtensionFallbackPolicy,
630}
631
632#[derive(Clone, Debug, Eq, PartialEq)]
634pub struct ExtensionSettingsSchema {
635 pub schema_id: String,
637 pub codec_id: String,
639}
640
641#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct ExtensionMethodDescriptor {
644 pub name: String,
646 pub direction: ExtensionDirection,
648 pub http_era_disposition: Option<ExtensionHttpEraDisposition>,
650 pub legacy_fallback: bool,
653}
654
655#[derive(Clone, Debug, Eq, PartialEq)]
657pub struct ExtensionNotificationDescriptor {
658 pub name: String,
660 pub direction: ExtensionDirection,
662}
663
664#[derive(Clone, Debug, Eq, PartialEq)]
666pub struct ExtensionRoutingHeaderDescriptor {
667 pub name: String,
669}
670
671#[derive(Clone, Debug, Eq, PartialEq)]
673pub struct StdioCorrelationDescriptor {
674 pub metadata_key: String,
676 pub methods: Vec<String>,
678 pub direction: ExtensionDirection,
680}
681
682#[derive(Clone, Debug, Eq, PartialEq)]
684pub struct ExtensionDescriptor {
685 pub id: ExtensionId,
687 pub client_settings: ExtensionSettingsSchema,
689 pub server_settings: ExtensionSettingsSchema,
691 pub resolver: ExtensionNegotiationResolver,
693 pub method: Option<ExtensionMethodDescriptor>,
695 pub notification: Option<ExtensionNotificationDescriptor>,
697 pub result_discriminator: Option<String>,
699 pub routing_headers: Vec<ExtensionRoutingHeaderDescriptor>,
701 pub stdio_correlation: Option<StdioCorrelationDescriptor>,
703}
704
705#[must_use]
707#[cfg(feature = "tasks")]
708pub fn official_tasks_extension_id() -> ExtensionId {
709 ExtensionId::parse(OFFICIAL_TASKS_EXTENSION_ID)
710 .expect("the fixed official Tasks identifier satisfies the extension grammar")
711}
712
713#[must_use]
721#[cfg(feature = "tasks")]
722pub fn official_tasks_descriptor() -> ExtensionDescriptor {
723 ExtensionDescriptor {
724 id: official_tasks_extension_id(),
725 client_settings: ExtensionSettingsSchema {
726 schema_id: OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID.to_owned(),
727 codec_id: OFFICIAL_TASKS_EMPTY_SETTINGS_CODEC_ID.to_owned(),
728 },
729 server_settings: ExtensionSettingsSchema {
730 schema_id: OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID.to_owned(),
731 codec_id: OFFICIAL_TASKS_EMPTY_SETTINGS_CODEC_ID.to_owned(),
732 },
733 resolver: ExtensionNegotiationResolver {
734 id: OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID.to_owned(),
735 version: 1,
736 fallback: ExtensionFallbackPolicy::RejectOneSided,
737 },
738 method: Some(official_tasks_method(OFFICIAL_TASKS_METHODS[0])),
739 notification: Some(ExtensionNotificationDescriptor {
740 name: OFFICIAL_TASKS_NOTIFICATION.to_owned(),
741 direction: ExtensionDirection::ServerToClient,
742 }),
743 result_discriminator: Some(OFFICIAL_TASKS_RESULT_DISCRIMINATOR.to_owned()),
744 routing_headers: Vec::new(),
745 stdio_correlation: None,
746 }
747}
748
749#[cfg(feature = "tasks")]
756pub fn register_official_tasks_extension(
757 registry: &mut ExtensionDescriptorRegistry,
758) -> Result<ExtensionId, ExtensionRegistryError> {
759 let id = official_tasks_extension_id();
760 let mut candidate = registry.clone();
761 candidate.register(official_tasks_descriptor())?;
762 for name in OFFICIAL_TASKS_METHODS.into_iter().skip(1) {
763 candidate.register_method(&id, official_tasks_method(name))?;
764 }
765 *registry = candidate;
766 Ok(id)
767}
768
769#[cfg(feature = "tasks")]
770fn official_tasks_method(name: &str) -> ExtensionMethodDescriptor {
771 ExtensionMethodDescriptor {
772 name: name.to_owned(),
773 direction: ExtensionDirection::ClientToServer,
774 http_era_disposition: Some(ExtensionHttpEraDisposition::ModernExclusive),
775 legacy_fallback: false,
776 }
777}
778
779#[derive(Clone, Debug, Eq, PartialEq)]
781pub struct ExtensionRegistryReceipt {
782 digest: [u8; 32],
783 descriptor_count: usize,
784}
785
786impl ExtensionRegistryReceipt {
787 #[must_use]
789 pub const fn digest(&self) -> &[u8; 32] {
790 &self.digest
791 }
792
793 #[must_use]
795 pub const fn descriptor_count(&self) -> usize {
796 self.descriptor_count
797 }
798}
799
800#[derive(Clone, Debug, Eq, PartialEq)]
802pub enum ExtensionRegistryError {
803 InvalidIdentifier(String),
805 ReservedNamespace(String),
807 SettingsNotObject,
809 SettingsCodecRejected,
811 OfficialMcpAppsServerSettingsNotEmpty,
813 OfficialMcpAppsDescriptorMismatch,
815 SettingsTooManyEntries,
817 SettingsKeyTooLong,
819 SettingsTooLarge,
821 SettingsTooDeep,
823 MissingOwner(&'static str),
825 DuplicateExtensionId(String),
827 UnregisteredExtensionId(String),
829 OwnershipCollision { field: &'static str, value: String },
831 MissingHttpEraDisposition(String),
833 LegacyFallbackContradiction(String),
835 CoreMethodCollision(String),
837 CoreNotificationCollision(String),
839 CoreResultDiscriminatorCollision(String),
841 MemberNameTooLong { field: &'static str, value: String },
843 LocalOwnershipCollision { field: &'static str, value: String },
845 Frozen,
847 DigestTooLarge,
849}
850
851impl fmt::Display for ExtensionRegistryError {
852 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
853 match self {
854 Self::InvalidIdentifier(value) => {
855 write!(formatter, "invalid extension identifier: {value}")
856 }
857 Self::ReservedNamespace(value) => {
858 write!(formatter, "reserved extension namespace: {value}")
859 }
860 Self::SettingsNotObject => {
861 formatter.write_str("extension settings must be a JSON object")
862 }
863 Self::SettingsCodecRejected => {
864 formatter.write_str("extension settings codec rejected object")
865 }
866 Self::OfficialMcpAppsServerSettingsNotEmpty => {
867 formatter.write_str("official MCP Apps server settings must be empty")
868 }
869 Self::OfficialMcpAppsDescriptorMismatch => {
870 formatter.write_str("official MCP Apps descriptor differs from its frozen shape")
871 }
872 Self::SettingsTooManyEntries => {
873 formatter.write_str("extension settings exceed their entry limit")
874 }
875 Self::SettingsKeyTooLong => {
876 formatter.write_str("extension settings key exceeds its byte limit")
877 }
878 Self::SettingsTooLarge => {
879 formatter.write_str("extension settings value exceeds its byte limit")
880 }
881 Self::SettingsTooDeep => {
882 formatter.write_str("extension settings exceed their nesting limit")
883 }
884 Self::MissingOwner(field) => {
885 write!(formatter, "missing extension descriptor owner: {field}")
886 }
887 Self::DuplicateExtensionId(value) => {
888 write!(formatter, "duplicate extension identifier: {value}")
889 }
890 Self::UnregisteredExtensionId(value) => {
891 write!(formatter, "unregistered extension identifier: {value}")
892 }
893 Self::OwnershipCollision { field, value } => {
894 write!(formatter, "extension {field} ownership collision: {value}")
895 }
896 Self::MissingHttpEraDisposition(value) => write!(
897 formatter,
898 "client-to-server method has no HTTP-era disposition: {value}"
899 ),
900 Self::LegacyFallbackContradiction(value) => write!(
901 formatter,
902 "legacy fallback contradicts HTTP-era disposition: {value}"
903 ),
904 Self::CoreMethodCollision(value) => write!(
905 formatter,
906 "extension method collides with legacy/shared core method: {value}"
907 ),
908 Self::CoreNotificationCollision(value) => write!(
909 formatter,
910 "extension notification collides with legacy/shared core notification: {value}"
911 ),
912 Self::CoreResultDiscriminatorCollision(value) => write!(
913 formatter,
914 "extension result discriminator collides with final-core result: {value}"
915 ),
916 Self::MemberNameTooLong { field, value } => write!(
917 formatter,
918 "extension {field} exceeds its byte limit: {value}"
919 ),
920 Self::LocalOwnershipCollision { field, value } => write!(
921 formatter,
922 "extension {field} has incompatible local ownership: {value}"
923 ),
924 Self::Frozen => formatter.write_str("extension descriptor registry is frozen"),
925 Self::DigestTooLarge => {
926 formatter.write_str("extension descriptor registry digest subject exceeds bound")
927 }
928 }
929 }
930}
931
932impl std::error::Error for ExtensionRegistryError {}
933
934#[derive(Clone, Debug, Default, Eq, PartialEq)]
939pub struct ExtensionLocalEnablement {
940 compiled: BTreeSet<ExtensionId>,
941 runtime: BTreeSet<ExtensionId>,
942}
943
944impl ExtensionLocalEnablement {
945 pub fn enable(&mut self, id: ExtensionId) {
947 self.compiled.insert(id.clone());
948 self.runtime.insert(id);
949 }
950
951 pub fn set_compiled(&mut self, id: ExtensionId, enabled: bool) {
953 set_enabled(&mut self.compiled, id, enabled);
954 }
955
956 pub fn set_runtime(&mut self, id: ExtensionId, enabled: bool) {
958 set_enabled(&mut self.runtime, id, enabled);
959 }
960
961 #[must_use]
963 pub fn is_enabled(&self, id: &ExtensionId) -> bool {
964 self.compiled.contains(id) && self.runtime.contains(id)
965 }
966
967 fn configured_ids(&self) -> impl Iterator<Item = &ExtensionId> {
968 self.compiled.iter().chain(self.runtime.iter())
969 }
970}
971
972fn set_enabled(set: &mut BTreeSet<ExtensionId>, id: ExtensionId, enabled: bool) {
973 if enabled {
974 set.insert(id);
975 } else {
976 set.remove(&id);
977 }
978}
979
980#[derive(Clone, Copy, Debug, Eq, PartialEq)]
982pub enum ExtensionPeer {
983 Client,
985 Server,
987}
988
989#[derive(Clone, Copy, Debug, Eq, PartialEq)]
991pub enum ExtensionInactiveReason {
992 LocallyDisabled,
994 NotAdvertised,
996 ServerInactiveFallback,
998 ClientInactiveFallback,
1000 SettingsInactiveFallback,
1002}
1003
1004#[derive(Clone, Debug, PartialEq)]
1010pub struct EffectiveExtensionSettings {
1011 settings: ExtensionSettings,
1012 fingerprint: [u8; 32],
1013}
1014
1015impl EffectiveExtensionSettings {
1016 #[must_use]
1018 pub const fn settings(&self) -> &ExtensionSettings {
1019 &self.settings
1020 }
1021
1022 #[must_use]
1024 pub const fn fingerprint(&self) -> &[u8; 32] {
1025 &self.fingerprint
1026 }
1027}
1028
1029#[derive(Clone, Debug, PartialEq)]
1031pub struct NegotiatedExtension {
1032 id: ExtensionId,
1033 effective_settings: EffectiveExtensionSettings,
1034}
1035
1036impl NegotiatedExtension {
1037 #[must_use]
1039 pub const fn id(&self) -> &ExtensionId {
1040 &self.id
1041 }
1042
1043 #[must_use]
1045 pub const fn effective_settings(&self) -> &EffectiveExtensionSettings {
1046 &self.effective_settings
1047 }
1048}
1049
1050#[derive(Clone, Debug, PartialEq)]
1052pub struct NegotiatedExtensionSet {
1053 registry_receipt: ExtensionRegistryReceipt,
1054 protocol_era: ProtocolEra,
1055 active: BTreeMap<ExtensionId, NegotiatedExtension>,
1056 inactive: BTreeMap<ExtensionId, ExtensionInactiveReason>,
1057 unknown_client: BTreeMap<ExtensionId, ExtensionSettings>,
1058 unknown_server: BTreeMap<ExtensionId, ExtensionSettings>,
1059}
1060
1061#[derive(Clone, Debug, Eq, PartialEq)]
1068pub struct McpAppsActivationReceipt {
1069 registry_digest: [u8; 32],
1070 effective_settings_fingerprint: [u8; 32],
1071}
1072
1073impl NegotiatedExtensionSet {
1074 #[must_use]
1076 pub const fn registry_receipt(&self) -> &ExtensionRegistryReceipt {
1077 &self.registry_receipt
1078 }
1079
1080 #[must_use]
1082 pub const fn protocol_era(&self) -> ProtocolEra {
1083 self.protocol_era
1084 }
1085
1086 #[must_use]
1088 pub fn active(&self, id: &ExtensionId) -> Option<&NegotiatedExtension> {
1089 self.active.get(id)
1090 }
1091
1092 #[must_use]
1094 pub fn inactive_reason(&self, id: &ExtensionId) -> Option<ExtensionInactiveReason> {
1095 self.inactive.get(id).copied()
1096 }
1097
1098 #[must_use]
1100 pub fn active_extensions(&self) -> impl ExactSizeIterator<Item = &NegotiatedExtension> {
1101 self.active.values()
1102 }
1103
1104 #[must_use]
1111 pub fn mcp_apps_activation_receipt(
1112 &self,
1113 registry: &ExtensionDescriptorRegistry,
1114 ) -> Option<McpAppsActivationReceipt> {
1115 if self.protocol_era != ProtocolEra::Modern2026 || self.ensure_registry(registry).is_err() {
1116 return None;
1117 }
1118 let id = official_mcp_apps_extension_id();
1119 let descriptor = registry.descriptor(&id)?;
1120 if validate_official_mcp_apps_descriptor(descriptor).is_err() {
1121 return None;
1122 }
1123 let active = self.active(&id)?;
1124 let settings =
1125 McpAppsClientSettings::from_extension_settings(active.effective_settings().settings())
1126 .ok()?;
1127 if !settings.supports_mcp_apps_html() {
1128 return None;
1129 }
1130 Some(McpAppsActivationReceipt {
1131 registry_digest: *self.registry_receipt.digest(),
1132 effective_settings_fingerprint: *active.effective_settings().fingerprint(),
1133 })
1134 }
1135
1136 #[must_use]
1138 pub const fn unknown_client_extensions(&self) -> &BTreeMap<ExtensionId, ExtensionSettings> {
1139 &self.unknown_client
1140 }
1141
1142 #[must_use]
1144 pub const fn unknown_server_extensions(&self) -> &BTreeMap<ExtensionId, ExtensionSettings> {
1145 &self.unknown_server
1146 }
1147}
1148
1149#[derive(Clone, Debug, Eq, PartialEq)]
1151pub enum ExtensionNegotiationError {
1152 LegacyProtocolExcluded,
1154 RegistryNotFrozen,
1156 UnregisteredLocalEnablement(String),
1158 DiscoveryTooManyExtensions(ExtensionPeer),
1160 OneSidedSupport { id: String, missing: ExtensionPeer },
1162 SettingsCompatibilityRejected(String),
1164 EffectiveSettingsTooLarge(String),
1166}
1167
1168impl fmt::Display for ExtensionNegotiationError {
1169 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1170 match self {
1171 Self::LegacyProtocolExcluded => {
1172 formatter.write_str("extensions are excluded from exact MCP 2024-11-05")
1173 }
1174 Self::RegistryNotFrozen => {
1175 formatter.write_str("extension descriptor registry is not frozen")
1176 }
1177 Self::UnregisteredLocalEnablement(id) => {
1178 write!(
1179 formatter,
1180 "local extension enablement has no descriptor: {id}"
1181 )
1182 }
1183 Self::DiscoveryTooManyExtensions(peer) => {
1184 write!(
1185 formatter,
1186 "{peer:?} extension discovery exceeds its entry limit"
1187 )
1188 }
1189 Self::OneSidedSupport { id, missing } => {
1190 write!(formatter, "extension {id} is missing {missing:?} support")
1191 }
1192 Self::SettingsCompatibilityRejected(id) => {
1193 write!(formatter, "extension settings are incompatible: {id}")
1194 }
1195 Self::EffectiveSettingsTooLarge(id) => {
1196 write!(
1197 formatter,
1198 "effective extension settings exceed their bound: {id}"
1199 )
1200 }
1201 }
1202 }
1203}
1204
1205impl std::error::Error for ExtensionNegotiationError {}
1206
1207#[derive(Clone, Debug, PartialEq)]
1209pub enum ExtensionSettingsResolution {
1210 Active(ExtensionSettings),
1212 Inactive,
1214}
1215
1216pub trait ExtensionSettingsCompatibilityResolver {
1222 fn resolve(
1224 &mut self,
1225 descriptor: &ExtensionDescriptor,
1226 client: &ExtensionSettings,
1227 server: &ExtensionSettings,
1228 ) -> Result<ExtensionSettings, ExtensionNegotiationError>;
1229
1230 fn resolve_with_disposition(
1236 &mut self,
1237 descriptor: &ExtensionDescriptor,
1238 client: &ExtensionSettings,
1239 server: &ExtensionSettings,
1240 ) -> Result<ExtensionSettingsResolution, ExtensionNegotiationError> {
1241 self.resolve(descriptor, client, server)
1242 .map(ExtensionSettingsResolution::Active)
1243 }
1244}
1245
1246impl<F> ExtensionSettingsCompatibilityResolver for F
1247where
1248 F: FnMut(
1249 &ExtensionDescriptor,
1250 &ExtensionSettings,
1251 &ExtensionSettings,
1252 ) -> Result<ExtensionSettings, ExtensionNegotiationError>,
1253{
1254 fn resolve(
1255 &mut self,
1256 descriptor: &ExtensionDescriptor,
1257 client: &ExtensionSettings,
1258 server: &ExtensionSettings,
1259 ) -> Result<ExtensionSettings, ExtensionNegotiationError> {
1260 self(descriptor, client, server)
1261 }
1262}
1263
1264impl ExtensionSettingsCompatibilityResolver for RejectingExtensionNegotiationResolver {
1265 fn resolve(
1266 &mut self,
1267 descriptor: &ExtensionDescriptor,
1268 _client: &ExtensionSettings,
1269 _server: &ExtensionSettings,
1270 ) -> Result<ExtensionSettings, ExtensionNegotiationError> {
1271 Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
1272 descriptor.id.to_string(),
1273 ))
1274 }
1275}
1276
1277#[cfg(feature = "tasks")]
1278impl ExtensionSettingsCompatibilityResolver for OfficialTasksNegotiationResolver {
1279 fn resolve(
1280 &mut self,
1281 descriptor: &ExtensionDescriptor,
1282 client: &ExtensionSettings,
1283 server: &ExtensionSettings,
1284 ) -> Result<ExtensionSettings, ExtensionNegotiationError> {
1285 if descriptor.id.as_str() == OFFICIAL_TASKS_EXTENSION_ID {
1286 resolve_official_tasks_settings(descriptor, client, server)
1287 } else {
1288 Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
1289 descriptor.id.to_string(),
1290 ))
1291 }
1292 }
1293}
1294
1295#[cfg(feature = "tasks")]
1296impl<R> ExtensionSettingsCompatibilityResolver for TasksNegotiationResolver<R>
1297where
1298 R: ExtensionSettingsCompatibilityResolver,
1299{
1300 fn resolve(
1301 &mut self,
1302 descriptor: &ExtensionDescriptor,
1303 client: &ExtensionSettings,
1304 server: &ExtensionSettings,
1305 ) -> Result<ExtensionSettings, ExtensionNegotiationError> {
1306 match self.resolve_with_disposition(descriptor, client, server)? {
1307 ExtensionSettingsResolution::Active(settings) => Ok(settings),
1308 ExtensionSettingsResolution::Inactive => Err(
1309 ExtensionNegotiationError::SettingsCompatibilityRejected(descriptor.id.to_string()),
1310 ),
1311 }
1312 }
1313
1314 fn resolve_with_disposition(
1315 &mut self,
1316 descriptor: &ExtensionDescriptor,
1317 client: &ExtensionSettings,
1318 server: &ExtensionSettings,
1319 ) -> Result<ExtensionSettingsResolution, ExtensionNegotiationError> {
1320 if descriptor.id.as_str() == OFFICIAL_TASKS_EXTENSION_ID {
1321 resolve_official_tasks_settings(descriptor, client, server)
1322 .map(ExtensionSettingsResolution::Active)
1323 } else {
1324 self.fallback
1325 .resolve_with_disposition(descriptor, client, server)
1326 }
1327 }
1328}
1329
1330impl<R> ExtensionSettingsCompatibilityResolver for McpAppsNegotiationResolver<R>
1331where
1332 R: ExtensionSettingsCompatibilityResolver,
1333{
1334 fn resolve(
1335 &mut self,
1336 descriptor: &ExtensionDescriptor,
1337 client: &ExtensionSettings,
1338 server: &ExtensionSettings,
1339 ) -> Result<ExtensionSettings, ExtensionNegotiationError> {
1340 match self.resolve_with_disposition(descriptor, client, server)? {
1341 ExtensionSettingsResolution::Active(settings) => Ok(settings),
1342 ExtensionSettingsResolution::Inactive => Err(
1343 ExtensionNegotiationError::SettingsCompatibilityRejected(descriptor.id.to_string()),
1344 ),
1345 }
1346 }
1347
1348 fn resolve_with_disposition(
1349 &mut self,
1350 descriptor: &ExtensionDescriptor,
1351 client: &ExtensionSettings,
1352 server: &ExtensionSettings,
1353 ) -> Result<ExtensionSettingsResolution, ExtensionNegotiationError> {
1354 if descriptor.id.as_str() == OFFICIAL_MCP_APPS_EXTENSION_ID {
1355 resolve_official_mcp_apps_settings(descriptor, client, server)
1356 } else {
1357 self.fallback
1358 .resolve_with_disposition(descriptor, client, server)
1359 }
1360 }
1361}
1362
1363#[derive(Clone, Debug, Eq, PartialEq)]
1365pub enum ExtensionDispatchError {
1366 LegacyProtocolExcluded,
1368 ProtocolEraMismatch {
1370 negotiated: ProtocolEra,
1372 request: ProtocolEra,
1374 },
1375 RegistryReceiptMismatch,
1377 InactiveCapability(String),
1379 CapabilityDoesNotOwn {
1381 capability: String,
1383 field: &'static str,
1385 value: String,
1387 },
1388 NameTooLong(String),
1390 NoActiveOwner { field: &'static str, value: String },
1392 DirectionMismatch {
1394 field: &'static str,
1395 value: String,
1396 expected: ExtensionDirection,
1397 actual: ExtensionDirection,
1398 },
1399 AmbiguousActiveOwner { field: &'static str, value: String },
1401}
1402
1403impl fmt::Display for ExtensionDispatchError {
1404 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1405 match self {
1406 Self::LegacyProtocolExcluded => {
1407 formatter.write_str("extensions are excluded from exact MCP 2024-11-05")
1408 }
1409 Self::ProtocolEraMismatch {
1410 negotiated,
1411 request,
1412 } => write!(
1413 formatter,
1414 "extension request era {request:?} does not match negotiated era {negotiated:?}"
1415 ),
1416 Self::RegistryReceiptMismatch => {
1417 formatter.write_str("extension dispatch registry does not match negotiation")
1418 }
1419 Self::InactiveCapability(capability) => {
1420 write!(
1421 formatter,
1422 "extension capability is not active: {capability}"
1423 )
1424 }
1425 Self::CapabilityDoesNotOwn {
1426 capability,
1427 field,
1428 value,
1429 } => write!(
1430 formatter,
1431 "extension capability {capability} does not own {field}: {value}"
1432 ),
1433 Self::NameTooLong(value) => {
1434 write!(
1435 formatter,
1436 "extension dispatch name exceeds its byte limit: {value}"
1437 )
1438 }
1439 Self::NoActiveOwner { field, value } => {
1440 write!(formatter, "no active extension owns {field}: {value}")
1441 }
1442 Self::DirectionMismatch {
1443 field,
1444 value,
1445 expected,
1446 actual,
1447 } => write!(
1448 formatter,
1449 "extension {field} has direction {actual:?}, not {expected:?}: {value}"
1450 ),
1451 Self::AmbiguousActiveOwner { field, value } => {
1452 write!(formatter, "multiple active extensions own {field}: {value}")
1453 }
1454 }
1455 }
1456}
1457
1458impl std::error::Error for ExtensionDispatchError {}
1459
1460#[derive(Clone, Debug, Default)]
1462pub struct ExtensionDescriptorRegistry {
1463 descriptors: BTreeMap<ExtensionId, ExtensionDescriptor>,
1464 additional_methods: BTreeMap<ExtensionId, BTreeMap<String, ExtensionMethodDescriptor>>,
1465 receipt: Option<ExtensionRegistryReceipt>,
1466}
1467
1468impl ExtensionDescriptorRegistry {
1469 #[must_use]
1471 pub fn new() -> Self {
1472 Self::default()
1473 }
1474
1475 pub fn register(
1477 &mut self,
1478 descriptor: ExtensionDescriptor,
1479 ) -> Result<(), ExtensionRegistryError> {
1480 if self.receipt.is_some() {
1481 return Err(ExtensionRegistryError::Frozen);
1482 }
1483 if self.descriptors.len() >= MAX_EXTENSION_DESCRIPTORS {
1484 return Err(ExtensionRegistryError::DigestTooLarge);
1485 }
1486 validate_descriptor(&descriptor)?;
1487 if self.descriptors.contains_key(&descriptor.id) {
1488 return Err(ExtensionRegistryError::DuplicateExtensionId(
1489 descriptor.id.to_string(),
1490 ));
1491 }
1492 for existing in self.descriptors.values() {
1493 ensure_no_cross_owner_collision(existing, &descriptor)?;
1494 }
1495 if let Some(method) = &descriptor.method {
1496 for (existing_id, methods) in &self.additional_methods {
1497 if methods.contains_key(&method.name) {
1498 return Err(ExtensionRegistryError::OwnershipCollision {
1499 field: "method",
1500 value: method.name.clone(),
1501 });
1502 }
1503 if self
1504 .descriptors
1505 .get(existing_id)
1506 .and_then(|existing| existing.notification.as_ref())
1507 .is_some_and(|notification| notification.name == method.name)
1508 {
1509 return Err(ExtensionRegistryError::OwnershipCollision {
1510 field: "method/notification",
1511 value: method.name.clone(),
1512 });
1513 }
1514 }
1515 }
1516 if let Some(notification) = &descriptor.notification {
1517 for methods in self.additional_methods.values() {
1518 if methods.contains_key(¬ification.name) {
1519 return Err(ExtensionRegistryError::OwnershipCollision {
1520 field: "method/notification",
1521 value: notification.name.clone(),
1522 });
1523 }
1524 }
1525 }
1526 self.descriptors.insert(descriptor.id.clone(), descriptor);
1527 Ok(())
1528 }
1529
1530 pub fn register_method(
1537 &mut self,
1538 id: &ExtensionId,
1539 method: ExtensionMethodDescriptor,
1540 ) -> Result<(), ExtensionRegistryError> {
1541 if self.receipt.is_some() {
1542 return Err(ExtensionRegistryError::Frozen);
1543 }
1544 let Some(descriptor) = self.descriptors.get(id) else {
1545 return Err(ExtensionRegistryError::UnregisteredExtensionId(
1546 id.to_string(),
1547 ));
1548 };
1549 validate_extension_method(&method)?;
1550 if descriptor
1551 .method
1552 .as_ref()
1553 .is_some_and(|registered| registered.name == method.name)
1554 || self
1555 .additional_methods
1556 .get(id)
1557 .is_some_and(|methods| methods.contains_key(&method.name))
1558 {
1559 return Err(ExtensionRegistryError::LocalOwnershipCollision {
1560 field: "method",
1561 value: method.name,
1562 });
1563 }
1564 if descriptor
1565 .notification
1566 .as_ref()
1567 .is_some_and(|notification| notification.name == method.name)
1568 {
1569 return Err(ExtensionRegistryError::LocalOwnershipCollision {
1570 field: "method/notification",
1571 value: method.name,
1572 });
1573 }
1574 for (existing_id, existing) in &self.descriptors {
1575 if existing_id == id {
1576 continue;
1577 }
1578 if existing
1579 .method
1580 .as_ref()
1581 .is_some_and(|registered| registered.name == method.name)
1582 || self
1583 .additional_methods
1584 .get(existing_id)
1585 .is_some_and(|methods| methods.contains_key(&method.name))
1586 {
1587 return Err(ExtensionRegistryError::OwnershipCollision {
1588 field: "method",
1589 value: method.name,
1590 });
1591 }
1592 if existing
1593 .notification
1594 .as_ref()
1595 .is_some_and(|notification| notification.name == method.name)
1596 {
1597 return Err(ExtensionRegistryError::OwnershipCollision {
1598 field: "method/notification",
1599 value: method.name,
1600 });
1601 }
1602 }
1603 self.additional_methods
1604 .entry(id.clone())
1605 .or_default()
1606 .insert(method.name.clone(), method);
1607 Ok(())
1608 }
1609
1610 pub fn freeze(&mut self) -> Result<ExtensionRegistryReceipt, ExtensionRegistryError> {
1612 if let Some(receipt) = &self.receipt {
1613 return Ok(receipt.clone());
1614 }
1615 let canonical = self.canonical_subject()?;
1616 let digest = sha256_bounded(canonical.as_bytes(), MAX_EXTENSION_REGISTRY_CANONICAL_BYTES)
1617 .map_err(|_| ExtensionRegistryError::DigestTooLarge)?
1618 .into_bytes();
1619 let receipt = ExtensionRegistryReceipt {
1620 digest,
1621 descriptor_count: self.descriptors.len(),
1622 };
1623 self.receipt = Some(receipt.clone());
1624 Ok(receipt)
1625 }
1626
1627 #[must_use]
1629 pub fn receipt(&self) -> Option<&ExtensionRegistryReceipt> {
1630 self.receipt.as_ref()
1631 }
1632
1633 #[must_use]
1635 pub fn descriptor(&self, id: &ExtensionId) -> Option<&ExtensionDescriptor> {
1636 self.descriptors.get(id)
1637 }
1638
1639 fn method(&self, id: &ExtensionId, name: &str) -> Option<&ExtensionMethodDescriptor> {
1640 self.descriptors
1641 .get(id)
1642 .and_then(|descriptor| {
1643 descriptor
1644 .method
1645 .as_ref()
1646 .filter(|method| method.name == name)
1647 })
1648 .or_else(|| self.additional_methods.get(id)?.get(name))
1649 }
1650
1651 #[must_use]
1658 pub fn method_descriptor(
1659 &self,
1660 id: &ExtensionId,
1661 name: &str,
1662 ) -> Option<&ExtensionMethodDescriptor> {
1663 self.method(id, name)
1664 }
1665
1666 #[must_use]
1668 pub fn descriptors(&self) -> impl ExactSizeIterator<Item = &ExtensionDescriptor> {
1669 self.descriptors.values()
1670 }
1671
1672 #[must_use]
1674 pub fn preserve_unknown_peer_extensions(
1675 &self,
1676 peer: BTreeMap<ExtensionId, ExtensionSettings>,
1677 ) -> BTreeMap<ExtensionId, ExtensionSettings> {
1678 peer.into_iter()
1679 .filter(|(id, _)| !self.descriptors.contains_key(id))
1680 .collect()
1681 }
1682
1683 pub fn negotiate<R>(
1690 &self,
1691 protocol_era: ProtocolEra,
1692 local: &ExtensionLocalEnablement,
1693 client: &ClientExtensionDiscovery,
1694 server: &ServerExtensionDiscovery,
1695 resolver: &mut R,
1696 ) -> Result<NegotiatedExtensionSet, ExtensionNegotiationError>
1697 where
1698 R: ExtensionSettingsCompatibilityResolver,
1699 {
1700 if matches!(protocol_era, ProtocolEra::Legacy2024) {
1701 return Err(ExtensionNegotiationError::LegacyProtocolExcluded);
1702 }
1703 let Some(receipt) = self.receipt.clone() else {
1704 return Err(ExtensionNegotiationError::RegistryNotFrozen);
1705 };
1706 validate_discovery(&client.extensions, ExtensionPeer::Client)?;
1707 validate_discovery(&server.extensions, ExtensionPeer::Server)?;
1708 for id in local.configured_ids() {
1709 if !self.descriptors.contains_key(id) {
1710 return Err(ExtensionNegotiationError::UnregisteredLocalEnablement(
1711 id.to_string(),
1712 ));
1713 }
1714 }
1715
1716 let unknown_client = self.preserve_unknown_peer_extensions(client.extensions.clone());
1717 let unknown_server = self.preserve_unknown_peer_extensions(server.extensions.clone());
1718 let mut active = BTreeMap::new();
1719 let mut inactive = BTreeMap::new();
1720
1721 for descriptor in self.descriptors.values() {
1722 let id = &descriptor.id;
1723 if !local.is_enabled(id) {
1724 inactive.insert(id.clone(), ExtensionInactiveReason::LocallyDisabled);
1725 continue;
1726 }
1727
1728 match (client.extensions.get(id), server.extensions.get(id)) {
1729 (Some(client), Some(server)) => {
1730 #[cfg(feature = "tasks")]
1731 {
1732 enforce_official_tasks_empty_settings(id, client)?;
1733 enforce_official_tasks_empty_settings(id, server)?;
1734 }
1735 match resolver.resolve_with_disposition(descriptor, client, server)? {
1736 ExtensionSettingsResolution::Active(effective) => {
1737 #[cfg(feature = "tasks")]
1738 enforce_official_tasks_empty_settings(id, &effective)?;
1739 let fingerprint =
1740 effective_settings_fingerprint(descriptor, &effective)?;
1741 active.insert(
1742 id.clone(),
1743 NegotiatedExtension {
1744 id: id.clone(),
1745 effective_settings: EffectiveExtensionSettings {
1746 settings: effective,
1747 fingerprint,
1748 },
1749 },
1750 );
1751 }
1752 ExtensionSettingsResolution::Inactive => {
1753 inactive.insert(
1754 id.clone(),
1755 ExtensionInactiveReason::SettingsInactiveFallback,
1756 );
1757 }
1758 }
1759 }
1760 (None, None) => {
1761 inactive.insert(id.clone(), ExtensionInactiveReason::NotAdvertised);
1762 }
1763 (None, Some(_)) => match descriptor.resolver.fallback {
1764 ExtensionFallbackPolicy::ServerInactiveFallback
1765 | ExtensionFallbackPolicy::InactiveOnEitherPeer => {
1766 inactive
1767 .insert(id.clone(), ExtensionInactiveReason::ServerInactiveFallback);
1768 }
1769 ExtensionFallbackPolicy::RejectOneSided
1770 | ExtensionFallbackPolicy::ClientInactiveFallback => {
1771 return Err(ExtensionNegotiationError::OneSidedSupport {
1772 id: id.to_string(),
1773 missing: ExtensionPeer::Client,
1774 });
1775 }
1776 },
1777 (Some(_), None) => match descriptor.resolver.fallback {
1778 ExtensionFallbackPolicy::ClientInactiveFallback
1779 | ExtensionFallbackPolicy::InactiveOnEitherPeer => {
1780 inactive
1781 .insert(id.clone(), ExtensionInactiveReason::ClientInactiveFallback);
1782 }
1783 ExtensionFallbackPolicy::RejectOneSided
1784 | ExtensionFallbackPolicy::ServerInactiveFallback => {
1785 return Err(ExtensionNegotiationError::OneSidedSupport {
1786 id: id.to_string(),
1787 missing: ExtensionPeer::Server,
1788 });
1789 }
1790 },
1791 }
1792 }
1793
1794 Ok(NegotiatedExtensionSet {
1795 registry_receipt: receipt,
1796 protocol_era,
1797 active,
1798 inactive,
1799 unknown_client,
1800 unknown_server,
1801 })
1802 }
1803
1804 fn canonical_subject(&self) -> Result<String, ExtensionRegistryError> {
1805 let rows = self
1806 .descriptors
1807 .values()
1808 .map(|descriptor| {
1809 canonical_descriptor_row(descriptor, self.additional_methods.get(&descriptor.id))
1810 })
1811 .collect::<Vec<_>>();
1812 let json = serde_json::to_string(&("fastmcp.ext-01.descriptor-registry.v1", rows))
1813 .map_err(|_| ExtensionRegistryError::DigestTooLarge)?;
1814 let subject = json.replace('"', r#"\""#);
1818 if subject.len() > MAX_EXTENSION_REGISTRY_CANONICAL_BYTES {
1819 return Err(ExtensionRegistryError::DigestTooLarge);
1820 }
1821 Ok(subject)
1822 }
1823}
1824
1825#[cfg(feature = "tasks")]
1826fn enforce_official_tasks_empty_settings(
1827 id: &ExtensionId,
1828 settings: &ExtensionSettings,
1829) -> Result<(), ExtensionNegotiationError> {
1830 if id.as_str() != OFFICIAL_TASKS_EXTENSION_ID || settings.as_object().is_empty() {
1831 return Ok(());
1832 }
1833 Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
1834 id.to_string(),
1835 ))
1836}
1837
1838fn validate_discovery(
1839 extensions: &BTreeMap<ExtensionId, ExtensionSettings>,
1840 peer: ExtensionPeer,
1841) -> Result<(), ExtensionNegotiationError> {
1842 if extensions.len() > MAX_EXTENSION_DESCRIPTORS {
1843 return Err(ExtensionNegotiationError::DiscoveryTooManyExtensions(peer));
1844 }
1845 Ok(())
1846}
1847
1848fn effective_settings_fingerprint(
1849 descriptor: &ExtensionDescriptor,
1850 effective: &ExtensionSettings,
1851) -> Result<[u8; 32], ExtensionNegotiationError> {
1852 let subject = serde_json::to_vec(&serde_json::json!({
1853 "domain": "fastmcp.ext-01.effective-settings.v1",
1854 "id": descriptor.id.as_str(),
1855 "resolver": [descriptor.resolver.id, descriptor.resolver.version],
1856 "clientSchema": descriptor.client_settings.schema_id,
1857 "serverSchema": descriptor.server_settings.schema_id,
1858 "effective": canonicalize_value(&Value::Object(effective.as_object().clone())),
1859 }))
1860 .map_err(|_| ExtensionNegotiationError::EffectiveSettingsTooLarge(descriptor.id.to_string()))?;
1861 if subject.len() > MAX_EXTENSION_REGISTRY_CANONICAL_BYTES {
1862 return Err(ExtensionNegotiationError::EffectiveSettingsTooLarge(
1863 descriptor.id.to_string(),
1864 ));
1865 }
1866 sha256_bounded(&subject, MAX_EXTENSION_REGISTRY_CANONICAL_BYTES)
1867 .map(|digest| digest.into_bytes())
1868 .map_err(|_| {
1869 ExtensionNegotiationError::EffectiveSettingsTooLarge(descriptor.id.to_string())
1870 })
1871}
1872
1873fn canonicalize_value(value: &Value) -> Value {
1874 match value {
1875 Value::Array(values) => Value::Array(values.iter().map(canonicalize_value).collect()),
1876 Value::Object(values) => Value::Object(
1877 values
1878 .iter()
1879 .map(|(key, value)| (key.clone(), canonicalize_value(value)))
1880 .collect(),
1881 ),
1882 Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => value.clone(),
1883 }
1884}
1885
1886impl NegotiatedExtensionSet {
1887 pub fn admit_capability<'a>(
1889 &self,
1890 registry: &'a ExtensionDescriptorRegistry,
1891 request_era: ProtocolEra,
1892 capability: &ExtensionId,
1893 ) -> Result<&'a ExtensionDescriptor, ExtensionDispatchError> {
1894 self.ensure_request_era(request_era)?;
1895 self.ensure_registry(registry)?;
1896 if !self.active.contains_key(capability) {
1897 return Err(ExtensionDispatchError::InactiveCapability(
1898 capability.to_string(),
1899 ));
1900 }
1901 registry
1902 .descriptor(capability)
1903 .ok_or(ExtensionDispatchError::RegistryReceiptMismatch)
1904 }
1905
1906 pub fn admit_method<'a>(
1908 &self,
1909 registry: &'a ExtensionDescriptorRegistry,
1910 request_era: ProtocolEra,
1911 capability: &ExtensionId,
1912 name: &str,
1913 direction: ExtensionDirection,
1914 ) -> Result<&'a ExtensionDescriptor, ExtensionDispatchError> {
1915 validate_dispatch_name(name)?;
1916 let descriptor = self.admit_capability(registry, request_era, capability)?;
1917 let Some(method) = registry.method(capability, name) else {
1918 return Err(ExtensionDispatchError::CapabilityDoesNotOwn {
1919 capability: capability.to_string(),
1920 field: "method",
1921 value: name.to_owned(),
1922 });
1923 };
1924 if method.direction != direction {
1925 return Err(ExtensionDispatchError::DirectionMismatch {
1926 field: "method",
1927 value: name.to_owned(),
1928 expected: direction,
1929 actual: method.direction,
1930 });
1931 }
1932 Ok(descriptor)
1933 }
1934
1935 pub fn admit_notification<'a>(
1937 &self,
1938 registry: &'a ExtensionDescriptorRegistry,
1939 request_era: ProtocolEra,
1940 capability: &ExtensionId,
1941 name: &str,
1942 direction: ExtensionDirection,
1943 ) -> Result<&'a ExtensionDescriptor, ExtensionDispatchError> {
1944 validate_dispatch_name(name)?;
1945 let descriptor = self.admit_capability(registry, request_era, capability)?;
1946 let Some(notification) = descriptor.notification.as_ref() else {
1947 return Err(ExtensionDispatchError::CapabilityDoesNotOwn {
1948 capability: capability.to_string(),
1949 field: "notification",
1950 value: name.to_owned(),
1951 });
1952 };
1953 if notification.name != name {
1954 return Err(ExtensionDispatchError::CapabilityDoesNotOwn {
1955 capability: capability.to_string(),
1956 field: "notification",
1957 value: name.to_owned(),
1958 });
1959 }
1960 if notification.direction != direction {
1961 return Err(ExtensionDispatchError::DirectionMismatch {
1962 field: "notification",
1963 value: name.to_owned(),
1964 expected: direction,
1965 actual: notification.direction,
1966 });
1967 }
1968 Ok(descriptor)
1969 }
1970
1971 pub fn admit_result_discriminator<'a>(
1973 &self,
1974 registry: &'a ExtensionDescriptorRegistry,
1975 request_era: ProtocolEra,
1976 capability: &ExtensionId,
1977 discriminator: &str,
1978 ) -> Result<&'a ExtensionDescriptor, ExtensionDispatchError> {
1979 validate_dispatch_name(discriminator)?;
1980 let descriptor = self.admit_capability(registry, request_era, capability)?;
1981 if descriptor.result_discriminator.as_deref() != Some(discriminator) {
1982 return Err(ExtensionDispatchError::CapabilityDoesNotOwn {
1983 capability: capability.to_string(),
1984 field: "result discriminator",
1985 value: discriminator.to_owned(),
1986 });
1987 }
1988 Ok(descriptor)
1989 }
1990
1991 fn ensure_registry(
1992 &self,
1993 registry: &ExtensionDescriptorRegistry,
1994 ) -> Result<(), ExtensionDispatchError> {
1995 if registry.receipt() == Some(&self.registry_receipt) {
1996 Ok(())
1997 } else {
1998 Err(ExtensionDispatchError::RegistryReceiptMismatch)
1999 }
2000 }
2001
2002 fn ensure_request_era(&self, request_era: ProtocolEra) -> Result<(), ExtensionDispatchError> {
2003 if matches!(request_era, ProtocolEra::Legacy2024) {
2004 return Err(ExtensionDispatchError::LegacyProtocolExcluded);
2005 }
2006 if self.protocol_era != request_era {
2007 return Err(ExtensionDispatchError::ProtocolEraMismatch {
2008 negotiated: self.protocol_era,
2009 request: request_era,
2010 });
2011 }
2012 Ok(())
2013 }
2014}
2015
2016fn validate_dispatch_name(name: &str) -> Result<(), ExtensionDispatchError> {
2017 if name.len() > MAX_EXTENSION_MEMBER_NAME_BYTES {
2018 return Err(ExtensionDispatchError::NameTooLong(name.to_owned()));
2019 }
2020 Ok(())
2021}
2022
2023fn validate_descriptor(descriptor: &ExtensionDescriptor) -> Result<(), ExtensionRegistryError> {
2024 if descriptor.id.as_str() == OFFICIAL_MCP_APPS_EXTENSION_ID {
2025 validate_official_mcp_apps_descriptor(descriptor)?;
2026 }
2027 for (field, value) in [
2028 (
2029 "client settings schema",
2030 descriptor.client_settings.schema_id.as_str(),
2031 ),
2032 (
2033 "client settings codec",
2034 descriptor.client_settings.codec_id.as_str(),
2035 ),
2036 (
2037 "server settings schema",
2038 descriptor.server_settings.schema_id.as_str(),
2039 ),
2040 (
2041 "server settings codec",
2042 descriptor.server_settings.codec_id.as_str(),
2043 ),
2044 ("resolver", descriptor.resolver.id.as_str()),
2045 ] {
2046 validate_descriptor_identity(field, value)?;
2047 }
2048 if let Some(method) = &descriptor.method {
2049 validate_extension_method(method)?;
2050 }
2051 if let Some(notification) = &descriptor.notification {
2052 validate_member_name("notification", ¬ification.name)?;
2053 if core_or_legacy_method(¬ification.name) {
2054 return Err(ExtensionRegistryError::CoreNotificationCollision(
2055 notification.name.clone(),
2056 ));
2057 }
2058 if descriptor
2059 .method
2060 .as_ref()
2061 .is_some_and(|method| method.name == notification.name)
2062 {
2063 return Err(ExtensionRegistryError::LocalOwnershipCollision {
2064 field: "method/notification",
2065 value: notification.name.clone(),
2066 });
2067 }
2068 }
2069 if let Some(discriminator) = &descriptor.result_discriminator {
2070 validate_member_name("result discriminator", discriminator)?;
2071 if matches!(discriminator.as_str(), "complete" | "input_required") {
2072 return Err(ExtensionRegistryError::CoreResultDiscriminatorCollision(
2073 discriminator.clone(),
2074 ));
2075 }
2076 }
2077 if descriptor.routing_headers.len() > MAX_EXTENSION_ROUTING_HEADERS {
2078 return Err(ExtensionRegistryError::LocalOwnershipCollision {
2079 field: "routing headers",
2080 value: descriptor.id.to_string(),
2081 });
2082 }
2083 for (index, header) in descriptor.routing_headers.iter().enumerate() {
2084 if header.name.is_empty() {
2085 return Err(ExtensionRegistryError::MissingOwner("routing header"));
2086 }
2087 if header.name.len() > MAX_EXTENSION_ROUTING_HEADER_BYTES {
2088 return Err(ExtensionRegistryError::MemberNameTooLong {
2089 field: "routing header",
2090 value: header.name.clone(),
2091 });
2092 }
2093 if descriptor.routing_headers[..index]
2094 .iter()
2095 .any(|prior| prior.name.eq_ignore_ascii_case(&header.name))
2096 {
2097 return Err(ExtensionRegistryError::LocalOwnershipCollision {
2098 field: "routing header",
2099 value: header.name.clone(),
2100 });
2101 }
2102 }
2103 if let Some(correlation) = &descriptor.stdio_correlation {
2104 if correlation.metadata_key.is_empty() {
2105 return Err(ExtensionRegistryError::MissingOwner("stdio correlation"));
2106 }
2107 ExtensionId::parse(correlation.metadata_key.clone())?;
2108 if correlation.methods.is_empty()
2109 || correlation.methods.len() > MAX_STDIO_CORRELATION_METHODS
2110 {
2111 return Err(ExtensionRegistryError::MissingOwner("stdio correlation"));
2112 }
2113 let Some(notification) = &descriptor.notification else {
2114 return Err(ExtensionRegistryError::MissingOwner("stdio notification"));
2115 };
2116 if notification.direction != correlation.direction
2117 || !correlation
2118 .methods
2119 .iter()
2120 .any(|method| method == ¬ification.name)
2121 {
2122 return Err(ExtensionRegistryError::LocalOwnershipCollision {
2123 field: "stdio correlation notification",
2124 value: correlation.metadata_key.clone(),
2125 });
2126 }
2127 for (index, method) in correlation.methods.iter().enumerate() {
2128 validate_member_name("stdio correlation method", method)?;
2129 if correlation.methods[..index].contains(method) {
2130 return Err(ExtensionRegistryError::LocalOwnershipCollision {
2131 field: "stdio correlation method",
2132 value: method.clone(),
2133 });
2134 }
2135 }
2136 }
2137 Ok(())
2138}
2139
2140fn validate_extension_method(
2141 method: &ExtensionMethodDescriptor,
2142) -> Result<(), ExtensionRegistryError> {
2143 validate_member_name("method", &method.name)?;
2144 if core_or_legacy_method(&method.name) {
2145 return Err(ExtensionRegistryError::CoreMethodCollision(
2146 method.name.clone(),
2147 ));
2148 }
2149 if method.direction == ExtensionDirection::ClientToServer {
2150 if method.http_era_disposition.is_none() {
2151 return Err(ExtensionRegistryError::MissingHttpEraDisposition(
2152 method.name.clone(),
2153 ));
2154 }
2155 if method.legacy_fallback {
2156 return Err(ExtensionRegistryError::LegacyFallbackContradiction(
2157 method.name.clone(),
2158 ));
2159 }
2160 } else if method.http_era_disposition.is_some() {
2161 return Err(ExtensionRegistryError::MissingHttpEraDisposition(
2162 method.name.clone(),
2163 ));
2164 } else if method.legacy_fallback {
2165 return Err(ExtensionRegistryError::LegacyFallbackContradiction(
2166 method.name.clone(),
2167 ));
2168 }
2169 Ok(())
2170}
2171
2172fn validate_descriptor_identity(
2173 field: &'static str,
2174 value: &str,
2175) -> Result<(), ExtensionRegistryError> {
2176 if value.is_empty() {
2177 return Err(ExtensionRegistryError::MissingOwner(field));
2178 }
2179 if value.len() > MAX_EXTENSION_MEMBER_NAME_BYTES {
2180 return Err(ExtensionRegistryError::MemberNameTooLong {
2181 field,
2182 value: value.to_owned(),
2183 });
2184 }
2185 Ok(())
2186}
2187
2188fn validate_member_name(field: &'static str, value: &str) -> Result<(), ExtensionRegistryError> {
2189 if value.is_empty() {
2190 return Err(ExtensionRegistryError::MissingOwner(field));
2191 }
2192 if value.len() > MAX_EXTENSION_MEMBER_NAME_BYTES {
2193 return Err(ExtensionRegistryError::MemberNameTooLong {
2194 field,
2195 value: value.to_owned(),
2196 });
2197 }
2198 Ok(())
2199}
2200
2201fn ensure_no_cross_owner_collision(
2202 left: &ExtensionDescriptor,
2203 right: &ExtensionDescriptor,
2204) -> Result<(), ExtensionRegistryError> {
2205 let collision = |field: &'static str, left: Option<&str>, right: Option<&str>| {
2206 (left.zip(right).filter(|(a, b)| a == b)).map(|(value, _)| {
2207 ExtensionRegistryError::OwnershipCollision {
2208 field,
2209 value: value.to_owned(),
2210 }
2211 })
2212 };
2213 if let Some(error) = collision(
2214 "method",
2215 left.method.as_ref().map(|m| m.name.as_str()),
2216 right.method.as_ref().map(|m| m.name.as_str()),
2217 ) {
2218 return Err(error);
2219 }
2220 if let Some(error) = collision(
2221 "notification",
2222 left.notification.as_ref().map(|n| n.name.as_str()),
2223 right.notification.as_ref().map(|n| n.name.as_str()),
2224 ) {
2225 return Err(error);
2226 }
2227 if let Some(error) = collision(
2228 "method/notification",
2229 left.method.as_ref().map(|m| m.name.as_str()),
2230 right.notification.as_ref().map(|n| n.name.as_str()),
2231 ) {
2232 return Err(error);
2233 }
2234 if let Some(error) = collision(
2235 "method/notification",
2236 left.notification.as_ref().map(|n| n.name.as_str()),
2237 right.method.as_ref().map(|m| m.name.as_str()),
2238 ) {
2239 return Err(error);
2240 }
2241 if let Some(error) = collision(
2242 "result discriminator",
2243 left.result_discriminator.as_deref(),
2244 right.result_discriminator.as_deref(),
2245 ) {
2246 return Err(error);
2247 }
2248 for lhs in &left.routing_headers {
2249 for rhs in &right.routing_headers {
2250 if lhs.name.eq_ignore_ascii_case(&rhs.name) {
2251 return Err(ExtensionRegistryError::OwnershipCollision {
2252 field: "routing header",
2253 value: lhs.name.clone(),
2254 });
2255 }
2256 }
2257 }
2258 if let (Some(lhs), Some(rhs)) = (&left.stdio_correlation, &right.stdio_correlation) {
2259 if lhs.metadata_key == rhs.metadata_key {
2260 return Err(ExtensionRegistryError::OwnershipCollision {
2261 field: "metadata key",
2262 value: lhs.metadata_key.clone(),
2263 });
2264 }
2265 for method in &lhs.methods {
2266 if rhs.methods.contains(method) && lhs.direction == rhs.direction {
2267 return Err(ExtensionRegistryError::OwnershipCollision {
2268 field: "stdio correlation method",
2269 value: method.clone(),
2270 });
2271 }
2272 }
2273 }
2274 Ok(())
2275}
2276
2277fn core_or_legacy_method(method: &str) -> bool {
2278 final_2026_07_28_method(method).is_some() || legacy_2024_11_05_method(method).is_some()
2279}
2280
2281fn canonical_descriptor_row(
2282 descriptor: &ExtensionDescriptor,
2283 additional_methods: Option<&BTreeMap<String, ExtensionMethodDescriptor>>,
2284) -> Value {
2285 if additional_methods.is_none_or(|methods| methods.is_empty()) {
2286 return serde_json::json!({
2287 "id": descriptor.id.as_str(),
2288 "clientSchema": descriptor.client_settings.schema_id,
2289 "clientCodec": descriptor.client_settings.codec_id,
2290 "serverSchema": descriptor.server_settings.schema_id,
2291 "serverCodec": descriptor.server_settings.codec_id,
2292 "resolver": [descriptor.resolver.id, descriptor.resolver.version, format!("{:?}", descriptor.resolver.fallback)],
2293 "method": descriptor.method.as_ref().map(|m| (&m.name, format!("{:?}", m.direction), m.http_era_disposition.map(|e| format!("{:?}", e)), m.legacy_fallback)),
2294 "notification": descriptor.notification.as_ref().map(|n| (&n.name, format!("{:?}", n.direction))),
2295 "resultDiscriminator": descriptor.result_discriminator,
2296 "routingHeaders": descriptor.routing_headers.iter().map(|h| &h.name).collect::<Vec<_>>(),
2297 "stdio": descriptor.stdio_correlation.as_ref().map(|s| (&s.metadata_key, &s.methods, format!("{:?}", s.direction))),
2298 });
2299 }
2300 let mut methods = descriptor
2301 .method
2302 .iter()
2303 .chain(
2304 additional_methods
2305 .into_iter()
2306 .flat_map(|methods| methods.values()),
2307 )
2308 .map(|method| {
2309 (
2310 method.name.clone(),
2311 format!("{:?}", method.direction),
2312 method
2313 .http_era_disposition
2314 .map(|disposition| format!("{disposition:?}")),
2315 method.legacy_fallback,
2316 )
2317 })
2318 .collect::<Vec<_>>();
2319 methods.sort_by(|left, right| left.0.cmp(&right.0));
2320 serde_json::json!({
2321 "id": descriptor.id.as_str(),
2322 "clientSchema": descriptor.client_settings.schema_id,
2323 "clientCodec": descriptor.client_settings.codec_id,
2324 "serverSchema": descriptor.server_settings.schema_id,
2325 "serverCodec": descriptor.server_settings.codec_id,
2326 "resolver": [descriptor.resolver.id, descriptor.resolver.version, format!("{:?}", descriptor.resolver.fallback)],
2327 "methods": methods,
2328 "notification": descriptor.notification.as_ref().map(|n| (&n.name, format!("{:?}", n.direction))),
2329 "resultDiscriminator": descriptor.result_discriminator,
2330 "routingHeaders": descriptor.routing_headers.iter().map(|h| &h.name).collect::<Vec<_>>(),
2331 "stdio": descriptor.stdio_correlation.as_ref().map(|s| (&s.metadata_key, &s.methods, format!("{:?}", s.direction))),
2332 })
2333}
2334
2335#[cfg(test)]
2336mod tests {
2337 use super::*;
2338 use serde_json::json;
2339
2340 fn descriptor(
2341 id: ExtensionId,
2342 method: &str,
2343 notification: &str,
2344 result_discriminator: &str,
2345 ) -> ExtensionDescriptor {
2346 ExtensionDescriptor {
2347 id,
2348 client_settings: ExtensionSettingsSchema {
2349 schema_id: "client-weather-v1".to_owned(),
2350 codec_id: "client-weather-codec-v1".to_owned(),
2351 },
2352 server_settings: ExtensionSettingsSchema {
2353 schema_id: "server-weather-v1".to_owned(),
2354 codec_id: "server-weather-codec-v1".to_owned(),
2355 },
2356 resolver: ExtensionNegotiationResolver {
2357 id: "weather-compatibility-v1".to_owned(),
2358 version: 1,
2359 fallback: ExtensionFallbackPolicy::RejectOneSided,
2360 },
2361 method: Some(ExtensionMethodDescriptor {
2362 name: method.to_owned(),
2363 direction: ExtensionDirection::ClientToServer,
2364 http_era_disposition: Some(ExtensionHttpEraDisposition::ModernExclusive),
2365 legacy_fallback: false,
2366 }),
2367 notification: Some(ExtensionNotificationDescriptor {
2368 name: notification.to_owned(),
2369 direction: ExtensionDirection::ServerToClient,
2370 }),
2371 result_discriminator: Some(result_discriminator.to_owned()),
2372 routing_headers: vec![ExtensionRoutingHeaderDescriptor {
2376 name: format!("Mcp-{}", method.rsplit('/').next().unwrap_or("weather")),
2377 }],
2378 stdio_correlation: None,
2379 }
2380 }
2381
2382 #[test]
2383 #[cfg(feature = "tasks")]
2384 fn ext_03_final_extension_identifier_wire_grammar_one_variable_negative() {
2385 let official = official_tasks_extension_id();
2386 assert_eq!(official.as_str(), OFFICIAL_TASKS_EXTENSION_ID);
2387 assert!(ExtensionId::parse("Example/tasks").is_ok());
2388
2389 assert_eq!(
2390 ExtensionId::parse(format!("{OFFICIAL_TASKS_EXTENSION_ID}_")),
2391 Err(ExtensionRegistryError::InvalidIdentifier(
2392 "io.modelcontextprotocol/tasks_".to_owned()
2393 )),
2394 "only the terminal non-alphanumeric name byte changes from the admitted official key"
2395 );
2396 }
2397
2398 #[test]
2399 fn apps_01_official_descriptor_negotiation_round_trip_positive() {
2400 let client_wire = json!({
2401 "mimeTypes": [
2402 MCP_APPS_HTML_MIME_TYPE,
2403 "application/vnd.example.dashboard+json",
2404 MCP_APPS_HTML_MIME_TYPE,
2405 ],
2406 });
2407 let client_settings = ExtensionSettings::new(client_wire.clone())
2408 .expect("the ordered, duplicated MCP Apps MIME advertisement is generic JSON");
2409 let decoded = McpAppsClientSettings::from_extension_settings(&client_settings)
2410 .expect("the required closed client settings object decodes");
2411 assert_eq!(
2412 decoded.to_extension_settings().into_value(),
2413 client_wire,
2414 "the typed MCP Apps codec preserves peer MIME ordering and duplicates"
2415 );
2416 assert!(decoded.supports_mcp_apps_html());
2417
2418 let mut registry = ExtensionDescriptorRegistry::new();
2419 let id = register_official_mcp_apps_extension(&mut registry)
2420 .expect("the official MCP Apps descriptor registers");
2421 let descriptor = registry
2422 .descriptor(&id)
2423 .expect("registered MCP Apps descriptor remains available before freeze");
2424 assert_eq!(descriptor.id.as_str(), OFFICIAL_MCP_APPS_EXTENSION_ID);
2425 assert_eq!(
2426 descriptor.client_settings.schema_id,
2427 MCP_APPS_CLIENT_SETTINGS_SCHEMA_ID
2428 );
2429 assert_eq!(
2430 descriptor.server_settings.schema_id,
2431 MCP_APPS_SERVER_SETTINGS_SCHEMA_ID
2432 );
2433 assert_eq!(descriptor.resolver.id, MCP_APPS_NEGOTIATION_RESOLVER_ID);
2434 assert_eq!(
2435 descriptor.resolver.version,
2436 MCP_APPS_NEGOTIATION_RESOLVER_VERSION
2437 );
2438 assert_eq!(
2439 descriptor.resolver.fallback,
2440 ExtensionFallbackPolicy::InactiveOnEitherPeer
2441 );
2442 assert!(descriptor.method.is_none());
2443 assert!(descriptor.notification.is_none());
2444 assert_eq!(
2445 validate_official_mcp_apps_descriptor(descriptor),
2446 Ok(()),
2447 "the public descriptor is the exact method-free Apps capability shape"
2448 );
2449 registry.freeze().expect("MCP Apps registry freezes");
2450
2451 let client = ClientExtensionDiscovery {
2452 extensions: BTreeMap::from([(id.clone(), client_settings)]),
2453 };
2454 let server = ServerExtensionDiscovery {
2455 extensions: BTreeMap::from([(id.clone(), official_mcp_apps_empty_server_settings())]),
2456 };
2457 let mut local = ExtensionLocalEnablement::default();
2458 local.enable(id.clone());
2459 let mut resolver = official_mcp_apps_negotiation_resolver();
2460 let negotiated = registry
2461 .negotiate(
2462 ProtocolEra::Modern2026,
2463 &local,
2464 &client,
2465 &server,
2466 &mut resolver,
2467 )
2468 .expect("the exact bilateral MCP Apps settings activate the descriptor");
2469
2470 assert_eq!(
2471 MCP_APPS_ACTIVATION_PREDICATE_ID,
2472 "fastmcp-2026-07-28-apps-bilateral-mime-v1"
2473 );
2474 assert_eq!(
2475 negotiated
2476 .active(&id)
2477 .expect("the enabled bilateral MCP Apps descriptor is active")
2478 .effective_settings()
2479 .settings()
2480 .clone()
2481 .into_value(),
2482 client_wire,
2483 "negotiation retains the same validated client settings object"
2484 );
2485 }
2486
2487 #[test]
2488 fn apps_01_descriptor_rejects_one_method_plant_without_registration_mutation() {
2489 let accepted = official_mcp_apps_descriptor();
2490 let mut planted = accepted.clone();
2491 planted.method = Some(ExtensionMethodDescriptor {
2492 name: MCP_APPS_INITIALIZE_METHOD.to_owned(),
2493 direction: ExtensionDirection::ClientToServer,
2494 http_era_disposition: Some(ExtensionHttpEraDisposition::ModernExclusive),
2495 legacy_fallback: false,
2496 });
2497
2498 assert_eq!(
2499 validate_official_mcp_apps_descriptor(&planted),
2500 Err(ExtensionRegistryError::OfficialMcpAppsDescriptorMismatch),
2501 "only adding a client/server method rejects the Host/View-only Apps descriptor"
2502 );
2503 let mut registry = ExtensionDescriptorRegistry::new();
2504 assert_eq!(
2505 registry.register(planted),
2506 Err(ExtensionRegistryError::OfficialMcpAppsDescriptorMismatch),
2507 "the registry must reject the same descriptor mutation before ownership changes"
2508 );
2509 assert_eq!(registry.descriptors().len(), 0);
2510 assert_eq!(
2511 validate_official_mcp_apps_descriptor(&accepted),
2512 Ok(()),
2513 "the rejected one-variable descriptor cannot mutate the admitted baseline"
2514 );
2515 }
2516
2517 #[test]
2518 fn apps_01_server_marker_requires_the_exact_empty_object() {
2519 let accepted = official_mcp_apps_empty_server_settings();
2520 assert_eq!(
2521 validate_official_mcp_apps_server_settings(&accepted),
2522 Ok(()),
2523 "the official Apps server marker is exactly the empty object"
2524 );
2525
2526 let rejected = ExtensionSettings::new(json!({ "unexpected": true }))
2527 .expect("the one-field alternate is still generic extension settings");
2528 assert_eq!(
2529 validate_official_mcp_apps_server_settings(&rejected),
2530 Err(ExtensionRegistryError::OfficialMcpAppsServerSettingsNotEmpty),
2531 "adding one server setting makes the official Apps marker invalid"
2532 );
2533 assert!(
2534 accepted.as_object().is_empty(),
2535 "rejecting the alternate cannot alter the admitted marker"
2536 );
2537 }
2538
2539 #[test]
2540 fn apps_01_typed_mime_settings_cannot_bypass_generic_value_bound() {
2541 let oversized = vec!["x".repeat(MAX_MCP_APPS_MIME_TYPE_BYTES); MAX_MCP_APPS_MIME_TYPES];
2542
2543 assert_eq!(
2544 McpAppsClientSettings::new(oversized),
2545 Err(ExtensionRegistryError::SettingsTooLarge),
2546 "the typed Apps constructor must enforce the generic per-value discovery bound"
2547 );
2548 }
2549
2550 #[test]
2551 #[cfg(feature = "tasks")]
2552 fn apps_01_typed_resolver_negotiates_apps_and_tasks_together() {
2553 let mut registry = ExtensionDescriptorRegistry::new();
2554 let tasks = register_official_tasks_extension(&mut registry)
2555 .expect("the official Tasks descriptor registers");
2556 let apps = register_official_mcp_apps_extension(&mut registry)
2557 .expect("the official MCP Apps descriptor registers");
2558 registry.freeze().expect("official descriptors freeze");
2559
2560 let client = ClientExtensionDiscovery {
2561 extensions: BTreeMap::from([
2562 (tasks.clone(), official_tasks_empty_settings()),
2563 (
2564 apps.clone(),
2565 ExtensionSettings::new(json!({"mimeTypes": [MCP_APPS_HTML_MIME_TYPE]}))
2566 .expect("bounded Apps client settings"),
2567 ),
2568 ]),
2569 };
2570 let server = ServerExtensionDiscovery {
2571 extensions: BTreeMap::from([
2572 (tasks.clone(), official_tasks_empty_settings()),
2573 (apps.clone(), official_mcp_apps_empty_server_settings()),
2574 ]),
2575 };
2576 let mut local = ExtensionLocalEnablement::default();
2577 local.enable(tasks.clone());
2578 local.enable(apps.clone());
2579 let mut resolver = official_mcp_apps_negotiation_resolver();
2580
2581 let negotiated = registry
2582 .negotiate(
2583 ProtocolEra::Modern2026,
2584 &local,
2585 &client,
2586 &server,
2587 &mut resolver,
2588 )
2589 .expect("the supplied resolver supports the official descriptor set");
2590
2591 assert!(negotiated.active(&tasks).is_some());
2592 assert!(negotiated.active(&apps).is_some());
2593 }
2594
2595 #[test]
2596 #[cfg(feature = "tasks")]
2597 fn apps_01_tasks_wrapper_preserves_apps_inactive_disposition() {
2598 let mut registry = ExtensionDescriptorRegistry::new();
2599 let tasks = register_official_tasks_extension(&mut registry)
2600 .expect("the official Tasks descriptor registers");
2601 let apps = register_official_mcp_apps_extension(&mut registry)
2602 .expect("the official MCP Apps descriptor registers");
2603 registry.freeze().expect("official descriptors freeze");
2604
2605 let client = ClientExtensionDiscovery {
2606 extensions: BTreeMap::from([
2607 (tasks.clone(), official_tasks_empty_settings()),
2608 (
2609 apps.clone(),
2610 McpAppsClientSettings::new(vec!["text/plain".to_owned()])
2611 .expect("another bounded MIME type is valid Apps settings")
2612 .to_extension_settings(),
2613 ),
2614 ]),
2615 };
2616 let server = ServerExtensionDiscovery {
2617 extensions: BTreeMap::from([
2618 (tasks.clone(), official_tasks_empty_settings()),
2619 (apps.clone(), official_mcp_apps_empty_server_settings()),
2620 ]),
2621 };
2622 let mut local = ExtensionLocalEnablement::default();
2623 local.enable(tasks.clone());
2624 local.enable(apps.clone());
2625 let mut resolver =
2626 TasksNegotiationResolver::with_fallback(official_mcp_apps_negotiation_resolver());
2627
2628 let negotiated = registry
2629 .negotiate(
2630 ProtocolEra::Modern2026,
2631 &local,
2632 &client,
2633 &server,
2634 &mut resolver,
2635 )
2636 .expect("inactive Apps does not reject a composed Tasks resolver");
2637
2638 assert!(negotiated.active(&tasks).is_some());
2639 assert_eq!(
2640 negotiated.inactive_reason(&apps),
2641 Some(ExtensionInactiveReason::SettingsInactiveFallback)
2642 );
2643 }
2644
2645 #[test]
2646 fn apps_01_other_valid_mime_type_selects_inactive_fallback() {
2647 let mut registry = ExtensionDescriptorRegistry::new();
2648 let id = register_official_mcp_apps_extension(&mut registry)
2649 .expect("the official MCP Apps descriptor registers");
2650 registry.freeze().expect("MCP Apps registry freezes");
2651 let client = ClientExtensionDiscovery {
2652 extensions: BTreeMap::from([(
2653 id.clone(),
2654 ExtensionSettings::new(json!({"mimeTypes": ["text/plain"]}))
2655 .expect("a closed client settings object with another MIME type is valid"),
2656 )]),
2657 };
2658 let server = ServerExtensionDiscovery {
2659 extensions: BTreeMap::from([(id.clone(), official_mcp_apps_empty_server_settings())]),
2660 };
2661 let mut local = ExtensionLocalEnablement::default();
2662 local.enable(id.clone());
2663 let mut resolver = official_mcp_apps_negotiation_resolver();
2664
2665 let negotiated = registry
2666 .negotiate(
2667 ProtocolEra::Modern2026,
2668 &local,
2669 &client,
2670 &server,
2671 &mut resolver,
2672 )
2673 .expect("valid client settings without the Apps HTML MIME choose fallback");
2674
2675 assert!(negotiated.active(&id).is_none());
2676 assert_eq!(
2677 negotiated.inactive_reason(&id),
2678 Some(ExtensionInactiveReason::SettingsInactiveFallback)
2679 );
2680 }
2681
2682 #[test]
2683 fn apps_01_official_descriptor_legacy_era_one_field_negative() {
2684 let client_wire = json!({"mimeTypes": [MCP_APPS_HTML_MIME_TYPE]});
2685 let client_settings =
2686 ExtensionSettings::new(client_wire.clone()).expect("valid MCP Apps client settings");
2687 let mut registry = ExtensionDescriptorRegistry::new();
2688 let id = register_official_mcp_apps_extension(&mut registry)
2689 .expect("the official MCP Apps descriptor registers");
2690 registry.freeze().expect("MCP Apps registry freezes");
2691 let client = ClientExtensionDiscovery {
2692 extensions: BTreeMap::from([(id.clone(), client_settings)]),
2693 };
2694 let server = ServerExtensionDiscovery {
2695 extensions: BTreeMap::from([(id.clone(), official_mcp_apps_empty_server_settings())]),
2696 };
2697 let mut local = ExtensionLocalEnablement::default();
2698 local.enable(id.clone());
2699 let resolver_calls = std::cell::Cell::new(0);
2700 let mut resolver = |descriptor: &ExtensionDescriptor,
2701 client: &ExtensionSettings,
2702 server: &ExtensionSettings| {
2703 resolver_calls.set(resolver_calls.get() + 1);
2704 match resolve_official_mcp_apps_settings(descriptor, client, server)? {
2705 ExtensionSettingsResolution::Active(settings) => Ok(settings),
2706 ExtensionSettingsResolution::Inactive => {
2707 Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
2708 descriptor.id.to_string(),
2709 ))
2710 }
2711 }
2712 };
2713
2714 registry
2715 .negotiate(
2716 ProtocolEra::Modern2026,
2717 &local,
2718 &client,
2719 &server,
2720 &mut resolver,
2721 )
2722 .expect("the modern baseline activates MCP Apps");
2723 assert_eq!(resolver_calls.get(), 1);
2724
2725 assert_eq!(
2726 registry.negotiate(
2727 ProtocolEra::Legacy2024,
2728 &local,
2729 &client,
2730 &server,
2731 &mut resolver,
2732 ),
2733 Err(ExtensionNegotiationError::LegacyProtocolExcluded),
2734 "changing only the protocol era rejects MCP Apps before resolver execution"
2735 );
2736 assert_eq!(resolver_calls.get(), 1);
2737 assert_eq!(
2738 client.extensions[&id].clone().into_value(),
2739 client_wire,
2740 "the rejected legacy-era negotiation cannot mutate the accepted modern wire"
2741 );
2742 }
2743
2744 #[test]
2745 #[cfg(feature = "tasks")]
2746 fn task_01_official_tasks_public_registry_positive() {
2747 let mut registry = ExtensionDescriptorRegistry::new();
2748 let id = register_official_tasks_extension(&mut registry)
2749 .expect("the public official Tasks surface registers atomically");
2750 let descriptor = registry
2751 .descriptor(&id)
2752 .expect("the public Tasks registration retains its descriptor");
2753 assert_eq!(descriptor.id.as_str(), OFFICIAL_TASKS_EXTENSION_ID);
2754 assert_eq!(
2755 descriptor.client_settings.schema_id,
2756 OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID
2757 );
2758 assert_eq!(
2759 descriptor.server_settings.schema_id,
2760 OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID
2761 );
2762 assert_eq!(
2763 descriptor
2764 .method
2765 .as_ref()
2766 .map(|method| method.name.as_str()),
2767 Some(OFFICIAL_TASKS_METHODS[0])
2768 );
2769 assert_eq!(
2770 descriptor.result_discriminator.as_deref(),
2771 Some(OFFICIAL_TASKS_RESULT_DISCRIMINATOR)
2772 );
2773 registry.freeze().expect("Tasks registry freezes");
2774
2775 let client = ClientExtensionDiscovery {
2776 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
2777 };
2778 let server = ServerExtensionDiscovery {
2779 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
2780 };
2781 let mut local = ExtensionLocalEnablement::default();
2782 local.enable(id.clone());
2783 let mut resolver =
2784 |_descriptor: &ExtensionDescriptor,
2785 _client: &ExtensionSettings,
2786 _server: &ExtensionSettings| { Ok(official_tasks_empty_settings()) };
2787
2788 let negotiated = registry
2789 .negotiate(
2790 ProtocolEra::Modern2026,
2791 &local,
2792 &client,
2793 &server,
2794 &mut resolver,
2795 )
2796 .expect("current client and server capabilities negotiate Tasks");
2797 for method in OFFICIAL_TASKS_METHODS {
2798 assert_eq!(
2799 negotiated
2800 .admit_method(
2801 ®istry,
2802 ProtocolEra::Modern2026,
2803 &id,
2804 method,
2805 ExtensionDirection::ClientToServer,
2806 )
2807 .expect("registered Tasks request is admitted")
2808 .id,
2809 id
2810 );
2811 }
2812 for method in ["tasks/list", "tasks/submit"] {
2813 assert_eq!(
2814 negotiated.admit_method(
2815 ®istry,
2816 ProtocolEra::Modern2026,
2817 &id,
2818 method,
2819 ExtensionDirection::ClientToServer,
2820 ),
2821 Err(ExtensionDispatchError::CapabilityDoesNotOwn {
2822 capability: id.to_string(),
2823 field: "method",
2824 value: method.to_owned(),
2825 }),
2826 "the official Tasks registration owns no additional request methods"
2827 );
2828 }
2829 assert_eq!(
2830 negotiated
2831 .admit_notification(
2832 ®istry,
2833 ProtocolEra::Modern2026,
2834 &id,
2835 OFFICIAL_TASKS_NOTIFICATION,
2836 ExtensionDirection::ServerToClient,
2837 )
2838 .expect("registered Tasks notification is admitted")
2839 .id,
2840 id
2841 );
2842 assert_eq!(
2843 negotiated
2844 .admit_result_discriminator(
2845 ®istry,
2846 ProtocolEra::Modern2026,
2847 &id,
2848 OFFICIAL_TASKS_RESULT_DISCRIMINATOR,
2849 )
2850 .expect("official Tasks tools/call result discriminator is admitted")
2851 .id,
2852 id
2853 );
2854 }
2855
2856 #[test]
2857 #[cfg(feature = "tasks")]
2858 fn task_01_official_tasks_undeclared_result_discriminator_one_variable_negative() {
2859 let mut registry = ExtensionDescriptorRegistry::new();
2860 let id = register_official_tasks_extension(&mut registry)
2861 .expect("the public official Tasks surface registers");
2862 registry.freeze().expect("Tasks registry freezes");
2863 let client = ClientExtensionDiscovery {
2864 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
2865 };
2866 let server = ServerExtensionDiscovery {
2867 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
2868 };
2869 let mut local = ExtensionLocalEnablement::default();
2870 local.enable(id.clone());
2871 let mut resolver =
2872 |_descriptor: &ExtensionDescriptor,
2873 _client: &ExtensionSettings,
2874 _server: &ExtensionSettings| { Ok(official_tasks_empty_settings()) };
2875 let negotiated = registry
2876 .negotiate(
2877 ProtocolEra::Modern2026,
2878 &local,
2879 &client,
2880 &server,
2881 &mut resolver,
2882 )
2883 .expect("current client and server capabilities negotiate Tasks");
2884
2885 let wrong_discriminator = "task-other";
2886 assert_eq!(
2887 negotiated.admit_result_discriminator(
2888 ®istry,
2889 ProtocolEra::Modern2026,
2890 &id,
2891 wrong_discriminator,
2892 ),
2893 Err(ExtensionDispatchError::CapabilityDoesNotOwn {
2894 capability: id.to_string(),
2895 field: "result discriminator",
2896 value: wrong_discriminator.to_owned(),
2897 }),
2898 "only the undeclared result discriminator differs from the admitted task value"
2899 );
2900 }
2901
2902 #[test]
2903 #[cfg(feature = "tasks")]
2904 fn task_01_official_tasks_nonempty_client_settings_one_variable_negative() {
2905 let mut registry = ExtensionDescriptorRegistry::new();
2906 let id = register_official_tasks_extension(&mut registry)
2907 .expect("the public official Tasks surface registers");
2908 let receipt = registry.freeze().expect("Tasks registry freezes");
2909 let client = ClientExtensionDiscovery {
2910 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
2911 };
2912 let server = ServerExtensionDiscovery {
2913 extensions: BTreeMap::from([(id.clone(), official_tasks_empty_settings())]),
2914 };
2915 let mut local = ExtensionLocalEnablement::default();
2916 local.enable(id.clone());
2917 let resolver_calls = std::cell::Cell::new(0);
2918 let mut resolver = |_descriptor: &ExtensionDescriptor,
2919 _client: &ExtensionSettings,
2920 _server: &ExtensionSettings| {
2921 resolver_calls.set(resolver_calls.get() + 1);
2922 Ok(official_tasks_empty_settings())
2923 };
2924
2925 registry
2926 .negotiate(
2927 ProtocolEra::Modern2026,
2928 &local,
2929 &client,
2930 &server,
2931 &mut resolver,
2932 )
2933 .expect("the empty-settings baseline negotiates Tasks");
2934 assert_eq!(resolver_calls.get(), 1);
2935
2936 let mut planted_client = client.clone();
2937 planted_client.extensions.insert(
2938 id.clone(),
2939 ExtensionSettings::new(json!({"unexpected": true}))
2940 .expect("the one-field mutation is generic extension JSON"),
2941 );
2942
2943 assert_eq!(
2944 registry.negotiate(
2945 ProtocolEra::Modern2026,
2946 &local,
2947 &planted_client,
2948 &server,
2949 &mut resolver,
2950 ),
2951 Err(ExtensionNegotiationError::SettingsCompatibilityRejected(
2952 id.to_string()
2953 )),
2954 "only adding one client settings field rejects the exact empty Tasks settings"
2955 );
2956 assert_eq!(
2957 resolver_calls.get(),
2958 1,
2959 "rejected admission cannot invoke the resolver"
2960 );
2961 assert_eq!(registry.receipt(), Some(&receipt));
2962 }
2963
2964 #[test]
2965 fn ext_03_final_core_method_collision_one_variable_negative() {
2966 let baseline = descriptor(
2967 ExtensionId::parse("com.example/discover").expect("valid extension ID"),
2968 "com.example/discover",
2969 "com.example/discover_changed",
2970 "com.example/discover_result",
2971 );
2972 assert!(validate_descriptor(&baseline).is_ok());
2973
2974 let mut planted = baseline.clone();
2975 planted
2976 .method
2977 .as_mut()
2978 .expect("baseline owns an extension method")
2979 .name = crate::methods::SERVER_DISCOVER.to_owned();
2980 assert_eq!(
2981 validate_descriptor(&planted),
2982 Err(ExtensionRegistryError::CoreMethodCollision(
2983 crate::methods::SERVER_DISCOVER.to_owned()
2984 )),
2985 "only replacing the extension method with final server/discover makes it invalid"
2986 );
2987 }
2988
2989 #[test]
2990 fn ext_01_unit_bilateral_negotiation_and_directional_dispatch_positive() {
2991 let id = ExtensionId::parse("com.example/weather").expect("valid extension ID");
2992 let mut registry = ExtensionDescriptorRegistry::new();
2993 registry
2994 .register(descriptor(
2995 id.clone(),
2996 "com.example/weather",
2997 "com.example/weather_changed",
2998 "com.example/weather_result",
2999 ))
3000 .expect("descriptor registers");
3001 registry
3002 .freeze()
3003 .expect("registry freezes before negotiation");
3004
3005 let client_settings = ExtensionSettings::new(json!({
3006 "unit": "celsius",
3007 "preserved": [null, 1.5, {"nested": true}],
3008 }))
3009 .expect("current-message client settings are bounded JSON");
3010 let server_settings = ExtensionSettings::new(json!({"maxCities": 4}))
3011 .expect("server discovery settings are bounded JSON");
3012 let unknown_id = ExtensionId::parse("org.example/diagnostic")
3013 .expect("unknown but structurally valid ID");
3014
3015 let client = ClientExtensionDiscovery {
3016 extensions: BTreeMap::from([
3017 (id.clone(), client_settings),
3018 (
3019 unknown_id.clone(),
3020 ExtensionSettings::new(json!({"opaque": null}))
3021 .expect("bounded unknown settings"),
3022 ),
3023 ]),
3024 };
3025 let server = ServerExtensionDiscovery {
3026 extensions: BTreeMap::from([(id.clone(), server_settings)]),
3027 };
3028 let mut local = ExtensionLocalEnablement::default();
3029 local.enable(id.clone());
3030 let mut resolver = |descriptor: &ExtensionDescriptor,
3031 client: &ExtensionSettings,
3032 server: &ExtensionSettings| {
3033 assert_eq!(descriptor.resolver.id, "weather-compatibility-v1");
3034 ExtensionSettings::new(json!({
3035 "unit": client.as_object()["unit"].clone(),
3036 "maxCities": server.as_object()["maxCities"].clone(),
3037 }))
3038 .map_err(|_| {
3039 ExtensionNegotiationError::SettingsCompatibilityRejected(descriptor.id.to_string())
3040 })
3041 };
3042 let negotiated = registry
3043 .negotiate(
3044 ProtocolEra::Modern2026,
3045 &local,
3046 &client,
3047 &server,
3048 &mut resolver,
3049 )
3050 .expect("bilateral current-message settings negotiate");
3051
3052 assert_eq!(negotiated.protocol_era(), ProtocolEra::Modern2026);
3053 assert_eq!(negotiated.active_extensions().len(), 1);
3054 assert_eq!(
3055 negotiated
3056 .active(&id)
3057 .expect("registered bilateral extension is active")
3058 .effective_settings()
3059 .settings()
3060 .as_object()["unit"],
3061 json!("celsius")
3062 );
3063 assert_eq!(
3064 negotiated.unknown_client_extensions()[&unknown_id].as_object()["opaque"],
3065 Value::Null,
3066 "unknown peer data remains diagnostic and cannot activate dispatch"
3067 );
3068 assert_eq!(
3069 negotiated
3070 .admit_capability(®istry, ProtocolEra::Modern2026, &id)
3071 .expect("developer-opted-in bilateral capability is active")
3072 .id,
3073 id
3074 );
3075 assert_eq!(
3076 negotiated
3077 .admit_method(
3078 ®istry,
3079 ProtocolEra::Modern2026,
3080 &id,
3081 "com.example/weather",
3082 ExtensionDirection::ClientToServer,
3083 )
3084 .expect("active method dispatch")
3085 .id,
3086 id
3087 );
3088 assert_eq!(
3089 negotiated
3090 .admit_notification(
3091 ®istry,
3092 ProtocolEra::Modern2026,
3093 &id,
3094 "com.example/weather_changed",
3095 ExtensionDirection::ServerToClient,
3096 )
3097 .expect("active notification dispatch")
3098 .id,
3099 id
3100 );
3101 assert_eq!(
3102 negotiated
3103 .admit_result_discriminator(
3104 ®istry,
3105 ProtocolEra::Modern2026,
3106 &id,
3107 "com.example/weather_result",
3108 )
3109 .expect("active result discriminator dispatch")
3110 .id,
3111 id
3112 );
3113 assert_eq!(
3114 negotiated.admit_notification(
3115 ®istry,
3116 ProtocolEra::Modern2026,
3117 &id,
3118 "com.example/weather_changed",
3119 ExtensionDirection::ClientToServer,
3120 ),
3121 Err(ExtensionDispatchError::DirectionMismatch {
3122 field: "notification",
3123 value: "com.example/weather_changed".to_owned(),
3124 expected: ExtensionDirection::ClientToServer,
3125 actual: ExtensionDirection::ServerToClient,
3126 }),
3127 "only the requested direction changes; the same active descriptor must not dispatch"
3128 );
3129 }
3130
3131 fn negotiated_weather_extension() -> (
3132 ExtensionDescriptorRegistry,
3133 ExtensionId,
3134 NegotiatedExtensionSet,
3135 ) {
3136 let id = ExtensionId::parse("com.example/weather").expect("valid extension ID");
3137 let mut registry = ExtensionDescriptorRegistry::new();
3138 registry
3139 .register(descriptor(
3140 id.clone(),
3141 "com.example/weather",
3142 "com.example/weather_changed",
3143 "com.example/weather_result",
3144 ))
3145 .expect("descriptor registers");
3146 registry
3147 .freeze()
3148 .expect("registry freezes before negotiation");
3149
3150 let client = ClientExtensionDiscovery {
3151 extensions: BTreeMap::from([(
3152 id.clone(),
3153 ExtensionSettings::new(json!({"unit": "celsius"}))
3154 .expect("bounded client settings"),
3155 )]),
3156 };
3157 let server = ServerExtensionDiscovery {
3158 extensions: BTreeMap::from([(
3159 id.clone(),
3160 ExtensionSettings::new(json!({"maxCities": 4})).expect("bounded server settings"),
3161 )]),
3162 };
3163 let mut local = ExtensionLocalEnablement::default();
3164 local.enable(id.clone());
3165 let mut resolver = |descriptor: &ExtensionDescriptor,
3166 client: &ExtensionSettings,
3167 server: &ExtensionSettings| {
3168 ExtensionSettings::new(json!({
3169 "unit": client.as_object()["unit"].clone(),
3170 "maxCities": server.as_object()["maxCities"].clone(),
3171 }))
3172 .map_err(|_| {
3173 ExtensionNegotiationError::SettingsCompatibilityRejected(descriptor.id.to_string())
3174 })
3175 };
3176 let negotiated = registry
3177 .negotiate(
3178 ProtocolEra::Modern2026,
3179 &local,
3180 &client,
3181 &server,
3182 &mut resolver,
3183 )
3184 .expect("developer opt-in and both peer settings negotiate");
3185
3186 (registry, id, negotiated)
3187 }
3188
3189 #[test]
3190 fn ext_02_executable_request_admission_positive() {
3191 let (registry, id, negotiated) = negotiated_weather_extension();
3192
3193 assert_eq!(negotiated.protocol_era(), ProtocolEra::Modern2026);
3194 assert_eq!(negotiated.active_extensions().len(), 1);
3195 assert_eq!(
3196 negotiated
3197 .admit_capability(®istry, ProtocolEra::Modern2026, &id)
3198 .expect("active extension capability is admitted per request")
3199 .id,
3200 id
3201 );
3202 assert_eq!(
3203 negotiated
3204 .admit_method(
3205 ®istry,
3206 ProtocolEra::Modern2026,
3207 &id,
3208 "com.example/weather",
3209 ExtensionDirection::ClientToServer,
3210 )
3211 .expect("active extension method is admitted per request")
3212 .id,
3213 id
3214 );
3215 assert_eq!(
3216 negotiated
3217 .admit_result_discriminator(
3218 ®istry,
3219 ProtocolEra::Modern2026,
3220 &id,
3221 "com.example/weather_result",
3222 )
3223 .expect("active extension result discriminator is admitted per request")
3224 .id,
3225 id
3226 );
3227 }
3228
3229 #[test]
3230 fn ext_02_executable_request_admission_one_variable_negatives() {
3231 let (registry, id, negotiated) = negotiated_weather_extension();
3232 let active_count = negotiated.active_extensions().len();
3233
3234 assert_eq!(
3235 negotiated.admit_capability(®istry, ProtocolEra::Legacy2024, &id),
3236 Err(ExtensionDispatchError::LegacyProtocolExcluded),
3237 "changing only the request era must exclude exact legacy admission"
3238 );
3239 assert_eq!(
3240 negotiated.admit_method(
3241 ®istry,
3242 ProtocolEra::Modern2026,
3243 &id,
3244 "com.example/weather-other",
3245 ExtensionDirection::ClientToServer,
3246 ),
3247 Err(ExtensionDispatchError::CapabilityDoesNotOwn {
3248 capability: id.to_string(),
3249 field: "method",
3250 value: "com.example/weather-other".to_owned(),
3251 }),
3252 "changing only the method spelling must reject dispatch"
3253 );
3254 assert_eq!(
3255 negotiated.admit_result_discriminator(
3256 ®istry,
3257 ProtocolEra::Modern2026,
3258 &id,
3259 "com.example/weather_result-other",
3260 ),
3261 Err(ExtensionDispatchError::CapabilityDoesNotOwn {
3262 capability: id.to_string(),
3263 field: "result discriminator",
3264 value: "com.example/weather_result-other".to_owned(),
3265 }),
3266 "changing only the result discriminator must reject dispatch"
3267 );
3268 assert_eq!(
3269 negotiated.active_extensions().len(),
3270 active_count,
3271 "rejected requests cannot mutate the bounded negotiated state"
3272 );
3273 }
3274
3275 #[test]
3276 fn ext_02_developer_opt_in_and_legacy_negotiation_fail_closed() {
3277 let id = ExtensionId::parse("com.example/weather").expect("valid extension ID");
3278 let mut registry = ExtensionDescriptorRegistry::new();
3279 registry
3280 .register(descriptor(
3281 id.clone(),
3282 "com.example/weather",
3283 "com.example/weather_changed",
3284 "com.example/weather_result",
3285 ))
3286 .expect("descriptor registers");
3287 let receipt = registry
3288 .freeze()
3289 .expect("registry freezes before negotiation");
3290 let client = ClientExtensionDiscovery {
3291 extensions: BTreeMap::from([(
3292 id.clone(),
3293 ExtensionSettings::new(json!({})).expect("bounded client settings"),
3294 )]),
3295 };
3296 let server = ServerExtensionDiscovery {
3297 extensions: BTreeMap::from([(
3298 id.clone(),
3299 ExtensionSettings::new(json!({})).expect("bounded server settings"),
3300 )]),
3301 };
3302 let local = ExtensionLocalEnablement::default();
3303 let resolver_calls = std::cell::Cell::new(0);
3304 let mut resolver = |_descriptor: &ExtensionDescriptor,
3305 _client: &ExtensionSettings,
3306 _server: &ExtensionSettings| {
3307 resolver_calls.set(resolver_calls.get() + 1);
3308 Ok(ExtensionSettings::new(json!({})).expect("bounded effective settings"))
3309 };
3310
3311 let unopted = registry
3312 .negotiate(
3313 ProtocolEra::Modern2026,
3314 &local,
3315 &client,
3316 &server,
3317 &mut resolver,
3318 )
3319 .expect("registered descriptors remain inactive without developer opt-in");
3320 assert_eq!(resolver_calls.get(), 0);
3321 assert_eq!(
3322 unopted.inactive_reason(&id),
3323 Some(ExtensionInactiveReason::LocallyDisabled)
3324 );
3325 assert_eq!(
3326 unopted.admit_capability(®istry, ProtocolEra::Modern2026, &id),
3327 Err(ExtensionDispatchError::InactiveCapability(id.to_string()))
3328 );
3329
3330 assert_eq!(
3331 registry.negotiate(
3332 ProtocolEra::Legacy2024,
3333 &local,
3334 &client,
3335 &server,
3336 &mut resolver,
3337 ),
3338 Err(ExtensionNegotiationError::LegacyProtocolExcluded),
3339 "changing only the negotiation era must reject exact legacy before resolver execution"
3340 );
3341 assert_eq!(resolver_calls.get(), 0);
3342 assert_eq!(registry.receipt(), Some(&receipt));
3343 }
3344
3345 #[test]
3346 fn ext_02_oversized_discovery_is_rejected_before_bounded_state_allocation() {
3347 let mut registry = ExtensionDescriptorRegistry::new();
3348 registry.freeze().expect("empty registry freezes");
3349 let settings = ExtensionSettings::new(json!({})).expect("bounded settings");
3350 let client = ClientExtensionDiscovery {
3351 extensions: (0..=MAX_EXTENSION_DESCRIPTORS)
3352 .map(|index| {
3353 (
3354 ExtensionId::parse(format!("com.example/diagnostic-{index}"))
3355 .expect("bounded synthetic identifier"),
3356 settings.clone(),
3357 )
3358 })
3359 .collect(),
3360 };
3361 let mut resolver_called = false;
3362 let mut resolver = |_descriptor: &ExtensionDescriptor,
3363 _client: &ExtensionSettings,
3364 _server: &ExtensionSettings| {
3365 resolver_called = true;
3366 Ok(ExtensionSettings::new(json!({})).expect("bounded effective settings"))
3367 };
3368
3369 assert_eq!(
3370 registry.negotiate(
3371 ProtocolEra::Modern2026,
3372 &ExtensionLocalEnablement::default(),
3373 &client,
3374 &ServerExtensionDiscovery::default(),
3375 &mut resolver,
3376 ),
3377 Err(ExtensionNegotiationError::DiscoveryTooManyExtensions(
3378 ExtensionPeer::Client
3379 ))
3380 );
3381 assert!(!resolver_called);
3382 }
3383
3384 #[test]
3385 fn ext_01_unit_one_variable_collision_negative() {
3386 let first_id = ExtensionId::parse("com.example/first").expect("first ID");
3387 let second_id = ExtensionId::parse("com.example/second").expect("second ID");
3388 let first = descriptor(
3389 first_id,
3390 "com.example/first",
3391 "com.example/first_changed",
3392 "com.example/first_result",
3393 );
3394 let candidate = descriptor(
3395 second_id,
3396 "com.example/second",
3397 "com.example/second_changed",
3398 "com.example/second_result",
3399 );
3400 let mut registry = ExtensionDescriptorRegistry::new();
3401 registry.register(first).expect("baseline owner registers");
3402
3403 let mut non_colliding_baseline = registry.clone();
3404 non_colliding_baseline
3405 .register(candidate.clone())
3406 .expect("the unmodified candidate is a genuinely non-colliding extension");
3407 let baseline_count = registry.descriptors().len();
3408
3409 let mut planted = candidate.clone();
3410 planted.result_discriminator = Some("com.example/first_result".to_owned());
3411 assert_eq!(
3412 registry.register(planted),
3413 Err(ExtensionRegistryError::OwnershipCollision {
3414 field: "result discriminator",
3415 value: "com.example/first_result".to_owned(),
3416 }),
3417 "the otherwise valid candidate differs in only the colliding discriminator"
3418 );
3419 assert_eq!(
3420 registry.descriptors().len(),
3421 baseline_count,
3422 "rejected registration cannot mutate the frozen dispatch owner set"
3423 );
3424 }
3425
3426 #[test]
3427 fn ext_01_unit_one_level_over_settings_bound_is_rejected() {
3428 let mut accepted_value = Value::Null;
3429 for _ in 0..MAX_EXTENSION_SETTINGS_NESTING {
3430 accepted_value = Value::Array(vec![accepted_value]);
3431 }
3432 let accepted = ExtensionSettings::new(json!({"nested": accepted_value.clone()}))
3433 .expect("the exact nesting bound is admitted");
3434
3435 let planted = Value::Array(vec![accepted_value.clone()]);
3436 assert_eq!(
3437 ExtensionSettings::new(json!({"nested": planted})),
3438 Err(ExtensionRegistryError::SettingsTooDeep),
3439 "only one additional nesting level changes the accepted settings object"
3440 );
3441 assert_eq!(
3442 accepted.as_object()["nested"],
3443 json!(accepted_value),
3444 "rejected settings cannot mutate the previously admitted object"
3445 );
3446 }
3447}