Skip to main content

fastmcp_protocol/
methods.rs

1//! Canonical MCP JSON-RPC method names.
2//!
3//! Centralizing these as constants prevents the class of typo bug where a method
4//! name is spelled slightly wrong at a call site (e.g. the lifecycle notification
5//! `notifications/initialized` being sent as bare `initialized`), which the wire
6//! protocol silently ignores rather than rejecting.
7
8use std::collections::BTreeMap;
9use std::sync::OnceLock;
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::common_types::JsonInteger;
15
16/// The only legacy MCP wire version represented by this isolated surface.
17pub const LEGACY_2024_11_05_PROTOCOL_VERSION: &str = "2024-11-05";
18
19/// Final stateless server-discovery request.
20///
21/// This aliases the discovery module's exact wire literal so method dispatch
22/// and public protocol consumers share one source of truth.
23pub const SERVER_DISCOVER: &str = crate::server_discovery::SERVER_DISCOVER_METHOD;
24
25/// Final long-lived subscription request.
26pub const SUBSCRIPTIONS_LISTEN: &str = "subscriptions/listen";
27
28/// Final subscription-established notification.
29pub const NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED: &str =
30    "notifications/subscriptions/acknowledged";
31
32/// SHA-256 of the pinned official 2024-11-05 JSON schema.
33pub const LEGACY_2024_11_05_SCHEMA_SHA256: &str =
34    "61cea2392d4f284092d09bc84b9ac488c0d5618ac2b38a56942fc5b99fd960ce";
35
36/// Exact official MCP 2024-11-05 Draft 7 JSON schema, vendored as a read-only
37/// source input.  This is deliberately not synthesized from newer protocol
38/// types: consumers can inspect the pinned source of truth directly.
39pub const LEGACY_2024_11_05_SCHEMA_JSON: &str =
40    include_str!("../schema/mcp-schema-2024-11-05-48234828.json");
41
42/// Parses the pinned legacy schema, retaining its exact source bytes in
43/// [`LEGACY_2024_11_05_SCHEMA_JSON`].
44///
45/// This remains fallible so a malformed vendored input fails closed rather
46/// than introducing a process-wide panic in a protocol consumer.
47pub fn legacy_2024_11_05_schema() -> Result<&'static Value, Legacy2024WireError> {
48    static SCHEMA: OnceLock<Result<Value, Legacy2024WireError>> = OnceLock::new();
49    match SCHEMA.get_or_init(|| {
50        serde_json::from_str(LEGACY_2024_11_05_SCHEMA_JSON)
51            .map_err(|_| Legacy2024WireError("pinned MCP 2024-11-05 schema is not valid JSON"))
52    }) {
53        Ok(schema) => Ok(schema),
54        Err(error) => Err(error.clone()),
55    }
56}
57
58/// Lifecycle `initialize` request.
59pub const INITIALIZE: &str = "initialize";
60
61/// Lifecycle `initialized` notification (spec-correct name).
62pub const NOTIFICATIONS_INITIALIZED: &str = "notifications/initialized";
63
64/// Tools list request.
65pub const TOOLS_LIST: &str = "tools/list";
66
67/// Tools call request.
68pub const TOOLS_CALL: &str = "tools/call";
69
70/// Resources list request.
71pub const RESOURCES_LIST: &str = "resources/list";
72
73/// Resource templates list request.
74pub const RESOURCES_TEMPLATES_LIST: &str = "resources/templates/list";
75
76/// Resources read request.
77pub const RESOURCES_READ: &str = "resources/read";
78
79/// Prompts list request.
80pub const PROMPTS_LIST: &str = "prompts/list";
81
82/// Prompts get request.
83pub const PROMPTS_GET: &str = "prompts/get";
84
85/// Logging set-level request.
86pub const LOGGING_SET_LEVEL: &str = "logging/setLevel";
87
88/// Cancellation notification.
89pub const NOTIFICATIONS_CANCELLED: &str = "notifications/cancelled";
90
91/// Logging message notification.
92pub const NOTIFICATIONS_MESSAGE: &str = "notifications/message";
93
94/// Ping request.
95pub const PING: &str = "ping";
96
97/// Completion request.
98pub const COMPLETION_COMPLETE: &str = "completion/complete";
99
100/// Server-to-client sampling request.
101pub const SAMPLING_CREATE_MESSAGE: &str = "sampling/createMessage";
102
103/// Server-to-client roots list request.
104pub const ROOTS_LIST: &str = "roots/list";
105
106/// Progress notification, valid in either direction.
107pub const NOTIFICATIONS_PROGRESS: &str = "notifications/progress";
108
109/// Prompt-list-change notification.
110pub const NOTIFICATIONS_PROMPTS_LIST_CHANGED: &str = "notifications/prompts/list_changed";
111
112/// Resource-list-change notification.
113pub const NOTIFICATIONS_RESOURCES_LIST_CHANGED: &str = "notifications/resources/list_changed";
114
115/// Resource-update notification.
116pub const NOTIFICATIONS_RESOURCES_UPDATED: &str = "notifications/resources/updated";
117
118/// Roots-list-change notification.
119pub const NOTIFICATIONS_ROOTS_LIST_CHANGED: &str = "notifications/roots/list_changed";
120
121/// Tool-list-change notification.
122pub const NOTIFICATIONS_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";
123
124/// Resource-subscription request.
125pub const RESOURCES_SUBSCRIBE: &str = "resources/subscribe";
126
127/// Resource-unsubscription request.
128pub const RESOURCES_UNSUBSCRIBE: &str = "resources/unsubscribe";
129
130/// Direction permitted by the active MCP 2026-07-28 core message unions.
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Final2026Direction {
133    /// Only clients may send this method.
134    ClientToServer,
135    /// Only servers may send this method.
136    ServerToClient,
137    /// Either peer may send this method.
138    Bidirectional,
139}
140
141/// Peer that originated an MCP 2026-07-28 core message.
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum Final2026Peer {
144    /// A client-originated message.
145    Client,
146    /// A server-originated message.
147    Server,
148}
149
150impl Final2026Direction {
151    /// Returns whether this direction admits a message from `peer`.
152    #[must_use]
153    pub const fn admits_sender(self, peer: Final2026Peer) -> bool {
154        matches!(
155            (self, peer),
156            (Self::ClientToServer, Final2026Peer::Client)
157                | (Self::ServerToClient, Final2026Peer::Server)
158                | (Self::Bidirectional, _)
159        )
160    }
161}
162
163/// JSON-RPC envelope kind required by an active MCP 2026-07-28 core method.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Final2026EnvelopeKind {
166    /// The method is a request and therefore requires a non-null request ID.
167    Request,
168    /// The method is a notification and therefore must omit its request ID.
169    Notification,
170}
171
172/// Exact direction and envelope metadata for one active MCP 2026-07-28 core
173/// method.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct Final2026Method {
176    /// Exact JSON-RPC method literal.
177    pub name: &'static str,
178    /// Peer direction admitted by the active final core union.
179    pub direction: Final2026Direction,
180    /// Request-versus-notification envelope constraint.
181    pub envelope: Final2026EnvelopeKind,
182}
183
184impl Final2026Method {
185    /// Returns whether this method is a notification admitted from `peer`.
186    #[must_use]
187    pub const fn admits_notification_from(self, peer: Final2026Peer) -> bool {
188        matches!(self.envelope, Final2026EnvelopeKind::Notification)
189            && self.direction.admits_sender(peer)
190    }
191}
192
193/// All and only the method literals in the active MCP 2026-07-28 core
194/// request and notification unions.
195///
196/// The pinned final schema retains types for historical reverse requests, but
197/// they are not members of its active `ClientRequest`, `ClientNotification`,
198/// or `ServerNotification` unions. They therefore do not enter this dispatch
199/// table. The exact 2024-11-05 table remains separate below.
200pub const FINAL_2026_07_28_METHODS: [Final2026Method; 18] = [
201    Final2026Method {
202        name: SERVER_DISCOVER,
203        direction: Final2026Direction::ClientToServer,
204        envelope: Final2026EnvelopeKind::Request,
205    },
206    Final2026Method {
207        name: COMPLETION_COMPLETE,
208        direction: Final2026Direction::ClientToServer,
209        envelope: Final2026EnvelopeKind::Request,
210    },
211    Final2026Method {
212        name: PROMPTS_GET,
213        direction: Final2026Direction::ClientToServer,
214        envelope: Final2026EnvelopeKind::Request,
215    },
216    Final2026Method {
217        name: PROMPTS_LIST,
218        direction: Final2026Direction::ClientToServer,
219        envelope: Final2026EnvelopeKind::Request,
220    },
221    Final2026Method {
222        name: RESOURCES_LIST,
223        direction: Final2026Direction::ClientToServer,
224        envelope: Final2026EnvelopeKind::Request,
225    },
226    Final2026Method {
227        name: RESOURCES_TEMPLATES_LIST,
228        direction: Final2026Direction::ClientToServer,
229        envelope: Final2026EnvelopeKind::Request,
230    },
231    Final2026Method {
232        name: RESOURCES_READ,
233        direction: Final2026Direction::ClientToServer,
234        envelope: Final2026EnvelopeKind::Request,
235    },
236    Final2026Method {
237        name: SUBSCRIPTIONS_LISTEN,
238        direction: Final2026Direction::ClientToServer,
239        envelope: Final2026EnvelopeKind::Request,
240    },
241    Final2026Method {
242        name: TOOLS_CALL,
243        direction: Final2026Direction::ClientToServer,
244        envelope: Final2026EnvelopeKind::Request,
245    },
246    Final2026Method {
247        name: TOOLS_LIST,
248        direction: Final2026Direction::ClientToServer,
249        envelope: Final2026EnvelopeKind::Request,
250    },
251    Final2026Method {
252        name: NOTIFICATIONS_CANCELLED,
253        direction: Final2026Direction::Bidirectional,
254        envelope: Final2026EnvelopeKind::Notification,
255    },
256    Final2026Method {
257        name: NOTIFICATIONS_PROGRESS,
258        direction: Final2026Direction::ServerToClient,
259        envelope: Final2026EnvelopeKind::Notification,
260    },
261    Final2026Method {
262        name: NOTIFICATIONS_MESSAGE,
263        direction: Final2026Direction::ServerToClient,
264        envelope: Final2026EnvelopeKind::Notification,
265    },
266    Final2026Method {
267        name: NOTIFICATIONS_RESOURCES_UPDATED,
268        direction: Final2026Direction::ServerToClient,
269        envelope: Final2026EnvelopeKind::Notification,
270    },
271    Final2026Method {
272        name: NOTIFICATIONS_RESOURCES_LIST_CHANGED,
273        direction: Final2026Direction::ServerToClient,
274        envelope: Final2026EnvelopeKind::Notification,
275    },
276    Final2026Method {
277        name: NOTIFICATIONS_TOOLS_LIST_CHANGED,
278        direction: Final2026Direction::ServerToClient,
279        envelope: Final2026EnvelopeKind::Notification,
280    },
281    Final2026Method {
282        name: NOTIFICATIONS_PROMPTS_LIST_CHANGED,
283        direction: Final2026Direction::ServerToClient,
284        envelope: Final2026EnvelopeKind::Notification,
285    },
286    Final2026Method {
287        name: NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED,
288        direction: Final2026Direction::ServerToClient,
289        envelope: Final2026EnvelopeKind::Notification,
290    },
291];
292
293/// Looks up one exact active MCP 2026-07-28 core method literal.
294#[must_use]
295pub fn final_2026_07_28_method(name: &str) -> Option<&'static Final2026Method> {
296    FINAL_2026_07_28_METHODS
297        .iter()
298        .find(|method| method.name == name)
299}
300
301/// Direction permitted by the 2024-11-05 tagged union.
302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum Legacy2024Direction {
304    /// Only clients may send this method.
305    ClientToServer,
306    /// Only servers may send this method.
307    ServerToClient,
308    /// Either peer may send this method.
309    Bidirectional,
310}
311
312/// JSON-RPC envelope kind required for a tagged method.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum Legacy2024EnvelopeKind {
315    /// The method is a request and therefore requires a non-null request ID.
316    Request,
317    /// The method is a notification and therefore must omit its request ID.
318    Notification,
319}
320
321/// Capability shape that owns a tagged method in the pinned legacy schema.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum Legacy2024Capability {
324    /// Client `sampling` capability.
325    ClientSampling,
326    /// Client `roots` capability.
327    ClientRoots,
328    /// Client `roots.listChanged` capability.
329    ClientRootsListChanged,
330    /// Server `logging` capability.
331    ServerLogging,
332    /// Server `prompts` capability.
333    ServerPrompts,
334    /// Server `prompts.listChanged` capability.
335    ServerPromptsListChanged,
336    /// Server `resources` capability.
337    ServerResources,
338    /// Server `resources.subscribe` capability.
339    ServerResourcesSubscribe,
340    /// Server `resources.listChanged` capability.
341    ServerResourcesListChanged,
342    /// Server `tools` capability.
343    ServerTools,
344    /// Server `tools.listChanged` capability.
345    ServerToolsListChanged,
346}
347
348/// Exact direction, envelope, and capability metadata for one tagged legacy method.
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct Legacy2024Method {
351    /// Exact JSON-RPC method literal.
352    pub name: &'static str,
353    /// Peer direction admitted by the tagged union.
354    pub direction: Legacy2024Direction,
355    /// Request-versus-notification envelope constraint.
356    pub envelope: Legacy2024EnvelopeKind,
357    /// Required advertised capability, when the 2024 schema defines one.
358    pub capability: Option<Legacy2024Capability>,
359}
360
361/// All and only the 24 method literals in the pinned MCP 2024-11-05 schema.
362pub const LEGACY_2024_11_05_METHODS: [Legacy2024Method; 24] = [
363    Legacy2024Method {
364        name: INITIALIZE,
365        direction: Legacy2024Direction::ClientToServer,
366        envelope: Legacy2024EnvelopeKind::Request,
367        capability: None,
368    },
369    Legacy2024Method {
370        name: NOTIFICATIONS_INITIALIZED,
371        direction: Legacy2024Direction::ClientToServer,
372        envelope: Legacy2024EnvelopeKind::Notification,
373        capability: None,
374    },
375    Legacy2024Method {
376        name: PING,
377        direction: Legacy2024Direction::Bidirectional,
378        envelope: Legacy2024EnvelopeKind::Request,
379        capability: None,
380    },
381    Legacy2024Method {
382        name: TOOLS_LIST,
383        direction: Legacy2024Direction::ClientToServer,
384        envelope: Legacy2024EnvelopeKind::Request,
385        capability: Some(Legacy2024Capability::ServerTools),
386    },
387    Legacy2024Method {
388        name: TOOLS_CALL,
389        direction: Legacy2024Direction::ClientToServer,
390        envelope: Legacy2024EnvelopeKind::Request,
391        capability: Some(Legacy2024Capability::ServerTools),
392    },
393    Legacy2024Method {
394        name: RESOURCES_LIST,
395        direction: Legacy2024Direction::ClientToServer,
396        envelope: Legacy2024EnvelopeKind::Request,
397        capability: Some(Legacy2024Capability::ServerResources),
398    },
399    Legacy2024Method {
400        name: RESOURCES_TEMPLATES_LIST,
401        direction: Legacy2024Direction::ClientToServer,
402        envelope: Legacy2024EnvelopeKind::Request,
403        capability: Some(Legacy2024Capability::ServerResources),
404    },
405    Legacy2024Method {
406        name: RESOURCES_READ,
407        direction: Legacy2024Direction::ClientToServer,
408        envelope: Legacy2024EnvelopeKind::Request,
409        capability: Some(Legacy2024Capability::ServerResources),
410    },
411    Legacy2024Method {
412        name: RESOURCES_SUBSCRIBE,
413        direction: Legacy2024Direction::ClientToServer,
414        envelope: Legacy2024EnvelopeKind::Request,
415        capability: Some(Legacy2024Capability::ServerResourcesSubscribe),
416    },
417    Legacy2024Method {
418        name: RESOURCES_UNSUBSCRIBE,
419        direction: Legacy2024Direction::ClientToServer,
420        envelope: Legacy2024EnvelopeKind::Request,
421        capability: Some(Legacy2024Capability::ServerResourcesSubscribe),
422    },
423    Legacy2024Method {
424        name: PROMPTS_LIST,
425        direction: Legacy2024Direction::ClientToServer,
426        envelope: Legacy2024EnvelopeKind::Request,
427        capability: Some(Legacy2024Capability::ServerPrompts),
428    },
429    Legacy2024Method {
430        name: PROMPTS_GET,
431        direction: Legacy2024Direction::ClientToServer,
432        envelope: Legacy2024EnvelopeKind::Request,
433        capability: Some(Legacy2024Capability::ServerPrompts),
434    },
435    Legacy2024Method {
436        name: LOGGING_SET_LEVEL,
437        direction: Legacy2024Direction::ClientToServer,
438        envelope: Legacy2024EnvelopeKind::Request,
439        capability: Some(Legacy2024Capability::ServerLogging),
440    },
441    Legacy2024Method {
442        name: COMPLETION_COMPLETE,
443        direction: Legacy2024Direction::ClientToServer,
444        envelope: Legacy2024EnvelopeKind::Request,
445        capability: None,
446    },
447    Legacy2024Method {
448        name: SAMPLING_CREATE_MESSAGE,
449        direction: Legacy2024Direction::ServerToClient,
450        envelope: Legacy2024EnvelopeKind::Request,
451        capability: Some(Legacy2024Capability::ClientSampling),
452    },
453    Legacy2024Method {
454        name: ROOTS_LIST,
455        direction: Legacy2024Direction::ServerToClient,
456        envelope: Legacy2024EnvelopeKind::Request,
457        capability: Some(Legacy2024Capability::ClientRoots),
458    },
459    Legacy2024Method {
460        name: NOTIFICATIONS_CANCELLED,
461        direction: Legacy2024Direction::Bidirectional,
462        envelope: Legacy2024EnvelopeKind::Notification,
463        capability: None,
464    },
465    Legacy2024Method {
466        name: NOTIFICATIONS_PROGRESS,
467        direction: Legacy2024Direction::Bidirectional,
468        envelope: Legacy2024EnvelopeKind::Notification,
469        capability: None,
470    },
471    Legacy2024Method {
472        name: NOTIFICATIONS_ROOTS_LIST_CHANGED,
473        direction: Legacy2024Direction::ClientToServer,
474        envelope: Legacy2024EnvelopeKind::Notification,
475        capability: Some(Legacy2024Capability::ClientRootsListChanged),
476    },
477    Legacy2024Method {
478        name: NOTIFICATIONS_MESSAGE,
479        direction: Legacy2024Direction::ServerToClient,
480        envelope: Legacy2024EnvelopeKind::Notification,
481        capability: Some(Legacy2024Capability::ServerLogging),
482    },
483    Legacy2024Method {
484        name: NOTIFICATIONS_PROMPTS_LIST_CHANGED,
485        direction: Legacy2024Direction::ServerToClient,
486        envelope: Legacy2024EnvelopeKind::Notification,
487        capability: Some(Legacy2024Capability::ServerPromptsListChanged),
488    },
489    Legacy2024Method {
490        name: NOTIFICATIONS_RESOURCES_LIST_CHANGED,
491        direction: Legacy2024Direction::ServerToClient,
492        envelope: Legacy2024EnvelopeKind::Notification,
493        capability: Some(Legacy2024Capability::ServerResourcesListChanged),
494    },
495    Legacy2024Method {
496        name: NOTIFICATIONS_RESOURCES_UPDATED,
497        direction: Legacy2024Direction::ServerToClient,
498        envelope: Legacy2024EnvelopeKind::Notification,
499        capability: Some(Legacy2024Capability::ServerResourcesSubscribe),
500    },
501    Legacy2024Method {
502        name: NOTIFICATIONS_TOOLS_LIST_CHANGED,
503        direction: Legacy2024Direction::ServerToClient,
504        envelope: Legacy2024EnvelopeKind::Notification,
505        capability: Some(Legacy2024Capability::ServerToolsListChanged),
506    },
507];
508
509/// Looks up one exact tagged 2024-11-05 method literal.
510#[must_use]
511pub fn legacy_2024_11_05_method(name: &str) -> Option<&'static Legacy2024Method> {
512    LEGACY_2024_11_05_METHODS
513        .iter()
514        .find(|method| method.name == name)
515}
516
517/// Typed shape of the 2024-11-05 client capabilities object.
518#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
519pub struct Legacy2024ClientCapabilities {
520    /// Non-standard client capabilities retained by the exact schema.
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub experimental: Option<BTreeMap<String, Value>>,
523    /// Sampling support, represented by an open object in the pinned schema.
524    #[serde(skip_serializing_if = "Option::is_none")]
525    pub sampling: Option<BTreeMap<String, Value>>,
526    /// Root-list support.
527    #[serde(skip_serializing_if = "Option::is_none")]
528    pub roots: Option<Legacy2024RootsCapability>,
529    /// Additional non-standard capability members allowed by the 2024 schema.
530    #[serde(flatten)]
531    pub extensions: BTreeMap<String, Value>,
532}
533
534/// Exact 2024-11-05 root capability shape.
535#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
536pub struct Legacy2024RootsCapability {
537    /// Whether root-list-change notifications are supported.
538    #[serde(
539        default,
540        rename = "listChanged",
541        skip_serializing_if = "std::ops::Not::not"
542    )]
543    pub list_changed: bool,
544    /// Additional fields allowed by the pinned open object shape.
545    #[serde(flatten)]
546    pub extensions: BTreeMap<String, Value>,
547}
548
549/// Typed shape of the 2024-11-05 server capabilities object.
550#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
551pub struct Legacy2024ServerCapabilities {
552    /// Non-standard server capabilities retained by the exact schema.
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub experimental: Option<BTreeMap<String, Value>>,
555    /// Server logging capability, represented by an open object.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub logging: Option<BTreeMap<String, Value>>,
558    /// Prompt capability shape.
559    #[serde(skip_serializing_if = "Option::is_none")]
560    pub prompts: Option<Legacy2024ListChangedCapability>,
561    /// Resource capability shape.
562    #[serde(skip_serializing_if = "Option::is_none")]
563    pub resources: Option<Legacy2024ResourcesCapability>,
564    /// Tool capability shape.
565    #[serde(skip_serializing_if = "Option::is_none")]
566    pub tools: Option<Legacy2024ListChangedCapability>,
567    /// Additional non-standard capability members allowed by the 2024 schema.
568    #[serde(flatten)]
569    pub extensions: BTreeMap<String, Value>,
570}
571
572/// Exact 2024-11-05 `listChanged` capability shape.
573#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
574pub struct Legacy2024ListChangedCapability {
575    /// Whether list-change notifications are supported.
576    #[serde(
577        default,
578        rename = "listChanged",
579        skip_serializing_if = "std::ops::Not::not"
580    )]
581    pub list_changed: bool,
582    /// Additional fields allowed by the pinned open object shape.
583    #[serde(flatten)]
584    pub extensions: BTreeMap<String, Value>,
585}
586
587/// Exact 2024-11-05 resources capability shape.
588#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
589pub struct Legacy2024ResourcesCapability {
590    /// Whether resource subscriptions are supported.
591    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
592    pub subscribe: bool,
593    /// Whether resource-list-change notifications are supported.
594    #[serde(
595        default,
596        rename = "listChanged",
597        skip_serializing_if = "std::ops::Not::not"
598    )]
599    pub list_changed: bool,
600    /// Additional fields allowed by the pinned open object shape.
601    #[serde(flatten)]
602    pub extensions: BTreeMap<String, Value>,
603}
604
605/// Validates and decodes the exact 2024-11-05 client capability shape.
606pub fn decode_legacy_2024_11_05_client_capabilities(
607    value: Value,
608) -> Result<Legacy2024ClientCapabilities, Legacy2024WireError> {
609    validate_legacy_2024_client_capability_members(&value)?;
610    let capabilities: Legacy2024ClientCapabilities = serde_json::from_value(value)
611        .map_err(|_| Legacy2024WireError("MCP 2024-11-05 client capabilities must be an object"))?;
612    validate_legacy_2024_experimental_capabilities(capabilities.experimental.as_ref())?;
613    Ok(capabilities)
614}
615
616/// Validates and decodes the exact 2024-11-05 server capability shape.
617pub fn decode_legacy_2024_11_05_server_capabilities(
618    value: Value,
619) -> Result<Legacy2024ServerCapabilities, Legacy2024WireError> {
620    let capabilities: Legacy2024ServerCapabilities = serde_json::from_value(value)
621        .map_err(|_| Legacy2024WireError("MCP 2024-11-05 server capabilities must be an object"))?;
622    validate_legacy_2024_experimental_capabilities(capabilities.experimental.as_ref())?;
623    Ok(capabilities)
624}
625
626/// Validates initialization-era server metadata before a consumer accepts it.
627pub fn validate_legacy_2024_11_05_initialize_result(
628    value: &Value,
629) -> Result<Legacy2024ServerCapabilities, Legacy2024WireError> {
630    let result = value.as_object().ok_or(Legacy2024WireError(
631        "MCP 2024-11-05 initialize result must be an object",
632    ))?;
633    if result.get("protocolVersion")
634        != Some(&Value::String(
635            LEGACY_2024_11_05_PROTOCOL_VERSION.to_owned(),
636        ))
637    {
638        return Err(Legacy2024WireError(
639            "initialize result protocolVersion must be exact MCP 2024-11-05",
640        ));
641    }
642    let server_info =
643        result
644            .get("serverInfo")
645            .and_then(Value::as_object)
646            .ok_or(Legacy2024WireError(
647                "MCP 2024-11-05 initialize result requires serverInfo object",
648            ))?;
649    if !server_info.get("name").is_some_and(Value::is_string)
650        || !server_info.get("version").is_some_and(Value::is_string)
651    {
652        return Err(Legacy2024WireError(
653            "MCP 2024-11-05 initialize result serverInfo requires string name and version",
654        ));
655    }
656    let capabilities = result
657        .get("capabilities")
658        .cloned()
659        .ok_or(Legacy2024WireError(
660            "MCP 2024-11-05 initialize result requires server capabilities",
661        ))?;
662    decode_legacy_2024_11_05_server_capabilities(capabilities)
663}
664
665fn validate_legacy_2024_client_capability_members(
666    value: &Value,
667) -> Result<(), Legacy2024WireError> {
668    let capabilities = value.as_object().ok_or(Legacy2024WireError(
669        "MCP 2024-11-05 client capabilities must be an object",
670    ))?;
671    if ["experimental", "sampling", "roots"]
672        .iter()
673        .any(|member| capabilities.get(*member).is_some_and(Value::is_null))
674    {
675        return Err(Legacy2024WireError(
676            "MCP 2024-11-05 client capability members must be objects when present",
677        ));
678    }
679    Ok(())
680}
681
682fn validate_legacy_2024_experimental_capabilities(
683    experimental: Option<&BTreeMap<String, Value>>,
684) -> Result<(), Legacy2024WireError> {
685    if experimental.is_some_and(|experimental| {
686        experimental
687            .values()
688            .any(|capability| !capability.is_object())
689    }) {
690        return Err(Legacy2024WireError(
691            "MCP 2024-11-05 experimental capabilities must map names to objects",
692        ));
693    }
694    Ok(())
695}
696
697/// A decoded exact-2024 JSON-RPC envelope.
698#[derive(Debug, Clone, PartialEq)]
699pub enum Legacy2024Envelope {
700    /// A tagged request with a non-null JSON-RPC request ID.
701    Request {
702        method: &'static Legacy2024Method,
703        id: Value,
704        params: Option<Value>,
705    },
706    /// A tagged notification which omits JSON-RPC request ID.
707    Notification {
708        method: &'static Legacy2024Method,
709        params: Option<Value>,
710    },
711    /// A successful JSON-RPC result envelope.
712    Response { id: Value, result: Value },
713    /// A JSON-RPC error envelope.
714    Error { id: Value, error: Value },
715}
716
717/// An exact-2024 raw wire admission failure.
718#[derive(Debug, Clone, PartialEq, Eq)]
719pub struct Legacy2024WireError(&'static str);
720
721impl Legacy2024WireError {
722    /// Stable reason intended for callers that need an exact refusal category.
723    #[must_use]
724    pub const fn reason(&self) -> &'static str {
725        self.0
726    }
727}
728
729impl std::fmt::Display for Legacy2024WireError {
730    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
731        formatter.write_str(self.0)
732    }
733}
734
735impl std::error::Error for Legacy2024WireError {}
736
737/// Classifies a server result after exact-2024 lossless admission.
738///
739/// Only the three ordinary server results that can cross this boundary are
740/// represented here. Modern task, structured-content, Apps, and elicitation
741/// surfaces have no exact 2024 wire equivalent and are refused instead of
742/// being silently dropped.
743#[derive(Debug, Clone, Copy, PartialEq, Eq)]
744pub enum Legacy2024ResultKind {
745    /// A `tools/call` result with a `content` array.
746    Tool,
747    /// A `resources/read` result with a `contents` array.
748    Resource,
749    /// A `prompts/get` result with a `messages` array.
750    Prompt,
751}
752
753/// Exact disposition of a result at the 2024-11-05 shared-result boundary.
754#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755pub enum Legacy2024ResultDisposition {
756    /// The shared result has a complete, field-preserving legacy representation.
757    Lossless(Legacy2024ResultKind),
758    /// The method belongs to the exact legacy adapter and has no shared result mapping.
759    LegacyOwned,
760}
761
762/// Classifies a result before it can cross from shared handling to exact 2024.
763///
764/// A successful [`Legacy2024ResultDisposition::Lossless`] classification proves
765/// that the result uses only the exact 2024 fields and recursively valid
766/// legacy values. A recognized legacy method without an ordinary shared result
767/// is [`Legacy2024ResultDisposition::LegacyOwned`]; unknown methods and every
768/// modern or malformed result are rejected.
769pub fn classify_legacy_2024_result(
770    method: &str,
771    result: &Value,
772) -> Result<Legacy2024ResultDisposition, Legacy2024WireError> {
773    let kind = match method {
774        TOOLS_CALL => Legacy2024ResultKind::Tool,
775        RESOURCES_READ => Legacy2024ResultKind::Resource,
776        PROMPTS_GET => Legacy2024ResultKind::Prompt,
777        _ if legacy_2024_11_05_method(method).is_some() => {
778            return Ok(Legacy2024ResultDisposition::LegacyOwned);
779        }
780        _ => {
781            return Err(Legacy2024WireError(
782                "method is not part of exact MCP 2024-11-05",
783            ));
784        }
785    };
786    let object = result.as_object().ok_or(Legacy2024WireError(
787        "ordinary MCP 2024-11-05 results must be objects",
788    ))?;
789    if crate::result::has_final_only_metadata(result) {
790        return Err(Legacy2024WireError(
791            "final protocol metadata cannot be represented by exact MCP 2024-11-05",
792        ));
793    }
794    validate_legacy_2024_result_members(kind, object)?;
795    Ok(Legacy2024ResultDisposition::Lossless(kind))
796}
797
798/// Returns an ordinary complete result unchanged only when it has an exact
799/// 2024 representation for `method`.
800pub fn translate_legacy_2024_result(
801    method: &str,
802    result: Value,
803) -> Result<Value, Legacy2024WireError> {
804    match classify_legacy_2024_result(method, &result)? {
805        Legacy2024ResultDisposition::Lossless(_) => Ok(result),
806        Legacy2024ResultDisposition::LegacyOwned => Err(Legacy2024WireError(
807            "method result is owned by the exact MCP 2024-11-05 adapter",
808        )),
809    }
810}
811
812fn validate_legacy_2024_result_members(
813    kind: Legacy2024ResultKind,
814    object: &serde_json::Map<String, Value>,
815) -> Result<(), Legacy2024WireError> {
816    let allowed: &[&str] = match kind {
817        Legacy2024ResultKind::Tool => &["content", "isError", "_meta"],
818        Legacy2024ResultKind::Resource => &["contents", "_meta"],
819        Legacy2024ResultKind::Prompt => &["messages", "description", "_meta"],
820    };
821    if [
822        "structuredContent",
823        "task",
824        "tasks",
825        "apps",
826        "elicitation",
827        "extensions",
828        "meta",
829    ]
830    .iter()
831    .any(|member| object.contains_key(*member))
832    {
833        return Err(Legacy2024WireError(
834            "modern-only result member cannot be represented by exact MCP 2024-11-05",
835        ));
836    }
837    if object
838        .keys()
839        .any(|member| !allowed.contains(&member.as_str()))
840    {
841        return Err(Legacy2024WireError(
842            "unclassified result member cannot be represented by exact MCP 2024-11-05",
843        ));
844    }
845    if !object.get("_meta").is_none_or(Value::is_object) {
846        return Err(Legacy2024WireError(
847            "exact MCP 2024-11-05 result _meta must be an object",
848        ));
849    }
850    match kind {
851        Legacy2024ResultKind::Tool => {
852            if !object.get("isError").is_none_or(Value::is_boolean) {
853                return Err(Legacy2024WireError(
854                    "tools/call result isError must be a boolean",
855                ));
856            }
857            let content =
858                object
859                    .get("content")
860                    .and_then(Value::as_array)
861                    .ok_or(Legacy2024WireError(
862                        "tools/call result requires a content array",
863                    ))?;
864            for item in content {
865                validate_legacy_2024_content(item)?;
866            }
867        }
868        Legacy2024ResultKind::Resource => {
869            let contents =
870                object
871                    .get("contents")
872                    .and_then(Value::as_array)
873                    .ok_or(Legacy2024WireError(
874                        "resources/read result requires a contents array",
875                    ))?;
876            for resource in contents {
877                validate_legacy_2024_resource_contents(resource)?;
878            }
879        }
880        Legacy2024ResultKind::Prompt => {
881            if !object.get("description").is_none_or(Value::is_string) {
882                return Err(Legacy2024WireError(
883                    "prompts/get result description must be a string",
884                ));
885            }
886            let messages =
887                object
888                    .get("messages")
889                    .and_then(Value::as_array)
890                    .ok_or(Legacy2024WireError(
891                        "prompts/get result requires a messages array",
892                    ))?;
893            for message in messages {
894                validate_legacy_2024_prompt_message(message)?;
895            }
896        }
897    }
898    Ok(())
899}
900
901fn validate_legacy_2024_prompt_message(value: &Value) -> Result<(), Legacy2024WireError> {
902    let message = value.as_object().ok_or(Legacy2024WireError(
903        "prompts/get result messages must contain objects",
904    ))?;
905    if !matches!(
906        message.get("role").and_then(Value::as_str),
907        Some("user" | "assistant")
908    ) {
909        return Err(Legacy2024WireError(
910            "prompts/get result messages require an exact user or assistant role",
911        ));
912    }
913    let content = message.get("content").ok_or(Legacy2024WireError(
914        "prompts/get result messages require content",
915    ))?;
916    validate_legacy_2024_content(content)
917}
918
919fn validate_legacy_2024_content(value: &Value) -> Result<(), Legacy2024WireError> {
920    let content = value.as_object().ok_or(Legacy2024WireError(
921        "exact MCP 2024-11-05 content entries must be objects",
922    ))?;
923    match content.get("type").and_then(Value::as_str) {
924        Some("text") if content.get("text").is_some_and(Value::is_string) => {}
925        Some("image")
926            if content.get("data").is_some_and(Value::is_string)
927                && content.get("mimeType").is_some_and(Value::is_string) => {}
928        Some("resource") => {
929            let resource = content.get("resource").ok_or(Legacy2024WireError(
930                "embedded resource content requires resource data",
931            ))?;
932            validate_legacy_2024_resource_contents(resource)?;
933        }
934        _ => {
935            return Err(Legacy2024WireError(
936                "exact MCP 2024-11-05 content must be text, image, or resource",
937            ));
938        }
939    }
940    if let Some(annotations) = content.get("annotations") {
941        validate_legacy_2024_annotations(annotations)?;
942    }
943    Ok(())
944}
945
946fn validate_legacy_2024_resource_contents(value: &Value) -> Result<(), Legacy2024WireError> {
947    let resource = value.as_object().ok_or(Legacy2024WireError(
948        "exact MCP 2024-11-05 resource contents must be objects",
949    ))?;
950    if !resource.get("uri").is_some_and(Value::is_string)
951        || !resource.get("mimeType").is_none_or(Value::is_string)
952        || !(resource.get("text").is_some_and(Value::is_string)
953            || resource.get("blob").is_some_and(Value::is_string))
954    {
955        return Err(Legacy2024WireError(
956            "exact MCP 2024-11-05 resource contents require string uri and text or blob data",
957        ));
958    }
959    Ok(())
960}
961
962/// Validates the required exact-2024 parameter members for the methods used by
963/// the server adapter. JSON-RPC itself already guarantees an object whenever
964/// parameters are present; this function adds the pinned method-level shape.
965pub fn validate_legacy_2024_11_05_method_params(
966    method: &str,
967    params: Option<&Value>,
968) -> Result<(), Legacy2024WireError> {
969    reject_final_metadata_from_legacy_params(params)?;
970    match method {
971        TOOLS_CALL => {
972            let params = required_params_object(method, params)?;
973            required_string(params, "name", "tools/call")?;
974            optional_object(params, "arguments", "tools/call")
975        }
976        RESOURCES_READ | RESOURCES_SUBSCRIBE | RESOURCES_UNSUBSCRIBE => {
977            let params = required_params_object(method, params)?;
978            required_string(params, "uri", method)
979        }
980        PROMPTS_GET => {
981            let params = required_params_object(method, params)?;
982            required_string(params, "name", "prompts/get")?;
983            let Some(arguments) = params.get("arguments") else {
984                return Ok(());
985            };
986            let arguments = arguments.as_object().ok_or(Legacy2024WireError(
987                "prompts/get arguments must be an object",
988            ))?;
989            if arguments.values().all(Value::is_string) {
990                Ok(())
991            } else {
992                Err(Legacy2024WireError(
993                    "prompts/get arguments must map names to strings",
994                ))
995            }
996        }
997        COMPLETION_COMPLETE => {
998            let params = required_params_object(method, params)?;
999            let argument =
1000                params
1001                    .get("argument")
1002                    .and_then(Value::as_object)
1003                    .ok_or(Legacy2024WireError(
1004                        "completion/complete requires an argument object",
1005                    ))?;
1006            required_string(argument, "name", "completion/complete argument")?;
1007            required_string(argument, "value", "completion/complete argument")?;
1008            let reference =
1009                params
1010                    .get("ref")
1011                    .and_then(Value::as_object)
1012                    .ok_or(Legacy2024WireError(
1013                        "completion/complete requires a reference object",
1014                    ))?;
1015            match reference.get("type").and_then(Value::as_str) {
1016                Some("ref/prompt") => required_string(reference, "name", "prompt reference"),
1017                Some("ref/resource") => required_string(reference, "uri", "resource reference"),
1018                _ => Err(Legacy2024WireError(
1019                    "completion/complete reference must be an exact prompt or resource reference",
1020                )),
1021            }
1022        }
1023        LOGGING_SET_LEVEL => {
1024            let params = required_params_object(method, params)?;
1025            let level = params
1026                .get("level")
1027                .and_then(Value::as_str)
1028                .ok_or(Legacy2024WireError(
1029                    "logging/setLevel requires a string level",
1030                ))?;
1031            if matches!(
1032                level,
1033                "alert"
1034                    | "critical"
1035                    | "debug"
1036                    | "emergency"
1037                    | "error"
1038                    | "info"
1039                    | "notice"
1040                    | "warning"
1041            ) {
1042                Ok(())
1043            } else {
1044                Err(Legacy2024WireError(
1045                    "logging/setLevel requires an exact MCP 2024-11-05 level",
1046                ))
1047            }
1048        }
1049        NOTIFICATIONS_CANCELLED => {
1050            let params = required_params_object(method, params)?;
1051            if !params.get("requestId").is_some_and(legacy_2024_request_id) {
1052                return Err(Legacy2024WireError(
1053                    "notifications/cancelled requires a non-null string or integer requestId",
1054                ));
1055            }
1056            if params.get("reason").is_none_or(Value::is_string) {
1057                Ok(())
1058            } else {
1059                Err(Legacy2024WireError(
1060                    "notifications/cancelled reason must be a string",
1061                ))
1062            }
1063        }
1064        NOTIFICATIONS_PROGRESS => {
1065            let params = required_params_object(method, params)?;
1066            let token = params.get("progressToken");
1067            if !token.is_some_and(legacy_2024_request_id)
1068                || !params.get("progress").is_some_and(Value::is_number)
1069                || !params.get("total").is_none_or(Value::is_number)
1070            {
1071                return Err(Legacy2024WireError(
1072                    "notifications/progress requires exact token, progress, and optional total members",
1073                ));
1074            }
1075            Ok(())
1076        }
1077        SAMPLING_CREATE_MESSAGE => {
1078            let params = required_params_object(method, params)?;
1079            validate_legacy_2024_sampling_create_message(params)
1080        }
1081        NOTIFICATIONS_MESSAGE => {
1082            let params = required_params_object(method, params)?;
1083            if !params.contains_key("data") {
1084                return Err(Legacy2024WireError(
1085                    "notifications/message requires a data member",
1086                ));
1087            }
1088            let level = params
1089                .get("level")
1090                .and_then(Value::as_str)
1091                .ok_or(Legacy2024WireError(
1092                    "notifications/message requires a string level",
1093                ))?;
1094            if !matches!(
1095                level,
1096                "alert"
1097                    | "critical"
1098                    | "debug"
1099                    | "emergency"
1100                    | "error"
1101                    | "info"
1102                    | "notice"
1103                    | "warning"
1104            ) || !params.get("logger").is_none_or(Value::is_string)
1105            {
1106                return Err(Legacy2024WireError(
1107                    "notifications/message requires exact level and optional string logger",
1108                ));
1109            }
1110            Ok(())
1111        }
1112        NOTIFICATIONS_RESOURCES_UPDATED => {
1113            let params = required_params_object(method, params)?;
1114            required_string(params, "uri", "notifications/resources/updated")
1115        }
1116        TOOLS_LIST | RESOURCES_LIST | RESOURCES_TEMPLATES_LIST | PROMPTS_LIST => {
1117            validate_legacy_2024_cursor_params(params, method)
1118        }
1119        NOTIFICATIONS_ROOTS_LIST_CHANGED
1120        | NOTIFICATIONS_INITIALIZED
1121        | NOTIFICATIONS_PROMPTS_LIST_CHANGED
1122        | NOTIFICATIONS_RESOURCES_LIST_CHANGED
1123        | NOTIFICATIONS_TOOLS_LIST_CHANGED => validate_legacy_2024_metadata_params(params, false),
1124        ROOTS_LIST | PING => validate_legacy_2024_metadata_params(params, true),
1125        INITIALIZE => validate_legacy_2024_initialize(method, params),
1126        _ => Err(Legacy2024WireError(
1127            "method is not part of exact MCP 2024-11-05",
1128        )),
1129    }
1130}
1131
1132/// Rejects final-era protocol metadata before it can be interpreted as open
1133/// legacy application metadata.
1134///
1135/// The exact 2024 envelope permits application-defined `_meta` entries, but
1136/// the final-era reserved names select different request semantics. Keeping
1137/// that distinction at raw admission prevents either adapter direction from
1138/// silently crossing eras.
1139fn reject_final_metadata_from_legacy_params(
1140    params: Option<&Value>,
1141) -> Result<(), Legacy2024WireError> {
1142    if params.is_some_and(crate::result::has_final_only_metadata) {
1143        Err(Legacy2024WireError(
1144            "final protocol metadata cannot be represented by exact MCP 2024-11-05",
1145        ))
1146    } else {
1147        Ok(())
1148    }
1149}
1150
1151fn required_params_object<'a>(
1152    method: &str,
1153    params: Option<&'a Value>,
1154) -> Result<&'a serde_json::Map<String, Value>, Legacy2024WireError> {
1155    params
1156        .and_then(Value::as_object)
1157        .ok_or(Legacy2024WireError(match method {
1158            TOOLS_CALL => "tools/call requires object params",
1159            RESOURCES_READ => "resources/read requires object params",
1160            RESOURCES_SUBSCRIBE => "resources/subscribe requires object params",
1161            RESOURCES_UNSUBSCRIBE => "resources/unsubscribe requires object params",
1162            PROMPTS_GET => "prompts/get requires object params",
1163            COMPLETION_COMPLETE => "completion/complete requires object params",
1164            LOGGING_SET_LEVEL => "logging/setLevel requires object params",
1165            NOTIFICATIONS_CANCELLED => "notifications/cancelled requires object params",
1166            NOTIFICATIONS_PROGRESS => "notifications/progress requires object params",
1167            SAMPLING_CREATE_MESSAGE => "sampling/createMessage requires object params",
1168            NOTIFICATIONS_MESSAGE => "notifications/message requires object params",
1169            NOTIFICATIONS_RESOURCES_UPDATED => {
1170                "notifications/resources/updated requires object params"
1171            }
1172            _ => "exact MCP 2024-11-05 method requires object params",
1173        }))
1174}
1175
1176fn validate_legacy_2024_sampling_create_message(
1177    params: &serde_json::Map<String, Value>,
1178) -> Result<(), Legacy2024WireError> {
1179    let messages = params
1180        .get("messages")
1181        .and_then(Value::as_array)
1182        .ok_or(Legacy2024WireError(
1183            "sampling/createMessage requires a messages array",
1184        ))?;
1185    if !params
1186        .get("maxTokens")
1187        .is_some_and(legacy_2024_json_integer)
1188    {
1189        return Err(Legacy2024WireError(
1190            "sampling/createMessage requires integer maxTokens",
1191        ));
1192    }
1193    for message in messages {
1194        validate_legacy_2024_sampling_message(message)?;
1195    }
1196    if !params
1197        .get("includeContext")
1198        .is_none_or(|value| matches!(value.as_str(), Some("allServers" | "none" | "thisServer")))
1199    {
1200        return Err(Legacy2024WireError(
1201            "sampling/createMessage includeContext must be allServers, none, or thisServer",
1202        ));
1203    }
1204    if !params.get("systemPrompt").is_none_or(Value::is_string)
1205        || !params.get("temperature").is_none_or(Value::is_number)
1206        || !params.get("stopSequences").is_none_or(|value| {
1207            value
1208                .as_array()
1209                .is_some_and(|items| items.iter().all(Value::is_string))
1210        })
1211        || !params.get("metadata").is_none_or(Value::is_object)
1212    {
1213        return Err(Legacy2024WireError(
1214            "sampling/createMessage optional fields must match the exact 2024-11-05 shapes",
1215        ));
1216    }
1217    if let Some(model_preferences) = params.get("modelPreferences") {
1218        validate_legacy_2024_model_preferences(model_preferences)?;
1219    }
1220    Ok(())
1221}
1222
1223fn validate_legacy_2024_sampling_message(value: &Value) -> Result<(), Legacy2024WireError> {
1224    let message = value.as_object().ok_or(Legacy2024WireError(
1225        "sampling/createMessage messages must contain objects",
1226    ))?;
1227    if !matches!(
1228        message.get("role").and_then(Value::as_str),
1229        Some("user" | "assistant")
1230    ) {
1231        return Err(Legacy2024WireError(
1232            "sampling/createMessage messages require an exact user or assistant role",
1233        ));
1234    }
1235    let content = message
1236        .get("content")
1237        .and_then(Value::as_object)
1238        .ok_or(Legacy2024WireError(
1239            "sampling/createMessage messages require text or image content",
1240        ))?;
1241    match content.get("type").and_then(Value::as_str) {
1242        Some("text") if content.get("text").is_some_and(Value::is_string) => {}
1243        Some("image")
1244            if content.get("data").is_some_and(Value::is_string)
1245                && content.get("mimeType").is_some_and(Value::is_string) => {}
1246        _ => {
1247            return Err(Legacy2024WireError(
1248                "sampling/createMessage messages require exact text or image content",
1249            ));
1250        }
1251    }
1252    if let Some(annotations) = content.get("annotations") {
1253        validate_legacy_2024_annotations(annotations)?;
1254    }
1255    Ok(())
1256}
1257
1258fn validate_legacy_2024_annotations(value: &Value) -> Result<(), Legacy2024WireError> {
1259    let annotations = value.as_object().ok_or(Legacy2024WireError(
1260        "sampling content annotations must be an object",
1261    ))?;
1262    if !annotations.get("audience").is_none_or(|value| {
1263        value.as_array().is_some_and(|audience| {
1264            audience
1265                .iter()
1266                .all(|role| matches!(role.as_str(), Some("user" | "assistant")))
1267        })
1268    }) {
1269        return Err(Legacy2024WireError(
1270            "sampling content annotation audience must contain exact roles",
1271        ));
1272    }
1273    if !annotations.get("priority").is_none_or(|value| {
1274        value
1275            .as_f64()
1276            .is_some_and(|priority| (0.0..=1.0).contains(&priority))
1277    }) {
1278        return Err(Legacy2024WireError(
1279            "sampling content annotation priority must be a number from zero through one",
1280        ));
1281    }
1282    Ok(())
1283}
1284
1285fn validate_legacy_2024_model_preferences(value: &Value) -> Result<(), Legacy2024WireError> {
1286    let preferences = value.as_object().ok_or(Legacy2024WireError(
1287        "sampling/createMessage modelPreferences must be an object",
1288    ))?;
1289    for member in ["costPriority", "speedPriority", "intelligencePriority"] {
1290        if !preferences.get(member).is_none_or(|value| {
1291            value
1292                .as_f64()
1293                .is_some_and(|priority| (0.0..=1.0).contains(&priority))
1294        }) {
1295            return Err(Legacy2024WireError(
1296                "sampling/createMessage model preference priorities must be numbers from zero through one",
1297            ));
1298        }
1299    }
1300    if !preferences.get("hints").is_none_or(|value| {
1301        value.as_array().is_some_and(|hints| {
1302            hints.iter().all(|hint| {
1303                hint.as_object()
1304                    .is_some_and(|hint| hint.get("name").is_none_or(Value::is_string))
1305            })
1306        })
1307    }) {
1308        return Err(Legacy2024WireError(
1309            "sampling/createMessage model preference hints must be objects with optional string names",
1310        ));
1311    }
1312    Ok(())
1313}
1314
1315fn optional_params_object(params: Option<&Value>, method: &str) -> Result<(), Legacy2024WireError> {
1316    if params.is_none_or(Value::is_object) {
1317        Ok(())
1318    } else {
1319        Err(Legacy2024WireError(match method {
1320            PING => "ping params must be an object when present",
1321            _ => "exact MCP 2024-11-05 params must be an object when present",
1322        }))
1323    }
1324}
1325
1326/// Validates optional exact-2024 parameter objects with a pagination cursor.
1327fn validate_legacy_2024_cursor_params(
1328    params: Option<&Value>,
1329    method: &str,
1330) -> Result<(), Legacy2024WireError> {
1331    optional_params_object(params, method)?;
1332    let Some(params) = params else {
1333        return Ok(());
1334    };
1335    if params
1336        .as_object()
1337        .and_then(|params| params.get("cursor"))
1338        .is_none_or(Value::is_string)
1339    {
1340        Ok(())
1341    } else {
1342        Err(Legacy2024WireError(
1343            "exact MCP 2024-11-05 cursor must be a string when present",
1344        ))
1345    }
1346}
1347
1348fn validate_legacy_2024_metadata_params(
1349    params: Option<&Value>,
1350    permits_progress_token: bool,
1351) -> Result<(), Legacy2024WireError> {
1352    optional_params_object(params, "metadata")?;
1353    let Some(params) = params else {
1354        return Ok(());
1355    };
1356    let params = params.as_object().ok_or(Legacy2024WireError(
1357        "exact MCP 2024-11-05 params must be an object when present",
1358    ))?;
1359    let Some(meta) = params.get("_meta") else {
1360        return Ok(());
1361    };
1362    let meta = meta.as_object().ok_or(Legacy2024WireError(
1363        "exact MCP 2024-11-05 _meta must be an object",
1364    ))?;
1365    if !permits_progress_token || meta.get("progressToken").is_none_or(legacy_2024_request_id) {
1366        Ok(())
1367    } else {
1368        Err(Legacy2024WireError(
1369            "exact MCP 2024-11-05 progressToken must be a string or integer",
1370        ))
1371    }
1372}
1373
1374fn required_string(
1375    object: &serde_json::Map<String, Value>,
1376    member: &str,
1377    subject: &str,
1378) -> Result<(), Legacy2024WireError> {
1379    if object.get(member).is_some_and(Value::is_string) {
1380        Ok(())
1381    } else {
1382        Err(Legacy2024WireError(match (subject, member) {
1383            ("tools/call", "name") => "tools/call requires a string name",
1384            ("prompts/get", "name") => "prompts/get requires a string name",
1385            ("completion/complete argument", "name") => {
1386                "completion/complete argument requires a string name"
1387            }
1388            ("completion/complete argument", "value") => {
1389                "completion/complete argument requires a string value"
1390            }
1391            ("prompt reference", "name") => "prompt reference requires a string name",
1392            ("resource reference", "uri") => "resource reference requires a string uri",
1393            ("resources/read", "uri") => "resources/read requires a string uri",
1394            ("resources/subscribe", "uri") => "resources/subscribe requires a string uri",
1395            ("resources/unsubscribe", "uri") => "resources/unsubscribe requires a string uri",
1396            ("notifications/resources/updated", "uri") => {
1397                "notifications/resources/updated requires a string uri"
1398            }
1399            _ => "exact MCP 2024-11-05 method requires a string member",
1400        }))
1401    }
1402}
1403
1404fn optional_object(
1405    object: &serde_json::Map<String, Value>,
1406    member: &str,
1407    subject: &str,
1408) -> Result<(), Legacy2024WireError> {
1409    if object.get(member).is_none_or(Value::is_object) {
1410        Ok(())
1411    } else {
1412        Err(Legacy2024WireError(match subject {
1413            "tools/call" => "tools/call arguments must be an object",
1414            _ => "exact MCP 2024-11-05 optional member must be an object",
1415        }))
1416    }
1417}
1418
1419/// Decodes one exact MCP 2024-11-05 JSON-RPC envelope before any lifecycle or
1420/// dispatch work.  Top-level batches, modern method literals, invalid IDs, and
1421/// 2025-11-25 initialization are rejected at this pure raw-admission boundary.
1422pub fn decode_legacy_2024_11_05_envelope(
1423    value: Value,
1424) -> Result<Legacy2024Envelope, Legacy2024WireError> {
1425    decode_legacy_2024_11_05_envelope_classified(value).map_err(|error| match error {
1426        Legacy2024EnvelopeError::Envelope(error)
1427        | Legacy2024EnvelopeError::Method(error)
1428        | Legacy2024EnvelopeError::MethodParams(error) => error,
1429    })
1430}
1431
1432/// One exact-2024 admission failure, split by JSON-RPC error taxonomy.
1433///
1434/// Envelope-structure failures map to Invalid Request (-32600); a
1435/// structurally valid JSON-RPC envelope naming a method outside the exact
1436/// 2024-11-05 inventory maps to Method Not Found (-32601); a valid envelope
1437/// whose method-owned params content is malformed maps to Invalid Params
1438/// (-32602). Envelope admission runs first, so a doubly-invalid frame
1439/// reports its envelope failure.
1440#[derive(Debug)]
1441pub enum Legacy2024EnvelopeError {
1442    /// The JSON-RPC envelope itself is not an exact MCP 2024-11-05 frame.
1443    Envelope(Legacy2024WireError),
1444    /// The envelope is a valid JSON-RPC frame, but its method name is not
1445    /// part of exact MCP 2024-11-05 (JSON-RPC 2.0 Method Not Found).
1446    Method(Legacy2024WireError),
1447    /// The envelope is valid but the method's params content is malformed.
1448    MethodParams(Legacy2024WireError),
1449}
1450
1451/// Decodes one exact-2024 envelope, classifying failures by taxonomy.
1452pub fn decode_legacy_2024_11_05_envelope_classified(
1453    value: Value,
1454) -> Result<Legacy2024Envelope, Legacy2024EnvelopeError> {
1455    let object = value.as_object().ok_or(Legacy2024EnvelopeError::Envelope(
1456        Legacy2024WireError(
1457            "MCP 2024-11-05 requires one top-level JSON-RPC object; batch arrays are unsupported",
1458        ),
1459    ))?;
1460    if object.get("jsonrpc") != Some(&Value::String("2.0".to_owned())) {
1461        return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1462            "jsonrpc must be exactly 2.0",
1463        )));
1464    }
1465
1466    let has_method = object.contains_key("method");
1467    let has_result = object.contains_key("result");
1468    let has_error = object.contains_key("error");
1469    if has_method && (has_result || has_error) {
1470        return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1471            "JSON-RPC method is mutually exclusive with result and error",
1472        )));
1473    }
1474    if has_result && has_error {
1475        return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1476            "MCP 2024-11-05 response envelopes require exactly one of result or error",
1477        )));
1478    }
1479    if !has_method && object.contains_key("params") {
1480        return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1481            "JSON-RPC params is only permitted on request and notification envelopes",
1482        )));
1483    }
1484
1485    if let Some(method_value) = object.get("method") {
1486        let method_name = method_value
1487            .as_str()
1488            .ok_or(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1489                "JSON-RPC method must be a string",
1490            )))?;
1491        // An unrecognized method on an otherwise valid JSON-RPC frame is
1492        // JSON-RPC 2.0 Method Not Found (-32601), not Invalid Request: the
1493        // envelope structure is sound, the method simply is not available
1494        // in exact MCP 2024-11-05.
1495        let method =
1496            legacy_2024_11_05_method(method_name).ok_or(Legacy2024EnvelopeError::Method(
1497                Legacy2024WireError("method is not part of exact MCP 2024-11-05"),
1498            ))?;
1499        let params = object.get("params").cloned();
1500        if params.as_ref().is_some_and(|params| !params.is_object()) {
1501            return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1502                "JSON-RPC params must be an object when present",
1503            )));
1504        }
1505
1506        return match method.envelope {
1507            Legacy2024EnvelopeKind::Request => {
1508                let id = object
1509                    .get("id")
1510                    .cloned()
1511                    .ok_or(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1512                        "MCP 2024-11-05 request envelopes require a non-null string or integer id",
1513                    )))?;
1514                if !legacy_2024_request_id(&id) {
1515                    return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1516                        "MCP 2024-11-05 request envelopes require a non-null string or integer id",
1517                    )));
1518                }
1519                // Initialize params validation is the exact-era gate, so its
1520                // failures stay envelope-class (-32600); other methods'
1521                // params-content failures are Invalid Params.
1522                validate_legacy_2024_11_05_method_params(method.name, params.as_ref()).map_err(
1523                    if method.name == INITIALIZE {
1524                        Legacy2024EnvelopeError::Envelope
1525                    } else {
1526                        Legacy2024EnvelopeError::MethodParams
1527                    },
1528                )?;
1529                Ok(Legacy2024Envelope::Request { method, id, params })
1530            }
1531            Legacy2024EnvelopeKind::Notification => {
1532                if object.contains_key("id") {
1533                    return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1534                        "MCP 2024-11-05 notification envelopes must omit id",
1535                    )));
1536                }
1537                validate_legacy_2024_11_05_method_params(method.name, params.as_ref()).map_err(
1538                    if method.name == INITIALIZE {
1539                        Legacy2024EnvelopeError::Envelope
1540                    } else {
1541                        Legacy2024EnvelopeError::MethodParams
1542                    },
1543                )?;
1544                Ok(Legacy2024Envelope::Notification { method, params })
1545            }
1546        };
1547    }
1548
1549    let id = object
1550        .get("id")
1551        .cloned()
1552        .ok_or(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1553            "MCP 2024-11-05 response envelopes require a non-null string or integer id",
1554        )))?;
1555    if !legacy_2024_request_id(&id) {
1556        return Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1557            "MCP 2024-11-05 response envelopes require a non-null string or integer id",
1558        )));
1559    }
1560    match (object.get("result"), object.get("error")) {
1561        (Some(result), None)
1562            if result.is_object()
1563                && result.get("resultType").is_none()
1564                && !crate::result::has_final_only_metadata(result) =>
1565        {
1566            Ok(Legacy2024Envelope::Response {
1567                id,
1568                result: result.clone(),
1569            })
1570        }
1571        (Some(result), None) if result.is_object() => {
1572            Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1573                "final result members cannot be represented by exact MCP 2024-11-05",
1574            )))
1575        }
1576        (Some(_), None) => Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1577            "MCP 2024-11-05 response result must be an object",
1578        ))),
1579        (None, Some(error)) if valid_legacy_2024_error(error) => Ok(Legacy2024Envelope::Error {
1580            id,
1581            error: error.clone(),
1582        }),
1583        (None, Some(_)) => Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1584            "MCP 2024-11-05 error envelopes require integer code and string message",
1585        ))),
1586        _ => Err(Legacy2024EnvelopeError::Envelope(Legacy2024WireError(
1587            "MCP 2024-11-05 response envelopes require exactly one of result or error",
1588        ))),
1589    }
1590}
1591
1592fn legacy_2024_request_id(value: &Value) -> bool {
1593    value.is_string() || legacy_2024_json_integer(value)
1594}
1595
1596fn legacy_2024_json_integer(value: &Value) -> bool {
1597    value
1598        .as_number()
1599        .is_some_and(|number| JsonInteger::try_from_number(number.clone()).is_ok())
1600}
1601
1602fn valid_legacy_2024_error(value: &Value) -> bool {
1603    value.as_object().is_some_and(|error| {
1604        error.get("code").is_some_and(legacy_2024_json_integer)
1605            && error.get("message").is_some_and(Value::is_string)
1606    })
1607}
1608
1609fn validate_legacy_2024_initialize(
1610    method: &str,
1611    params: Option<&Value>,
1612) -> Result<(), Legacy2024WireError> {
1613    if method != INITIALIZE {
1614        return Ok(());
1615    }
1616    let params = params
1617        .and_then(Value::as_object)
1618        .ok_or(Legacy2024WireError(
1619            "MCP 2024-11-05 initialize requires object params",
1620        ))?;
1621    if !params.get("protocolVersion").is_some_and(Value::is_string) {
1622        return Err(Legacy2024WireError(
1623            "initialize protocolVersion must be a string",
1624        ));
1625    }
1626    let client_info =
1627        params
1628            .get("clientInfo")
1629            .and_then(Value::as_object)
1630            .ok_or(Legacy2024WireError(
1631                "MCP 2024-11-05 initialize requires clientInfo object",
1632            ))?;
1633    if !client_info.get("name").is_some_and(Value::is_string)
1634        || !client_info.get("version").is_some_and(Value::is_string)
1635    {
1636        return Err(Legacy2024WireError(
1637            "MCP 2024-11-05 initialize clientInfo requires string name and version",
1638        ));
1639    }
1640    let capabilities = params
1641        .get("capabilities")
1642        .cloned()
1643        .ok_or(Legacy2024WireError(
1644            "MCP 2024-11-05 initialize requires client capabilities",
1645        ))?;
1646    decode_legacy_2024_11_05_client_capabilities(capabilities)?;
1647    Ok(())
1648}
1649
1650#[cfg(test)]
1651mod tests {
1652    use super::*;
1653    use serde_json::json;
1654
1655    fn initialize_wire() -> Value {
1656        json!({
1657            "jsonrpc": "2.0",
1658            "id": 1,
1659            "method": "initialize",
1660            "params": {
1661                "protocolVersion": "2024-11-05",
1662                "capabilities": {"sampling": {}, "roots": {"listChanged": true}},
1663                "clientInfo": {"name": "exact-legacy-client", "version": "1.0.0"}
1664            }
1665        })
1666    }
1667
1668    #[test]
1669    fn lifecycle_and_tool_constants_match_mcp_spec() {
1670        // Lifecycle: the initialized notification is `notifications/initialized`,
1671        // NOT bare `initialized`. https://modelcontextprotocol.io/specification
1672        assert_eq!(INITIALIZE, "initialize");
1673        assert_eq!(NOTIFICATIONS_INITIALIZED, "notifications/initialized");
1674        assert_eq!(SERVER_DISCOVER, "server/discover");
1675
1676        assert_eq!(TOOLS_LIST, "tools/list");
1677        assert_eq!(TOOLS_CALL, "tools/call");
1678        assert_eq!(RESOURCES_LIST, "resources/list");
1679        assert_eq!(RESOURCES_TEMPLATES_LIST, "resources/templates/list");
1680        assert_eq!(RESOURCES_READ, "resources/read");
1681        assert_eq!(PROMPTS_LIST, "prompts/list");
1682        assert_eq!(PROMPTS_GET, "prompts/get");
1683        assert_eq!(LOGGING_SET_LEVEL, "logging/setLevel");
1684        assert_eq!(NOTIFICATIONS_CANCELLED, "notifications/cancelled");
1685        assert_eq!(NOTIFICATIONS_MESSAGE, "notifications/message");
1686        assert_eq!(PING, "ping");
1687    }
1688
1689    #[test]
1690    fn final_2026_method_inventory_positive() {
1691        let expected = [
1692            "server/discover",
1693            "completion/complete",
1694            "prompts/get",
1695            "prompts/list",
1696            "resources/list",
1697            "resources/templates/list",
1698            "resources/read",
1699            "subscriptions/listen",
1700            "tools/call",
1701            "tools/list",
1702            "notifications/cancelled",
1703            "notifications/progress",
1704            "notifications/message",
1705            "notifications/resources/updated",
1706            "notifications/resources/list_changed",
1707            "notifications/tools/list_changed",
1708            "notifications/prompts/list_changed",
1709            "notifications/subscriptions/acknowledged",
1710        ];
1711        let actual: Vec<_> = FINAL_2026_07_28_METHODS
1712            .iter()
1713            .map(|method| method.name)
1714            .collect();
1715
1716        assert_eq!(actual, expected);
1717        assert_eq!(actual.len(), 18);
1718        assert_eq!(
1719            final_2026_07_28_method(SUBSCRIPTIONS_LISTEN),
1720            Some(&Final2026Method {
1721                name: SUBSCRIPTIONS_LISTEN,
1722                direction: Final2026Direction::ClientToServer,
1723                envelope: Final2026EnvelopeKind::Request,
1724            })
1725        );
1726        assert_eq!(
1727            final_2026_07_28_method(NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED),
1728            Some(&Final2026Method {
1729                name: NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED,
1730                direction: Final2026Direction::ServerToClient,
1731                envelope: Final2026EnvelopeKind::Notification,
1732            })
1733        );
1734    }
1735
1736    #[test]
1737    fn final_2026_notification_direction_admission_is_exact() {
1738        let cancelled = final_2026_07_28_method(NOTIFICATIONS_CANCELLED)
1739            .expect("cancelled belongs to the final method table");
1740        assert!(cancelled.admits_notification_from(Final2026Peer::Client));
1741        assert!(cancelled.admits_notification_from(Final2026Peer::Server));
1742
1743        let acknowledged = final_2026_07_28_method(NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED)
1744            .expect("subscription acknowledgement belongs to the final method table");
1745        assert!(acknowledged.admits_notification_from(Final2026Peer::Server));
1746        assert!(
1747            !acknowledged.admits_notification_from(Final2026Peer::Client),
1748            "changing only the sender rejects the server-only notification"
1749        );
1750
1751        let discover = final_2026_07_28_method(SERVER_DISCOVER)
1752            .expect("server/discover belongs to the final method table");
1753        assert!(
1754            !discover.admits_notification_from(Final2026Peer::Client),
1755            "a client-to-server request never enters either notification union"
1756        );
1757    }
1758
1759    #[test]
1760    fn final_2026_method_inventory_cross_era_and_unknown_negatives() {
1761        assert!(
1762            final_2026_07_28_method(RESOURCES_SUBSCRIBE).is_none(),
1763            "changing only the method from final subscriptions/listen to the legacy subscription RPC must reject it"
1764        );
1765        assert!(
1766            final_2026_07_28_method("com.example/unknown").is_none(),
1767            "an unregistered method must not enter the closed final core table"
1768        );
1769        assert!(
1770            legacy_2024_11_05_method(SUBSCRIPTIONS_LISTEN).is_none(),
1771            "the modern subscription method must not bleed into the exact legacy table"
1772        );
1773    }
1774
1775    #[test]
1776    fn leg_01_schema_parity_positive() {
1777        let schema = legacy_2024_11_05_schema().unwrap();
1778        assert_eq!(
1779            LEGACY_2024_11_05_SCHEMA_SHA256,
1780            "61cea2392d4f284092d09bc84b9ac488c0d5618ac2b38a56942fc5b99fd960ce"
1781        );
1782        assert_eq!(schema["$schema"], "http://json-schema.org/draft-07/schema#");
1783        assert_eq!(
1784            schema["definitions"]["InitializeRequest"]["properties"]["method"]["const"],
1785            INITIALIZE
1786        );
1787        assert_eq!(
1788            schema["definitions"]["ClientCapabilities"]["properties"].get("elicitation"),
1789            None
1790        );
1791        assert!(LEGACY_2024_11_05_SCHEMA_JSON.as_bytes().starts_with(b"{\n"));
1792    }
1793
1794    #[test]
1795    fn leg_01_initialize_open_extensions_and_protocol_version_positive() {
1796        let mut wire = initialize_wire();
1797        wire["params"]["protocolVersion"] = json!("2025-11-25");
1798        wire["params"]["capabilities"]["elicitation"] = json!({"form": {}});
1799        assert!(matches!(
1800            decode_legacy_2024_11_05_envelope(wire).unwrap(),
1801            Legacy2024Envelope::Request { method, .. } if method.name == INITIALIZE
1802        ));
1803    }
1804
1805    #[test]
1806    fn leg_01_initialize_open_extensions_and_protocol_version_planted_negative() {
1807        let mut wire = initialize_wire();
1808        wire["params"]["protocolVersion"] = Value::Bool(true);
1809        wire["params"]["capabilities"]["elicitation"] = json!({"form": {}});
1810        assert_eq!(
1811            decode_legacy_2024_11_05_envelope(wire)
1812                .unwrap_err()
1813                .reason(),
1814            "initialize protocolVersion must be a string"
1815        );
1816    }
1817
1818    #[test]
1819    fn leg_01_method_inventory_positive() {
1820        let expected = [
1821            "initialize",
1822            "notifications/initialized",
1823            "ping",
1824            "tools/list",
1825            "tools/call",
1826            "resources/list",
1827            "resources/templates/list",
1828            "resources/read",
1829            "resources/subscribe",
1830            "resources/unsubscribe",
1831            "prompts/list",
1832            "prompts/get",
1833            "logging/setLevel",
1834            "completion/complete",
1835            "sampling/createMessage",
1836            "roots/list",
1837            "notifications/cancelled",
1838            "notifications/progress",
1839            "notifications/roots/list_changed",
1840            "notifications/message",
1841            "notifications/prompts/list_changed",
1842            "notifications/resources/list_changed",
1843            "notifications/resources/updated",
1844            "notifications/tools/list_changed",
1845        ];
1846        let actual: Vec<_> = LEGACY_2024_11_05_METHODS
1847            .iter()
1848            .map(|method| method.name)
1849            .collect();
1850        assert_eq!(actual, expected);
1851        assert_eq!(actual.len(), 24);
1852        assert_eq!(
1853            legacy_2024_11_05_method(SAMPLING_CREATE_MESSAGE)
1854                .unwrap()
1855                .capability,
1856            Some(Legacy2024Capability::ClientSampling)
1857        );
1858        assert_eq!(
1859            legacy_2024_11_05_method(NOTIFICATIONS_RESOURCES_UPDATED)
1860                .unwrap()
1861                .capability,
1862            Some(Legacy2024Capability::ServerResourcesSubscribe)
1863        );
1864    }
1865
1866    #[test]
1867    fn leg_01_method_inventory_planted_negative() {
1868        let mut wire = json!({"jsonrpc": "2.0", "id": 7, "method": "tools/list"});
1869        wire["method"] = json!("elicitation/create");
1870        assert_eq!(
1871            decode_legacy_2024_11_05_envelope(wire)
1872                .unwrap_err()
1873                .reason(),
1874            "method is not part of exact MCP 2024-11-05"
1875        );
1876    }
1877
1878    #[test]
1879    fn leg_01_server_to_client_ping_params_positive() {
1880        let ping: Value = serde_json::from_str(
1881            r#"{"jsonrpc":"2.0","id":"server-ping","method":"ping","params":{"_meta":{"progressToken":922337203685477580812345678901234567890}}}"#,
1882        )
1883        .expect("huge mathematical integer ping token is valid JSON");
1884
1885        assert!(matches!(
1886            decode_legacy_2024_11_05_envelope(ping).unwrap(),
1887            Legacy2024Envelope::Request { method, .. } if method.name == PING
1888        ));
1889    }
1890
1891    #[test]
1892    fn leg_01_server_to_client_ping_params_planted_negative() {
1893        let ping: Value = serde_json::from_str(
1894            r#"{"jsonrpc":"2.0","id":"server-ping","method":"ping","params":{"_meta":{"progressToken":922337203685477580812345678901234567890.5}}}"#,
1895        )
1896        .expect("fractional ping token is valid JSON");
1897
1898        assert_eq!(
1899            decode_legacy_2024_11_05_envelope(ping)
1900                .unwrap_err()
1901                .reason(),
1902            "exact MCP 2024-11-05 progressToken must be a string or integer"
1903        );
1904    }
1905
1906    #[test]
1907    fn leg_01_integer_token_params_positive() {
1908        let huge: Value = serde_json::from_str("922337203685477580812345678901234567890")
1909            .expect("huge mathematical integer token is valid JSON");
1910        let mut cancelled = json!({
1911            "jsonrpc": "2.0",
1912            "method": NOTIFICATIONS_CANCELLED,
1913            "params": {"requestId": 0}
1914        });
1915        cancelled["params"]["requestId"] = huge.clone();
1916        assert!(decode_legacy_2024_11_05_envelope(cancelled).is_ok());
1917
1918        let mut progress = json!({
1919            "jsonrpc": "2.0",
1920            "method": NOTIFICATIONS_PROGRESS,
1921            "params": {"progressToken": 0, "progress": 1}
1922        });
1923        progress["params"]["progressToken"] = huge;
1924        assert!(decode_legacy_2024_11_05_envelope(progress).is_ok());
1925    }
1926
1927    #[test]
1928    fn leg_01_integer_token_params_planted_negative() {
1929        let fractional: Value = serde_json::from_str("922337203685477580812345678901234567890.5")
1930            .expect("fractional token is valid JSON");
1931        let mut cancelled = json!({
1932            "jsonrpc": "2.0",
1933            "method": NOTIFICATIONS_CANCELLED,
1934            "params": {"requestId": 0}
1935        });
1936        cancelled["params"]["requestId"] = fractional.clone();
1937        assert_eq!(
1938            decode_legacy_2024_11_05_envelope(cancelled)
1939                .unwrap_err()
1940                .reason(),
1941            "notifications/cancelled requires a non-null string or integer requestId"
1942        );
1943
1944        let mut progress = json!({
1945            "jsonrpc": "2.0",
1946            "method": NOTIFICATIONS_PROGRESS,
1947            "params": {"progressToken": 0, "progress": 1}
1948        });
1949        progress["params"]["progressToken"] = fractional;
1950        assert_eq!(
1951            decode_legacy_2024_11_05_envelope(progress)
1952                .unwrap_err()
1953                .reason(),
1954            "notifications/progress requires exact token, progress, and optional total members"
1955        );
1956    }
1957
1958    #[test]
1959    fn leg_01_cursor_params_positive() {
1960        for method in [
1961            TOOLS_LIST,
1962            RESOURCES_LIST,
1963            RESOURCES_TEMPLATES_LIST,
1964            PROMPTS_LIST,
1965        ] {
1966            let request = json!({
1967                "jsonrpc": "2.0",
1968                "id": "cursor-request",
1969                "method": method,
1970                "params": {"cursor": "opaque-cursor"}
1971            });
1972            assert!(
1973                decode_legacy_2024_11_05_envelope(request).is_ok(),
1974                "{method}"
1975            );
1976        }
1977    }
1978
1979    #[test]
1980    fn leg_01_cursor_params_planted_negative() {
1981        for method in [
1982            TOOLS_LIST,
1983            RESOURCES_LIST,
1984            RESOURCES_TEMPLATES_LIST,
1985            PROMPTS_LIST,
1986        ] {
1987            let request = json!({
1988                "jsonrpc": "2.0",
1989                "id": "cursor-request",
1990                "method": method,
1991                "params": {"cursor": false}
1992            });
1993            assert_eq!(
1994                decode_legacy_2024_11_05_envelope(request)
1995                    .unwrap_err()
1996                    .reason(),
1997                "exact MCP 2024-11-05 cursor must be a string when present",
1998                "{method}"
1999            );
2000        }
2001    }
2002
2003    #[test]
2004    fn leg_01_metadata_params_positive() {
2005        for method in [
2006            NOTIFICATIONS_INITIALIZED,
2007            NOTIFICATIONS_ROOTS_LIST_CHANGED,
2008            NOTIFICATIONS_PROMPTS_LIST_CHANGED,
2009            NOTIFICATIONS_RESOURCES_LIST_CHANGED,
2010            NOTIFICATIONS_TOOLS_LIST_CHANGED,
2011        ] {
2012            let notification = json!({
2013                "jsonrpc": "2.0",
2014                "method": method,
2015                "params": {"_meta": {"vendor": true}}
2016            });
2017            assert!(
2018                decode_legacy_2024_11_05_envelope(notification).is_ok(),
2019                "{method}"
2020            );
2021        }
2022        let roots = json!({
2023            "jsonrpc": "2.0",
2024            "id": "roots-request",
2025            "method": ROOTS_LIST,
2026            "params": {"_meta": {"progressToken": "roots-progress"}}
2027        });
2028        assert!(decode_legacy_2024_11_05_envelope(roots).is_ok());
2029    }
2030
2031    #[test]
2032    fn leg_01_metadata_params_planted_negative() {
2033        let notification = json!({
2034            "jsonrpc": "2.0",
2035            "method": NOTIFICATIONS_INITIALIZED,
2036            "params": {"_meta": false}
2037        });
2038        assert_eq!(
2039            decode_legacy_2024_11_05_envelope(notification)
2040                .unwrap_err()
2041                .reason(),
2042            "exact MCP 2024-11-05 _meta must be an object"
2043        );
2044
2045        let roots = json!({
2046            "jsonrpc": "2.0",
2047            "id": "roots-request",
2048            "method": ROOTS_LIST,
2049            "params": {"_meta": {"progressToken": false}}
2050        });
2051        assert_eq!(
2052            decode_legacy_2024_11_05_envelope(roots)
2053                .unwrap_err()
2054                .reason(),
2055            "exact MCP 2024-11-05 progressToken must be a string or integer"
2056        );
2057    }
2058
2059    #[test]
2060    fn leg_01_legacy_application_metadata_and_progress_token_remain_admitted() {
2061        let request = json!({
2062            "jsonrpc": "2.0",
2063            "id": "legacy-ping",
2064            "method": PING,
2065            "params": {
2066                "_meta": {
2067                    "com.example/application": {"opaque": true},
2068                    "progressToken": "legacy-progress"
2069                }
2070            }
2071        });
2072
2073        assert!(matches!(
2074            decode_legacy_2024_11_05_envelope_classified(request),
2075            Ok(Legacy2024Envelope::Request { method, .. }) if method.name == PING
2076        ));
2077    }
2078
2079    #[test]
2080    fn leg_01_final_reserved_metadata_is_rejected_from_legacy_request_params() {
2081        let accepted = json!({
2082            "jsonrpc": "2.0",
2083            "id": "legacy-list",
2084            "method": TOOLS_LIST,
2085            "params": {"_meta": {"com.example/application": true}}
2086        });
2087        assert!(decode_legacy_2024_11_05_envelope_classified(accepted.clone()).is_ok());
2088
2089        for member in [
2090            "io.modelcontextprotocol/protocolVersion",
2091            "io.modelcontextprotocol/clientCapabilities",
2092            "io.modelcontextprotocol/clientInfo",
2093            "io.modelcontextprotocol/serverInfo",
2094            "io.modelcontextprotocol/subscriptionId",
2095        ] {
2096            let mut rejected = accepted.clone();
2097            rejected["params"]["_meta"][member] = json!({});
2098            assert!(matches!(
2099                decode_legacy_2024_11_05_envelope_classified(rejected),
2100                Err(Legacy2024EnvelopeError::MethodParams(error))
2101                    if error.reason()
2102                        == "final protocol metadata cannot be represented by exact MCP 2024-11-05"
2103            ));
2104        }
2105    }
2106
2107    #[test]
2108    fn leg_01_final_result_members_are_rejected_from_legacy_responses() {
2109        let accepted = json!({
2110            "jsonrpc": "2.0",
2111            "id": 19,
2112            "result": {
2113                "legacy": true,
2114                "_meta": {"com.example/application": true}
2115            }
2116        });
2117        assert!(decode_legacy_2024_11_05_envelope_classified(accepted.clone()).is_ok());
2118
2119        let mut result_type = accepted.clone();
2120        result_type["result"]["resultType"] = json!("complete");
2121        assert!(matches!(
2122            decode_legacy_2024_11_05_envelope_classified(result_type),
2123            Err(Legacy2024EnvelopeError::Envelope(error))
2124                if error.reason()
2125                    == "final result members cannot be represented by exact MCP 2024-11-05"
2126        ));
2127
2128        for member in [
2129            "io.modelcontextprotocol/protocolVersion",
2130            "io.modelcontextprotocol/clientCapabilities",
2131            "io.modelcontextprotocol/clientInfo",
2132            "io.modelcontextprotocol/serverInfo",
2133            "io.modelcontextprotocol/subscriptionId",
2134        ] {
2135            let mut rejected = accepted.clone();
2136            rejected["result"]["_meta"][member] = json!({});
2137            assert!(matches!(
2138                decode_legacy_2024_11_05_envelope_classified(rejected),
2139                Err(Legacy2024EnvelopeError::Envelope(error))
2140                    if error.reason()
2141                        == "final result members cannot be represented by exact MCP 2024-11-05"
2142            ));
2143        }
2144    }
2145
2146    #[test]
2147    fn leg_01_legacy_tool_results_preserve_application_metadata_only() {
2148        let accepted = json!({
2149            "content": [{"type": "text", "text": "legacy"}],
2150            "_meta": {"com.example/application": true}
2151        });
2152        assert_eq!(
2153            translate_legacy_2024_result(TOOLS_CALL, accepted.clone()),
2154            Ok(accepted.clone())
2155        );
2156
2157        let mut rejected = accepted;
2158        rejected["_meta"]["io.modelcontextprotocol/serverInfo"] = json!({});
2159        assert_eq!(
2160            translate_legacy_2024_result(TOOLS_CALL, rejected)
2161                .expect_err("final-only result metadata cannot cross into exact 2024"),
2162            Legacy2024WireError(
2163                "final protocol metadata cannot be represented by exact MCP 2024-11-05"
2164            )
2165        );
2166    }
2167
2168    #[test]
2169    fn leg_01_request_and_response_shapes_are_mutually_exclusive() {
2170        let request = json!({
2171            "jsonrpc": "2.0",
2172            "id": "request",
2173            "method": PING,
2174            "params": {}
2175        });
2176        assert!(matches!(
2177            decode_legacy_2024_11_05_envelope_classified(request),
2178            Ok(Legacy2024Envelope::Request { method, .. }) if method.name == PING
2179        ));
2180
2181        let response = json!({
2182            "jsonrpc": "2.0",
2183            "id": "response",
2184            "result": {"legacy": true}
2185        });
2186        assert!(matches!(
2187            decode_legacy_2024_11_05_envelope_classified(response),
2188            Ok(Legacy2024Envelope::Response { id, .. }) if id == json!("response")
2189        ));
2190
2191        for rejected in [
2192            json!({
2193                "jsonrpc": "2.0",
2194                "id": "mixed-null-method",
2195                "method": null,
2196                "result": {
2197                    "legacy": true,
2198                    "_meta": {
2199                        "io.modelcontextprotocol/protocolVersion": "2026-07-28"
2200                    }
2201                }
2202            }),
2203            json!({
2204                "jsonrpc": "2.0",
2205                "id": "mixed-ping",
2206                "method": PING,
2207                "result": {"legacy": true}
2208            }),
2209        ] {
2210            assert!(matches!(
2211                decode_legacy_2024_11_05_envelope_classified(rejected),
2212                Err(Legacy2024EnvelopeError::Envelope(error))
2213                    if error.reason()
2214                        == "JSON-RPC method is mutually exclusive with result and error"
2215            ));
2216        }
2217
2218        let result_and_error = json!({
2219            "jsonrpc": "2.0",
2220            "id": "mixed-response",
2221            "result": {},
2222            "error": {"code": -32603, "message": "failed"}
2223        });
2224        assert!(matches!(
2225            decode_legacy_2024_11_05_envelope_classified(result_and_error),
2226            Err(Legacy2024EnvelopeError::Envelope(error))
2227                if error.reason()
2228                    == "MCP 2024-11-05 response envelopes require exactly one of result or error"
2229        ));
2230
2231        let response_with_params = json!({
2232            "jsonrpc": "2.0",
2233            "id": "response-with-params",
2234            "result": {"legacy": true},
2235            "params": {}
2236        });
2237        assert!(matches!(
2238            decode_legacy_2024_11_05_envelope_classified(response_with_params),
2239            Err(Legacy2024EnvelopeError::Envelope(error))
2240                if error.reason()
2241                    == "JSON-RPC params is only permitted on request and notification envelopes"
2242        ));
2243    }
2244
2245    #[test]
2246    fn leg_01_sampling_max_tokens_arbitrary_width_positive() {
2247        let request: Value = serde_json::from_str(
2248            r#"{"jsonrpc":"2.0","id":"sampling-request","method":"sampling/createMessage","params":{"messages":[],"maxTokens":922337203685477580812345678901234567890}}"#,
2249        )
2250        .expect("huge mathematical integer maxTokens is valid JSON");
2251        assert!(decode_legacy_2024_11_05_envelope(request).is_ok());
2252    }
2253
2254    #[test]
2255    fn leg_01_sampling_max_tokens_arbitrary_width_planted_negative() {
2256        let request: Value = serde_json::from_str(
2257            r#"{"jsonrpc":"2.0","id":"sampling-request","method":"sampling/createMessage","params":{"messages":[],"maxTokens":922337203685477580812345678901234567890.5}}"#,
2258        )
2259        .expect("fractional maxTokens is valid JSON");
2260        assert_eq!(
2261            decode_legacy_2024_11_05_envelope(request)
2262                .unwrap_err()
2263                .reason(),
2264            "sampling/createMessage requires integer maxTokens"
2265        );
2266    }
2267
2268    #[test]
2269    fn leg_01_envelopes_positive() {
2270        assert!(matches!(
2271            decode_legacy_2024_11_05_envelope(initialize_wire()).unwrap(),
2272            Legacy2024Envelope::Request { method, id, .. } if method.name == INITIALIZE && id == json!(1)
2273        ));
2274        assert!(matches!(
2275            decode_legacy_2024_11_05_envelope(json!({"jsonrpc":"2.0", "id":"reply", "result": {}})).unwrap(),
2276            Legacy2024Envelope::Response { id, result } if id == json!("reply") && result == json!({})
2277        ));
2278        assert!(matches!(
2279            decode_legacy_2024_11_05_envelope(json!({"jsonrpc":"2.0", "method":"notifications/initialized"})).unwrap(),
2280            Legacy2024Envelope::Notification { method, .. } if method.name == NOTIFICATIONS_INITIALIZED
2281        ));
2282    }
2283
2284    #[test]
2285    fn leg_01_error_codes_preserve_integral_exponent_and_huge_lexemes() {
2286        for raw_code in ["-3.2603e4", "340282366920938463463374607431768211457"] {
2287            let wire: Value = serde_json::from_str(&format!(
2288                r#"{{"jsonrpc":"2.0","id":1,"error":{{"code":{raw_code},"message":"failed"}}}}"#
2289            ))
2290            .expect("integral error-code wire must parse");
2291            // serde_json canonicalizes exponent spellings while parsing the
2292            // wire into a Value ("-3.2603e4" arrives as "-3.2603e+4"); the
2293            // envelope decoder's obligation is to admit the parsed code
2294            // without further lexeme or precision loss.
2295            let parsed_code = wire["error"]["code"]
2296                .as_number()
2297                .expect("error code parses as a JSON number")
2298                .as_str()
2299                .to_owned();
2300
2301            let Legacy2024Envelope::Error { error, .. } = decode_legacy_2024_11_05_envelope(wire)
2302                .expect("exact inbound error code must be admitted")
2303            else {
2304                panic!("error wire must decode as an error envelope");
2305            };
2306            assert_eq!(
2307                error["code"]
2308                    .as_number()
2309                    .expect("error code remains a JSON number")
2310                    .as_str(),
2311                parsed_code
2312            );
2313        }
2314    }
2315
2316    #[test]
2317    fn leg_01_error_codes_reject_fractional_near_miss() {
2318        let wire: Value = serde_json::from_str(
2319            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32603.5,"message":"failed"}}"#,
2320        )
2321        .expect("fractional error-code wire must parse");
2322
2323        assert_eq!(
2324            decode_legacy_2024_11_05_envelope(wire)
2325                .expect_err("fractional error code must not enter the exact envelope")
2326                .reason(),
2327            "MCP 2024-11-05 error envelopes require integer code and string message"
2328        );
2329    }
2330
2331    #[test]
2332    fn leg_01_top_level_batch_array_planted_negative() {
2333        let single = Value::Array(vec![initialize_wire()]);
2334        let mixed = Value::Array(vec![
2335            initialize_wire(),
2336            json!({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}),
2337        ]);
2338        let expected =
2339            "MCP 2024-11-05 requires one top-level JSON-RPC object; batch arrays are unsupported";
2340        assert_eq!(
2341            decode_legacy_2024_11_05_envelope(single)
2342                .unwrap_err()
2343                .reason(),
2344            expected
2345        );
2346        assert_eq!(
2347            decode_legacy_2024_11_05_envelope(mixed)
2348                .unwrap_err()
2349                .reason(),
2350            expected
2351        );
2352    }
2353
2354    #[test]
2355    fn leg_01_client_capability_members_positive() {
2356        for member in ["experimental", "sampling", "roots"] {
2357            let mut wire = initialize_wire();
2358            wire["params"]["capabilities"][member] = json!({});
2359            assert!(decode_legacy_2024_11_05_envelope(wire).is_ok(), "{member}");
2360        }
2361    }
2362
2363    #[test]
2364    fn leg_01_client_capability_members_planted_negative() {
2365        for member in ["experimental", "sampling", "roots"] {
2366            let mut wire = initialize_wire();
2367            wire["params"]["capabilities"][member] = Value::Null;
2368            assert_eq!(
2369                decode_legacy_2024_11_05_envelope(wire)
2370                    .unwrap_err()
2371                    .reason(),
2372                "MCP 2024-11-05 client capability members must be objects when present",
2373                "{member}"
2374            );
2375        }
2376    }
2377
2378    #[test]
2379    fn leg_01_a_positive() {
2380        let server_capabilities = validate_legacy_2024_11_05_initialize_result(&json!({
2381            "protocolVersion": "2024-11-05",
2382            "serverInfo": {"name": "exact-legacy-server", "version": "1.0.0"},
2383            "capabilities": {
2384                "logging": {}, "tools": {"listChanged": true},
2385                "resources": {"subscribe": true, "listChanged": true},
2386                "prompts": {"listChanged": true}
2387            }
2388        }))
2389        .unwrap();
2390        assert!(server_capabilities.resources.unwrap().subscribe);
2391    }
2392
2393    #[test]
2394    fn leg_01_a_planted_negative() {
2395        let mut wire = initialize_wire();
2396        wire["id"] = Value::Null;
2397        assert_eq!(
2398            decode_legacy_2024_11_05_envelope(wire)
2399                .unwrap_err()
2400                .reason(),
2401            "MCP 2024-11-05 request envelopes require a non-null string or integer id"
2402        );
2403    }
2404}