1use std::collections::BTreeMap;
9use std::sync::OnceLock;
10
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13
14use crate::common_types::JsonInteger;
15
16pub const LEGACY_2024_11_05_PROTOCOL_VERSION: &str = "2024-11-05";
18
19pub const SERVER_DISCOVER: &str = crate::server_discovery::SERVER_DISCOVER_METHOD;
24
25pub const SUBSCRIPTIONS_LISTEN: &str = "subscriptions/listen";
27
28pub const NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED: &str =
30 "notifications/subscriptions/acknowledged";
31
32pub const LEGACY_2024_11_05_SCHEMA_SHA256: &str =
34 "61cea2392d4f284092d09bc84b9ac488c0d5618ac2b38a56942fc5b99fd960ce";
35
36pub const LEGACY_2024_11_05_SCHEMA_JSON: &str =
40 include_str!("../schema/mcp-schema-2024-11-05-48234828.json");
41
42pub 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
58pub const INITIALIZE: &str = "initialize";
60
61pub const NOTIFICATIONS_INITIALIZED: &str = "notifications/initialized";
63
64pub const TOOLS_LIST: &str = "tools/list";
66
67pub const TOOLS_CALL: &str = "tools/call";
69
70pub const RESOURCES_LIST: &str = "resources/list";
72
73pub const RESOURCES_TEMPLATES_LIST: &str = "resources/templates/list";
75
76pub const RESOURCES_READ: &str = "resources/read";
78
79pub const PROMPTS_LIST: &str = "prompts/list";
81
82pub const PROMPTS_GET: &str = "prompts/get";
84
85pub const LOGGING_SET_LEVEL: &str = "logging/setLevel";
87
88pub const NOTIFICATIONS_CANCELLED: &str = "notifications/cancelled";
90
91pub const NOTIFICATIONS_MESSAGE: &str = "notifications/message";
93
94pub const PING: &str = "ping";
96
97pub const COMPLETION_COMPLETE: &str = "completion/complete";
99
100pub const SAMPLING_CREATE_MESSAGE: &str = "sampling/createMessage";
102
103pub const ROOTS_LIST: &str = "roots/list";
105
106pub const NOTIFICATIONS_PROGRESS: &str = "notifications/progress";
108
109pub const NOTIFICATIONS_PROMPTS_LIST_CHANGED: &str = "notifications/prompts/list_changed";
111
112pub const NOTIFICATIONS_RESOURCES_LIST_CHANGED: &str = "notifications/resources/list_changed";
114
115pub const NOTIFICATIONS_RESOURCES_UPDATED: &str = "notifications/resources/updated";
117
118pub const NOTIFICATIONS_ROOTS_LIST_CHANGED: &str = "notifications/roots/list_changed";
120
121pub const NOTIFICATIONS_TOOLS_LIST_CHANGED: &str = "notifications/tools/list_changed";
123
124pub const RESOURCES_SUBSCRIBE: &str = "resources/subscribe";
126
127pub const RESOURCES_UNSUBSCRIBE: &str = "resources/unsubscribe";
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum Final2026Direction {
133 ClientToServer,
135 ServerToClient,
137 Bidirectional,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
143pub enum Final2026Peer {
144 Client,
146 Server,
148}
149
150impl Final2026Direction {
151 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum Final2026EnvelopeKind {
166 Request,
168 Notification,
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub struct Final2026Method {
176 pub name: &'static str,
178 pub direction: Final2026Direction,
180 pub envelope: Final2026EnvelopeKind,
182}
183
184impl Final2026Method {
185 #[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
193pub 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
303pub enum Legacy2024Direction {
304 ClientToServer,
306 ServerToClient,
308 Bidirectional,
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum Legacy2024EnvelopeKind {
315 Request,
317 Notification,
319}
320
321#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum Legacy2024Capability {
324 ClientSampling,
326 ClientRoots,
328 ClientRootsListChanged,
330 ServerLogging,
332 ServerPrompts,
334 ServerPromptsListChanged,
336 ServerResources,
338 ServerResourcesSubscribe,
340 ServerResourcesListChanged,
342 ServerTools,
344 ServerToolsListChanged,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350pub struct Legacy2024Method {
351 pub name: &'static str,
353 pub direction: Legacy2024Direction,
355 pub envelope: Legacy2024EnvelopeKind,
357 pub capability: Option<Legacy2024Capability>,
359}
360
361pub 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#[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#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
519pub struct Legacy2024ClientCapabilities {
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub experimental: Option<BTreeMap<String, Value>>,
523 #[serde(skip_serializing_if = "Option::is_none")]
525 pub sampling: Option<BTreeMap<String, Value>>,
526 #[serde(skip_serializing_if = "Option::is_none")]
528 pub roots: Option<Legacy2024RootsCapability>,
529 #[serde(flatten)]
531 pub extensions: BTreeMap<String, Value>,
532}
533
534#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
536pub struct Legacy2024RootsCapability {
537 #[serde(
539 default,
540 rename = "listChanged",
541 skip_serializing_if = "std::ops::Not::not"
542 )]
543 pub list_changed: bool,
544 #[serde(flatten)]
546 pub extensions: BTreeMap<String, Value>,
547}
548
549#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
551pub struct Legacy2024ServerCapabilities {
552 #[serde(skip_serializing_if = "Option::is_none")]
554 pub experimental: Option<BTreeMap<String, Value>>,
555 #[serde(skip_serializing_if = "Option::is_none")]
557 pub logging: Option<BTreeMap<String, Value>>,
558 #[serde(skip_serializing_if = "Option::is_none")]
560 pub prompts: Option<Legacy2024ListChangedCapability>,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub resources: Option<Legacy2024ResourcesCapability>,
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub tools: Option<Legacy2024ListChangedCapability>,
567 #[serde(flatten)]
569 pub extensions: BTreeMap<String, Value>,
570}
571
572#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
574pub struct Legacy2024ListChangedCapability {
575 #[serde(
577 default,
578 rename = "listChanged",
579 skip_serializing_if = "std::ops::Not::not"
580 )]
581 pub list_changed: bool,
582 #[serde(flatten)]
584 pub extensions: BTreeMap<String, Value>,
585}
586
587#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
589pub struct Legacy2024ResourcesCapability {
590 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
592 pub subscribe: bool,
593 #[serde(
595 default,
596 rename = "listChanged",
597 skip_serializing_if = "std::ops::Not::not"
598 )]
599 pub list_changed: bool,
600 #[serde(flatten)]
602 pub extensions: BTreeMap<String, Value>,
603}
604
605pub 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
616pub 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
626pub 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#[derive(Debug, Clone, PartialEq)]
699pub enum Legacy2024Envelope {
700 Request {
702 method: &'static Legacy2024Method,
703 id: Value,
704 params: Option<Value>,
705 },
706 Notification {
708 method: &'static Legacy2024Method,
709 params: Option<Value>,
710 },
711 Response { id: Value, result: Value },
713 Error { id: Value, error: Value },
715}
716
717#[derive(Debug, Clone, PartialEq, Eq)]
719pub struct Legacy2024WireError(&'static str);
720
721impl Legacy2024WireError {
722 #[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
744pub enum Legacy2024ResultKind {
745 Tool,
747 Resource,
749 Prompt,
751}
752
753#[derive(Debug, Clone, Copy, PartialEq, Eq)]
755pub enum Legacy2024ResultDisposition {
756 Lossless(Legacy2024ResultKind),
758 LegacyOwned,
760}
761
762pub 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
798pub 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
962pub 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
1132fn 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
1326fn 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
1419pub 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#[derive(Debug)]
1441pub enum Legacy2024EnvelopeError {
1442 Envelope(Legacy2024WireError),
1444 Method(Legacy2024WireError),
1447 MethodParams(Legacy2024WireError),
1449}
1450
1451pub 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 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 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 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 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}