Skip to main content

fastmcp_protocol/
extensions.rs

1//! Frozen, runtime-neutral extension negotiation and request admission.
2//!
3//! This module owns no handler, transport, authorization, or client/server
4//! runtime state. It does bind a developer's explicit local opt-in and both
5//! peers' current settings to bounded, modern-only request admission.
6
7use 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
17/// Maximum descriptors retained by one registry.
18pub const MAX_EXTENSION_DESCRIPTORS: usize = 128;
19/// Maximum bytes in an extension identifier.
20pub const MAX_EXTENSION_ID_BYTES: usize = 512;
21/// Maximum bytes in one canonical descriptor-registry digest subject.
22pub const MAX_EXTENSION_REGISTRY_CANONICAL_BYTES: usize = 256 * 1024;
23/// Maximum extension settings entries preserved at the generic boundary.
24pub const MAX_EXTENSION_SETTINGS_ENTRIES: usize = 128;
25/// Maximum UTF-8 bytes in one extension settings key.
26pub const MAX_EXTENSION_SETTINGS_KEY_BYTES: usize = 512;
27/// Maximum canonical JSON bytes in one extension settings value.
28pub const MAX_EXTENSION_SETTINGS_VALUE_BYTES: usize = 16 * 1024;
29/// Maximum nesting depth admitted in a generic extension settings value.
30pub const MAX_EXTENSION_SETTINGS_NESTING: usize = 32;
31/// Maximum UTF-8 bytes in an extension-owned method or notification name.
32pub const MAX_EXTENSION_MEMBER_NAME_BYTES: usize = 512;
33/// Maximum extension-owned routing headers registered by one descriptor.
34pub const MAX_EXTENSION_ROUTING_HEADERS: usize = 32;
35/// Maximum UTF-8 bytes in an extension-owned routing header name.
36pub const MAX_EXTENSION_ROUTING_HEADER_BYTES: usize = 256;
37/// Maximum notification method names owned by one stdio correlation descriptor.
38pub const MAX_STDIO_CORRELATION_METHODS: usize = 32;
39
40/// Official Tasks extension identifier.
41#[cfg(feature = "tasks")]
42pub const OFFICIAL_TASKS_EXTENSION_ID: &str = "io.modelcontextprotocol/tasks";
43/// Official Tasks empty-settings schema identity for both peers.
44#[cfg(feature = "tasks")]
45pub const OFFICIAL_TASKS_EMPTY_SETTINGS_SCHEMA_ID: &str = "tasks-2026-07-28-empty-object-v1";
46/// Official Tasks empty-settings codec identity for both peers.
47#[cfg(feature = "tasks")]
48pub const OFFICIAL_TASKS_EMPTY_SETTINGS_CODEC_ID: &str = "tasks-2026-07-28-empty-object-v1";
49/// Official Tasks client-to-server request methods.
50#[cfg(feature = "tasks")]
51pub const OFFICIAL_TASKS_METHODS: [&str; 3] = ["tasks/get", "tasks/update", "tasks/cancel"];
52/// Official Tasks server-to-client notification method.
53#[cfg(feature = "tasks")]
54pub const OFFICIAL_TASKS_NOTIFICATION: &str = "notifications/tasks";
55/// Official Tasks `tools/call` result discriminator.
56#[cfg(feature = "tasks")]
57pub const OFFICIAL_TASKS_RESULT_DISCRIMINATOR: &str = "task";
58
59/// Official MCP Apps extension identifier.
60pub const OFFICIAL_MCP_APPS_EXTENSION_ID: &str = "io.modelcontextprotocol/ui";
61/// Pinned MCP Apps Host/View protocol version represented by this vocabulary.
62///
63/// This version belongs to the Apps postMessage protocol. It does not alter
64/// the MCP client/server protocol era or add an extension-owned RPC method.
65pub const MCP_APPS_PROTOCOL_VERSION: &str = "2026-01-26";
66/// MCP Apps HTML resource MIME type required for activation.
67pub const MCP_APPS_HTML_MIME_TYPE: &str = "text/html;profile=mcp-app";
68/// Stable MCP Apps client settings schema identity.
69pub const MCP_APPS_CLIENT_SETTINGS_SCHEMA_ID: &str = "apps-2026-01-26-client-mime-types-v1";
70/// Stable MCP Apps server empty-marker schema identity.
71pub const MCP_APPS_SERVER_SETTINGS_SCHEMA_ID: &str =
72    "fastmcp-2026-07-28-apps-empty-server-marker-v1";
73/// Stable MCP Apps bilateral compatibility resolver identity.
74pub const MCP_APPS_NEGOTIATION_RESOLVER_ID: &str = "fastmcp-apps-bilateral-resolver-v1";
75/// Stable resolver-version component of the frozen MCP Apps descriptor.
76pub const MCP_APPS_NEGOTIATION_RESOLVER_VERSION: u32 = 1;
77/// Stable MCP Apps activation predicate identity.
78pub const MCP_APPS_ACTIVATION_PREDICATE_ID: &str = "fastmcp-2026-07-28-apps-bilateral-mime-v1";
79/// Maximum MIME types retained in one MCP Apps client advertisement.
80pub const MAX_MCP_APPS_MIME_TYPES: usize = 128;
81/// Maximum UTF-8 bytes in one MCP Apps MIME type.
82pub const MAX_MCP_APPS_MIME_TYPE_BYTES: usize = 512;
83
84/// MCP Apps View-to-Host request method names.
85pub const MCP_APPS_OPEN_LINK_METHOD: &str = "ui/open-link";
86/// MCP Apps View-to-Host request method name.
87pub const MCP_APPS_DOWNLOAD_FILE_METHOD: &str = "ui/download-file";
88/// MCP Apps View-to-Host request method name.
89pub const MCP_APPS_MESSAGE_METHOD: &str = "ui/message";
90/// MCP Apps View-to-Host request method name.
91pub const MCP_APPS_UPDATE_MODEL_CONTEXT_METHOD: &str = "ui/update-model-context";
92/// MCP Apps Host-to-View request method name.
93pub const MCP_APPS_RESOURCE_TEARDOWN_METHOD: &str = "ui/resource-teardown";
94/// MCP Apps View-to-Host request method name.
95pub const MCP_APPS_INITIALIZE_METHOD: &str = "ui/initialize";
96/// MCP Apps View-to-Host request method name.
97pub const MCP_APPS_REQUEST_DISPLAY_MODE_METHOD: &str = "ui/request-display-mode";
98
99/// MCP Apps notification method names.
100pub const MCP_APPS_SANDBOX_PROXY_READY_NOTIFICATION: &str = "ui/notifications/sandbox-proxy-ready";
101/// MCP Apps notification method name.
102pub const MCP_APPS_SANDBOX_RESOURCE_READY_NOTIFICATION: &str =
103    "ui/notifications/sandbox-resource-ready";
104/// MCP Apps notification method name.
105pub const MCP_APPS_SIZE_CHANGED_NOTIFICATION: &str = "ui/notifications/size-changed";
106/// MCP Apps notification method name.
107pub const MCP_APPS_TOOL_INPUT_NOTIFICATION: &str = "ui/notifications/tool-input";
108/// MCP Apps notification method name.
109pub const MCP_APPS_TOOL_INPUT_PARTIAL_NOTIFICATION: &str = "ui/notifications/tool-input-partial";
110/// MCP Apps notification method name.
111pub const MCP_APPS_TOOL_RESULT_NOTIFICATION: &str = "ui/notifications/tool-result";
112/// MCP Apps notification method name.
113pub const MCP_APPS_TOOL_CANCELLED_NOTIFICATION: &str = "ui/notifications/tool-cancelled";
114/// MCP Apps notification method name.
115pub const MCP_APPS_HOST_CONTEXT_CHANGED_NOTIFICATION: &str =
116    "ui/notifications/host-context-changed";
117/// MCP Apps notification method name.
118pub const MCP_APPS_REQUEST_TEARDOWN_NOTIFICATION: &str = "ui/notifications/request-teardown";
119/// MCP Apps notification method name.
120pub const MCP_APPS_INITIALIZED_NOTIFICATION: &str = "ui/notifications/initialized";
121
122/// A validated extension identifier, preserving its exact wire spelling.
123#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
124pub struct ExtensionId(String);
125
126impl ExtensionId {
127    /// Validates the final metadata-key prefix/name grammar.
128    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        // The official second DNS labels are reserved: `com.mcp.*` or
140        // `org.modelcontextprotocol.*` would masquerade as official
141        // namespaces. Deeper labels (`com.example.mcp`) stay available, and
142        // the exact official prefix keeps resolving.
143        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    /// Returns the byte-preserved identifier.
154    #[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/// One extension's generic JSON-object settings.
193#[derive(Clone, Debug, PartialEq)]
194pub struct ExtensionSettings(Map<String, Value>);
195
196impl ExtensionSettings {
197    /// Admits exactly a JSON object, including an empty object.
198    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    /// Returns the preserved generic object.
207    #[must_use]
208    pub const fn as_object(&self) -> &Map<String, Value> {
209        &self.0
210    }
211
212    /// Returns the generic object as a JSON value without decoding it.
213    #[must_use]
214    pub fn into_value(self) -> Value {
215        Value::Object(self.0)
216    }
217
218    /// Decodes this descriptor-scoped object through a caller-selected typed codec.
219    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/// Returns the sole settings object admitted by the official Tasks extension.
229#[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/// Strict, ordered MCP Apps client capability settings.
242///
243/// The wire object is deliberately closed and requires `mimeTypes`. Its array
244/// preserves peer order and schema-valid duplicates exactly; support is granted
245/// solely by the presence of the exact HTML profile MIME type.
246#[derive(Clone, Debug, Eq, PartialEq)]
247pub struct McpAppsClientSettings {
248    mime_types: Vec<String>,
249}
250
251impl McpAppsClientSettings {
252    /// Validates ordered client-advertised MIME types without normalizing them.
253    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        // The typed limit is not permitted to bypass the generic discovery
263        // bound. `to_extension_settings` relies on this validation when it
264        // constructs the private generic settings value directly.
265        validate_settings_map(&mcp_apps_client_settings_map(&mime_types))?;
266        Ok(Self { mime_types })
267    }
268
269    /// Decodes the closed MCP Apps client settings object.
270    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    /// Returns advertised MIME types in their exact peer-supplied order.
281    #[must_use]
282    pub fn mime_types(&self) -> &[String] {
283        &self.mime_types
284    }
285
286    /// Returns whether this host advertises the required MCP Apps HTML profile.
287    #[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    /// Re-encodes the validated settings without changing order or duplicates.
295    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/// Returns the exact empty MCP Apps server settings marker.
310#[must_use]
311pub fn official_mcp_apps_empty_server_settings() -> ExtensionSettings {
312    ExtensionSettings(Map::new())
313}
314
315/// Validates the sole server settings marker admitted by official MCP Apps.
316///
317/// The official Apps descriptor is a server capability marker, not a
318/// server-configurable settings object. Reject a non-empty marker while the
319/// server is being configured so it cannot be advertised and fail later
320/// during per-request negotiation.
321pub 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/// Returns the validated identifier for the official MCP Apps extension.
332#[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/// Returns the descriptor for MCP Apps capability negotiation.
339///
340/// Apps bridge methods belong to the host/View postMessage channel, not the
341/// client/server extension-dispatch surface. This descriptor therefore owns
342/// only the bilateral capability settings contract.
343#[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
368/// Validates the complete, method-free MCP Apps capability descriptor.
369///
370/// The official Apps extension advertises bilateral MIME support only. View
371/// lifecycle and result messages use a separate Host/View channel, so an Apps
372/// descriptor must not acquire a JSON-RPC method, notification, result
373/// discriminator, routing header, or stdio-correlation owner by mutation.
374pub 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
384/// Registers the MCP Apps capability descriptor.
385///
386/// Registration never activates Apps. The local gates, exact empty server
387/// marker, and a current client MIME advertisement must all be present before
388/// [`resolve_official_mcp_apps_settings`] can activate it.
389pub 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/// Typed MCP Apps compatibility resolver that delegates every non-Apps
400/// descriptor to its supplied fallback.
401///
402/// One frozen registry may contain Apps alongside Tasks or private extensions.
403/// This wrapper consumes only the official Apps descriptor, so callers can
404/// compose it with their existing resolver rather than replacing it.
405#[derive(Clone, Debug)]
406pub struct McpAppsNegotiationResolver<R = RejectingExtensionNegotiationResolver> {
407    fallback: R,
408}
409
410/// Fallback used when no optional extension resolver is selected.
411///
412/// This keeps MCP Apps usable without compiling the Tasks extension while
413/// refusing any non-Apps descriptor rather than implicitly enabling a wire
414/// capability that the crate feature excluded.
415#[derive(Clone, Copy, Debug, Default)]
416pub struct RejectingExtensionNegotiationResolver;
417
418impl<R> McpAppsNegotiationResolver<R> {
419    /// Wraps an existing resolver with the official MCP Apps settings rules.
420    #[must_use]
421    pub const fn with_fallback(fallback: R) -> Self {
422        Self { fallback }
423    }
424}
425
426/// Resolves the official Tasks descriptor when Apps and Tasks share a registry.
427#[derive(Clone, Copy, Debug, Default)]
428#[cfg(feature = "tasks")]
429pub struct OfficialTasksNegotiationResolver;
430
431/// Typed Tasks compatibility resolver that delegates every non-Tasks
432/// descriptor to its supplied fallback.
433///
434/// This adapter preserves an inactive disposition selected by the fallback,
435/// which lets Tasks compose with bilateral capabilities such as MCP Apps.
436#[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    /// Wraps an existing resolver with the official Tasks settings rules.
445    #[must_use]
446    pub const fn with_fallback(fallback: R) -> Self {
447        Self { fallback }
448    }
449}
450
451/// Returns the standalone typed resolver used by the official MCP Apps descriptor.
452///
453/// Use [`McpAppsNegotiationResolver::with_fallback`] when the registry also
454/// contains private descriptors. When the `tasks` feature is selected, the
455/// default helper also resolves the official Tasks descriptor.
456#[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/// Returns the standalone typed resolver used by the official MCP Apps descriptor.
464#[must_use]
465#[cfg(not(feature = "tasks"))]
466pub const fn official_mcp_apps_negotiation_resolver() -> McpAppsNegotiationResolver {
467    McpAppsNegotiationResolver::with_fallback(RejectingExtensionNegotiationResolver)
468}
469
470/// Resolves a bilateral MCP Apps capability without comparing asymmetric peer objects.
471///
472/// On success, the effective settings retain the client's exact validated
473/// `mimeTypes` array. Missing peer advertisements take the descriptor's
474/// ordinary inactive fallback; malformed present settings reject negotiation.
475pub 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/// Public discovery input for one enabled local extension.
568#[derive(Clone, Debug, PartialEq)]
569pub struct ExtensionDiscovery {
570    /// Registered extension identifier.
571    pub id: ExtensionId,
572    /// Exact generic settings advertised by this peer.
573    pub settings: ExtensionSettings,
574}
575
576/// Public client discovery input; no client runtime is stored here.
577#[derive(Clone, Debug, Default, PartialEq)]
578pub struct ClientExtensionDiscovery {
579    /// Enabled client extensions keyed by their IDs.
580    pub extensions: BTreeMap<ExtensionId, ExtensionSettings>,
581}
582
583/// Public server discovery input; no server runtime is stored here.
584#[derive(Clone, Debug, Default, PartialEq)]
585pub struct ServerExtensionDiscovery {
586    /// Enabled server extensions keyed by their IDs.
587    pub extensions: BTreeMap<ExtensionId, ExtensionSettings>,
588}
589
590/// Direction of a registered RPC name.
591#[derive(Clone, Copy, Debug, Eq, PartialEq)]
592pub enum ExtensionDirection {
593    /// A client sends a request or notification to a server.
594    ClientToServer,
595    /// A server sends a request or notification to a client.
596    ServerToClient,
597}
598
599/// Frozen HTTP-era classification for client-to-server extension methods.
600#[derive(Clone, Copy, Debug, Eq, PartialEq)]
601pub enum ExtensionHttpEraDisposition {
602    /// The method is audited absent from the exact legacy adapter.
603    ModernExclusive,
604    /// Method text alone cannot select an era.
605    EraAmbiguous,
606}
607
608/// Declares the fallback expected by a descriptor's resolver.
609#[derive(Clone, Copy, Debug, Eq, PartialEq)]
610pub enum ExtensionFallbackPolicy {
611    /// Both peers must advertise compatible settings.
612    RejectOneSided,
613    /// A missing client advertisement selects the descriptor's inactive fallback.
614    ServerInactiveFallback,
615    /// A missing server advertisement selects the descriptor's inactive fallback.
616    ClientInactiveFallback,
617    /// Either missing peer advertisement selects the descriptor's inactive fallback.
618    InactiveOnEitherPeer,
619}
620
621/// Stable, total settings compatibility resolver metadata.
622#[derive(Clone, Debug, Eq, PartialEq)]
623pub struct ExtensionNegotiationResolver {
624    /// Stable resolver identifier included in the registry digest.
625    pub id: String,
626    /// Stable resolver version included in the registry digest.
627    pub version: u32,
628    /// One-sided behavior selected by this resolver.
629    pub fallback: ExtensionFallbackPolicy,
630}
631
632/// Stable schema and typed-codec identity, not an executable runtime codec.
633#[derive(Clone, Debug, Eq, PartialEq)]
634pub struct ExtensionSettingsSchema {
635    /// Stable schema identity.
636    pub schema_id: String,
637    /// Stable typed codec identity.
638    pub codec_id: String,
639}
640
641/// A registered method and its frozen era/fallback declarations.
642#[derive(Clone, Debug, Eq, PartialEq)]
643pub struct ExtensionMethodDescriptor {
644    /// Exact JSON-RPC method name.
645    pub name: String,
646    /// Message direction.
647    pub direction: ExtensionDirection,
648    /// Required only for client-to-server HTTP methods.
649    pub http_era_disposition: Option<ExtensionHttpEraDisposition>,
650    /// Whether exact legacy fallback is declared; exact legacy excludes extensions, so this must
651    /// remain false.
652    pub legacy_fallback: bool,
653}
654
655/// A registered notification name and direction.
656#[derive(Clone, Debug, Eq, PartialEq)]
657pub struct ExtensionNotificationDescriptor {
658    /// Exact notification name.
659    pub name: String,
660    /// Message direction.
661    pub direction: ExtensionDirection,
662}
663
664/// A routing-header name owned by an extension descriptor.
665#[derive(Clone, Debug, Eq, PartialEq)]
666pub struct ExtensionRoutingHeaderDescriptor {
667    /// Exact header name.
668    pub name: String,
669}
670
671/// A stdio notification-correlation metadata key owned by an extension.
672#[derive(Clone, Debug, Eq, PartialEq)]
673pub struct StdioCorrelationDescriptor {
674    /// Exact extension-owned metadata key.
675    pub metadata_key: String,
676    /// Notifications permitted to use the key.
677    pub methods: Vec<String>,
678    /// Direction required for every named notification.
679    pub direction: ExtensionDirection,
680}
681
682/// One immutable extension descriptor.
683#[derive(Clone, Debug, Eq, PartialEq)]
684pub struct ExtensionDescriptor {
685    /// Descriptor owner.
686    pub id: ExtensionId,
687    /// Stable client settings schema and codec identity.
688    pub client_settings: ExtensionSettingsSchema,
689    /// Stable server settings schema and codec identity.
690    pub server_settings: ExtensionSettingsSchema,
691    /// Stable compatibility resolver identity and fallback.
692    pub resolver: ExtensionNegotiationResolver,
693    /// Registered request method, when this extension defines one.
694    pub method: Option<ExtensionMethodDescriptor>,
695    /// Registered notification, when this extension defines one.
696    pub notification: Option<ExtensionNotificationDescriptor>,
697    /// Registered result discriminator, when this extension defines one.
698    pub result_discriminator: Option<String>,
699    /// Routing headers this descriptor owns.
700    pub routing_headers: Vec<ExtensionRoutingHeaderDescriptor>,
701    /// Optional stdio correlation metadata descriptor.
702    pub stdio_correlation: Option<StdioCorrelationDescriptor>,
703}
704
705/// Returns the validated identifier for the official Tasks extension.
706#[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/// Returns the complete official Tasks descriptor.
714///
715/// It owns exactly `tasks/get`, `tasks/update`, `tasks/cancel`,
716/// `notifications/tasks`, and the `tools/call` result discriminator `task`.
717/// Tasks settings are exactly the empty JSON object;
718/// [`ExtensionDescriptorRegistry::negotiate`] enforces that invariant for the
719/// two peer advertisements and the effective settings chosen by its resolver.
720#[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/// Registers the complete official Tasks surface atomically.
750///
751/// The resulting descriptor owns exactly the official Tasks request methods,
752/// its one server notification, and its `tools/call` result discriminator.
753/// Registration alone does not activate the extension; normal local enablement
754/// and bilateral negotiation still apply.
755#[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/// Immutable receipt returned by [`ExtensionDescriptorRegistry::freeze`].
780#[derive(Clone, Debug, Eq, PartialEq)]
781pub struct ExtensionRegistryReceipt {
782    digest: [u8; 32],
783    descriptor_count: usize,
784}
785
786impl ExtensionRegistryReceipt {
787    /// Returns the domain-separated descriptor-registry digest bytes.
788    #[must_use]
789    pub const fn digest(&self) -> &[u8; 32] {
790        &self.digest
791    }
792
793    /// Returns the number of frozen descriptors.
794    #[must_use]
795    pub const fn descriptor_count(&self) -> usize {
796        self.descriptor_count
797    }
798}
799
800/// Stable registry-validation diagnostics.
801#[derive(Clone, Debug, Eq, PartialEq)]
802pub enum ExtensionRegistryError {
803    /// The identifier did not satisfy the mandatory prefix/name grammar.
804    InvalidIdentifier(String),
805    /// The identifier used a reserved official second DNS label.
806    ReservedNamespace(String),
807    /// Generic settings were not a JSON object.
808    SettingsNotObject,
809    /// Typed settings decoding rejected the otherwise preserved object.
810    SettingsCodecRejected,
811    /// Official MCP Apps server settings must be the exact empty marker.
812    OfficialMcpAppsServerSettingsNotEmpty,
813    /// The official MCP Apps descriptor differed from its frozen method-free shape.
814    OfficialMcpAppsDescriptorMismatch,
815    /// Generic settings exceeded the fixed number of retained members.
816    SettingsTooManyEntries,
817    /// A generic settings key exceeded its fixed byte limit.
818    SettingsKeyTooLong,
819    /// A generic settings value exceeded its fixed encoded-byte limit.
820    SettingsTooLarge,
821    /// A generic settings value exceeded its fixed nesting limit.
822    SettingsTooDeep,
823    /// A descriptor omitted a required stable owner value.
824    MissingOwner(&'static str),
825    /// A descriptor ID was registered twice.
826    DuplicateExtensionId(String),
827    /// A method was added to an extension that is not registered.
828    UnregisteredExtensionId(String),
829    /// A named field already belongs to a different descriptor.
830    OwnershipCollision { field: &'static str, value: String },
831    /// A client-to-server method omitted its frozen era disposition.
832    MissingHttpEraDisposition(String),
833    /// Legacy fallback contradicts the frozen HTTP-era disposition.
834    LegacyFallbackContradiction(String),
835    /// A descriptor attempted to claim a legacy/shared core method.
836    CoreMethodCollision(String),
837    /// A descriptor attempted to claim a legacy/shared core notification.
838    CoreNotificationCollision(String),
839    /// A descriptor attempted to claim a final-core result discriminator.
840    CoreResultDiscriminatorCollision(String),
841    /// A descriptor member exceeded its bounded wire-name limit.
842    MemberNameTooLong { field: &'static str, value: String },
843    /// A descriptor claimed the same local wire name in incompatible roles.
844    LocalOwnershipCollision { field: &'static str, value: String },
845    /// Registration occurred after the immutable registry was frozen.
846    Frozen,
847    /// The canonical digest subject exceeded its bounded limit.
848    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/// Local feature and runtime opt-in state for the registered descriptors.
935///
936/// The default is deliberately empty: registering an extension does not enable
937/// it for any request.
938#[derive(Clone, Debug, Default, Eq, PartialEq)]
939pub struct ExtensionLocalEnablement {
940    compiled: BTreeSet<ExtensionId>,
941    runtime: BTreeSet<ExtensionId>,
942}
943
944impl ExtensionLocalEnablement {
945    /// Enables one descriptor at both the compile-feature and runtime layers.
946    pub fn enable(&mut self, id: ExtensionId) {
947        self.compiled.insert(id.clone());
948        self.runtime.insert(id);
949    }
950
951    /// Records whether the local build includes a descriptor's compile feature.
952    pub fn set_compiled(&mut self, id: ExtensionId, enabled: bool) {
953        set_enabled(&mut self.compiled, id, enabled);
954    }
955
956    /// Records whether the current local runtime enables a descriptor.
957    pub fn set_runtime(&mut self, id: ExtensionId, enabled: bool) {
958        set_enabled(&mut self.runtime, id, enabled);
959    }
960
961    /// Returns whether both local enablement gates are satisfied.
962    #[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/// The peer side that omitted an otherwise locally enabled extension.
981#[derive(Clone, Copy, Debug, Eq, PartialEq)]
982pub enum ExtensionPeer {
983    /// The current message omitted client extension settings.
984    Client,
985    /// Local server discovery omitted server extension settings.
986    Server,
987}
988
989/// A non-activating extension outcome retained for diagnostics.
990#[derive(Clone, Copy, Debug, Eq, PartialEq)]
991pub enum ExtensionInactiveReason {
992    /// The compile-time feature or runtime opt-in is absent.
993    LocallyDisabled,
994    /// Neither peer advertised the descriptor on this current exchange.
995    NotAdvertised,
996    /// The registered fallback was selected after the client omitted support.
997    ServerInactiveFallback,
998    /// The registered fallback was selected after the server omitted support.
999    ClientInactiveFallback,
1000    /// Both peers advertised valid settings, but the typed resolver selected an inactive fallback.
1001    SettingsInactiveFallback,
1002}
1003
1004/// Normalized typed settings produced by a descriptor's compatibility resolver.
1005///
1006/// Generic registry code retains client and server settings independently and
1007/// never compares their JSON objects for equality. A typed resolver receives
1008/// both objects and returns this one normalized, bounded value.
1009#[derive(Clone, Debug, PartialEq)]
1010pub struct EffectiveExtensionSettings {
1011    settings: ExtensionSettings,
1012    fingerprint: [u8; 32],
1013}
1014
1015impl EffectiveExtensionSettings {
1016    /// Returns the typed-resolver's normalized settings object.
1017    #[must_use]
1018    pub const fn settings(&self) -> &ExtensionSettings {
1019        &self.settings
1020    }
1021
1022    /// Returns the stable fingerprint bound to the descriptor and effective settings.
1023    #[must_use]
1024    pub const fn fingerprint(&self) -> &[u8; 32] {
1025        &self.fingerprint
1026    }
1027}
1028
1029/// An enabled extension after the current-message bilateral negotiation.
1030#[derive(Clone, Debug, PartialEq)]
1031pub struct NegotiatedExtension {
1032    id: ExtensionId,
1033    effective_settings: EffectiveExtensionSettings,
1034}
1035
1036impl NegotiatedExtension {
1037    /// Returns the registered descriptor identifier.
1038    #[must_use]
1039    pub const fn id(&self) -> &ExtensionId {
1040        &self.id
1041    }
1042
1043    /// Returns the compatibility resolver's normalized effective settings.
1044    #[must_use]
1045    pub const fn effective_settings(&self) -> &EffectiveExtensionSettings {
1046        &self.effective_settings
1047    }
1048}
1049
1050/// Frozen per-request extension state derived from both peers' current settings.
1051#[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/// Opaque current-exchange receipt authorizing one MCP Apps Host/View bridge.
1062///
1063/// This can only be derived from a frozen registry and a negotiated extension
1064/// set in which the exact official Apps descriptor is active. It deliberately
1065/// exposes no literal-based constructor: callers must retain the current
1066/// negotiation result rather than recreating Apps activation from schema IDs.
1067#[derive(Clone, Debug, Eq, PartialEq)]
1068pub struct McpAppsActivationReceipt {
1069    registry_digest: [u8; 32],
1070    effective_settings_fingerprint: [u8; 32],
1071}
1072
1073impl NegotiatedExtensionSet {
1074    /// Returns the registry receipt that this set is bound to.
1075    #[must_use]
1076    pub const fn registry_receipt(&self) -> &ExtensionRegistryReceipt {
1077        &self.registry_receipt
1078    }
1079
1080    /// Returns the exact modern protocol era that produced this request state.
1081    #[must_use]
1082    pub const fn protocol_era(&self) -> ProtocolEra {
1083        self.protocol_era
1084    }
1085
1086    /// Returns a negotiated extension when it is active for this exchange.
1087    #[must_use]
1088    pub fn active(&self, id: &ExtensionId) -> Option<&NegotiatedExtension> {
1089        self.active.get(id)
1090    }
1091
1092    /// Returns the recorded non-activating outcome for a registered descriptor.
1093    #[must_use]
1094    pub fn inactive_reason(&self, id: &ExtensionId) -> Option<ExtensionInactiveReason> {
1095        self.inactive.get(id).copied()
1096    }
1097
1098    /// Returns enabled descriptors in deterministic identifier order.
1099    #[must_use]
1100    pub fn active_extensions(&self) -> impl ExactSizeIterator<Item = &NegotiatedExtension> {
1101        self.active.values()
1102    }
1103
1104    /// Derives the sole receipt accepted by the Apps Host/View bridge for this
1105    /// current negotiated exchange.
1106    ///
1107    /// The receipt is unavailable for a legacy exchange, a different frozen
1108    /// registry, inactive Apps settings, or any descriptor that differs from
1109    /// the frozen official method-free Apps descriptor.
1110    #[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    /// Returns unknown current-message client settings preserved only for diagnostics.
1137    #[must_use]
1138    pub const fn unknown_client_extensions(&self) -> &BTreeMap<ExtensionId, ExtensionSettings> {
1139        &self.unknown_client
1140    }
1141
1142    /// Returns unknown server discovery settings preserved only for diagnostics.
1143    #[must_use]
1144    pub const fn unknown_server_extensions(&self) -> &BTreeMap<ExtensionId, ExtensionSettings> {
1145        &self.unknown_server
1146    }
1147}
1148
1149/// Error returned while deriving a per-request bilateral extension set.
1150#[derive(Clone, Debug, Eq, PartialEq)]
1151pub enum ExtensionNegotiationError {
1152    /// Extensions are excluded from exact MCP 2024-11-05 negotiation.
1153    LegacyProtocolExcluded,
1154    /// Descriptor registration must be frozen before negotiation.
1155    RegistryNotFrozen,
1156    /// A local feature/runtime configuration referenced an unregistered descriptor.
1157    UnregisteredLocalEnablement(String),
1158    /// A discovery map exceeded the generic retained-entry limit.
1159    DiscoveryTooManyExtensions(ExtensionPeer),
1160    /// Only one peer advertised a locally enabled descriptor without its matching fallback.
1161    OneSidedSupport { id: String, missing: ExtensionPeer },
1162    /// The selected typed resolver rejected both independently decoded settings objects.
1163    SettingsCompatibilityRejected(String),
1164    /// The normalized settings object could not be fingerprinted within its bound.
1165    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/// The activation outcome selected by a typed settings compatibility resolver.
1208#[derive(Clone, Debug, PartialEq)]
1209pub enum ExtensionSettingsResolution {
1210    /// Compatible settings activate the extension for this exchange.
1211    Active(ExtensionSettings),
1212    /// Valid settings select the descriptor's ordinary inactive fallback.
1213    Inactive,
1214}
1215
1216/// A typed compatibility resolver selected by a frozen descriptor ID/version.
1217///
1218/// This protocol-only seam deliberately accepts no server or client runtime
1219/// object. Implementations decode the two settings objects independently,
1220/// check compatibility, and return a normalized effective object.
1221pub trait ExtensionSettingsCompatibilityResolver {
1222    /// Resolves both current settings objects into normalized effective settings.
1223    fn resolve(
1224        &mut self,
1225        descriptor: &ExtensionDescriptor,
1226        client: &ExtensionSettings,
1227        server: &ExtensionSettings,
1228    ) -> Result<ExtensionSettings, ExtensionNegotiationError>;
1229
1230    /// Resolves settings and may select an ordinary inactive fallback.
1231    ///
1232    /// Existing resolvers that only implement [`Self::resolve`] remain active
1233    /// on success. Descriptors with a valid non-activating setting profile can
1234    /// override this method without treating that profile as malformed.
1235    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/// Direction-sensitive extension dispatch failure.
1364#[derive(Clone, Debug, Eq, PartialEq)]
1365pub enum ExtensionDispatchError {
1366    /// Extensions are excluded from an exact MCP 2024-11-05 request.
1367    LegacyProtocolExcluded,
1368    /// The request era does not match the immutable era that negotiated this set.
1369    ProtocolEraMismatch {
1370        /// The exact era that created this set.
1371        negotiated: ProtocolEra,
1372        /// The exact era attached to the request being admitted.
1373        request: ProtocolEra,
1374    },
1375    /// Dispatch used a registry other than the frozen registry that negotiated the set.
1376    RegistryReceiptMismatch,
1377    /// The named extension was not activated by developer opt-in and bilateral negotiation.
1378    InactiveCapability(String),
1379    /// The active capability does not own the named request member.
1380    CapabilityDoesNotOwn {
1381        /// Active extension capability identifier.
1382        capability: String,
1383        /// Registered member category.
1384        field: &'static str,
1385        /// Request-supplied member spelling.
1386        value: String,
1387    },
1388    /// The caller supplied an unbounded member name.
1389    NameTooLong(String),
1390    /// No active extension owns the requested member in this direction.
1391    NoActiveOwner { field: &'static str, value: String },
1392    /// An owner exists but is declared in the opposite direction.
1393    DirectionMismatch {
1394        field: &'static str,
1395        value: String,
1396        expected: ExtensionDirection,
1397        actual: ExtensionDirection,
1398    },
1399    /// A registry invariant was violated by more than one active owner.
1400    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/// Acyclic protocol-only descriptor registry.
1461#[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    /// Builds an empty registry; extensions are disabled until a descriptor is registered.
1470    #[must_use]
1471    pub fn new() -> Self {
1472        Self::default()
1473    }
1474
1475    /// Adds one descriptor after validating all descriptor-local and cross-owner rules.
1476    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(&notification.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    /// Adds another request method to an already registered extension descriptor.
1531    ///
1532    /// One extension can own more than one request method. The extension's
1533    /// settings, resolver, notification, and result discriminator stay on the
1534    /// original descriptor so every additional method shares the same negotiated
1535    /// capability and frozen receipt.
1536    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    /// Freezes this registry and returns its canonical domain-separated digest receipt.
1611    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    /// Returns the frozen receipt, if this registry has been frozen.
1628    #[must_use]
1629    pub fn receipt(&self) -> Option<&ExtensionRegistryReceipt> {
1630        self.receipt.as_ref()
1631    }
1632
1633    /// Returns a descriptor by its exact registered identifier.
1634    #[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    /// Returns the descriptor for one extension-owned request method.
1652    ///
1653    /// This includes the primary descriptor method and every additional method
1654    /// registered for the same extension capability. Server handler registries
1655    /// use it to reject handler configurations that could never be admitted at
1656    /// request dispatch.
1657    #[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    /// Returns the frozen descriptors in deterministic identifier order.
1667    #[must_use]
1668    pub fn descriptors(&self) -> impl ExactSizeIterator<Item = &ExtensionDescriptor> {
1669        self.descriptors.values()
1670    }
1671
1672    /// Preserves unknown peer settings as inert diagnostic data.
1673    #[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    /// Derives one frozen, bilateral extension set for the current exchange.
1684    ///
1685    /// The typed resolver is called only for descriptors enabled by both
1686    /// local gates and advertised by both peers on this exchange. The generic
1687    /// registry preserves the peer objects independently; it never treats
1688    /// JSON-object equality as compatibility.
1689    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        // EXT-01 freezes the canonical subject as escaped JSON token bytes,
1815        // rather than as an ordinary JSON document. Keep that framing before
1816        // hashing so the public receipt matches the frozen digest contract.
1817        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    /// Admits an active extension capability for one modern request.
1888    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    /// Admits a method owned by an active capability for one modern request.
1907    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    /// Admits a notification owned by an active capability for one modern request.
1936    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    /// Admits a result discriminator owned by an active capability for one modern request.
1972    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", &notification.name)?;
2053        if core_or_legacy_method(&notification.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 == &notification.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 are exclusively owned across the registry, so
2373            // each helper-built extension derives a distinct header from its
2374            // method; a shared literal would collide on the second register.
2375            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                        &registry,
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                    &registry,
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                    &registry,
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                    &registry,
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                &registry,
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(&registry, 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                    &registry,
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                    &registry,
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                    &registry,
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                &registry,
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(&registry, 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                    &registry,
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                    &registry,
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(&registry, 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                &registry,
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                &registry,
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(&registry, 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}