1use serde_json::{Map, Value, json};
9
10use crate::ClientCapabilities;
11
12pub const FINAL_PROTOCOL_VERSION: &str = "2026-07-28";
14
15pub const SUPPORTED_FINAL_PROTOCOL_VERSIONS: &[&str] = &[FINAL_PROTOCOL_VERSION];
17
18pub const MCP_PROTOCOL_VERSION_HEADER: &str = "MCP-Protocol-Version";
20
21pub const MCP_METHOD_HEADER: &str = "Mcp-Method";
23
24pub const MCP_NAME_HEADER: &str = "Mcp-Name";
26
27pub const HEADER_MISMATCH_ERROR_CODE: i32 = -32020;
29
30pub const MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE: i32 = -32021;
32
33pub const UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE: i32 = -32022;
35
36pub const MAX_REQUIRED_CAPABILITIES_ERROR_DATA_BYTES: usize = 64 * 1024;
38
39#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41pub struct FinalProtocolVersion;
42
43impl FinalProtocolVersion {
44 #[must_use]
46 pub const fn as_str(self) -> &'static str {
47 FINAL_PROTOCOL_VERSION
48 }
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct RequestVersionMetadata<'a> {
54 pub header_version: Option<&'a str>,
56 pub body_version: Option<&'a str>,
58}
59
60#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub struct FinalRequestAdmission {
63 version: FinalProtocolVersion,
64}
65
66impl FinalRequestAdmission {
67 #[must_use]
69 pub const fn protocol_version(self) -> FinalProtocolVersion {
70 self.version
71 }
72}
73
74#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct FinalHttpRequestMetadata<'a> {
77 pub version: RequestVersionMetadata<'a>,
79 pub header_method: Option<&'a str>,
81 pub body_method: Option<&'a str>,
83 pub header_name: Option<&'a str>,
85 pub body_name: Option<&'a str>,
87}
88
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum HeaderMismatchReason {
92 MissingHeader,
94 MissingBodyVersion,
96 EmptyHeader,
98 EmptyBodyVersion,
100 HeaderBodyVersionMismatch,
102 MissingMethodHeader,
104 MissingBodyMethod,
106 EmptyMethodHeader,
108 EmptyBodyMethod,
110 HeaderBodyMethodMismatch,
112 MissingNameHeader,
114 MissingBodyName,
116 EmptyNameHeader,
118 EmptyBodyName,
120 HeaderBodyNameMismatch,
122}
123
124#[derive(Clone, Copy, Debug, Eq, PartialEq)]
129pub struct HeaderMismatchError {
130 reason: HeaderMismatchReason,
131}
132
133impl HeaderMismatchError {
134 #[must_use]
136 pub const fn reason(self) -> HeaderMismatchReason {
137 self.reason
138 }
139
140 #[must_use]
142 pub const fn jsonrpc_error_code(self) -> i32 {
143 HEADER_MISMATCH_ERROR_CODE
144 }
145
146 #[must_use]
148 pub const fn http_status(self) -> u16 {
149 400
150 }
151
152 #[must_use]
154 pub fn canonical_error_data(self) -> Option<Value> {
155 None
156 }
157}
158
159#[derive(Clone, Debug, Eq, PartialEq)]
161pub struct UnsupportedProtocolVersionError {
162 requested: String,
163}
164
165impl UnsupportedProtocolVersionError {
166 #[must_use]
168 pub fn requested(&self) -> &str {
169 &self.requested
170 }
171
172 #[must_use]
174 pub const fn supported_versions(&self) -> &'static [&'static str] {
175 SUPPORTED_FINAL_PROTOCOL_VERSIONS
176 }
177
178 #[must_use]
180 pub const fn jsonrpc_error_code(&self) -> i32 {
181 UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
182 }
183
184 #[must_use]
186 pub const fn http_status(&self) -> u16 {
187 400
188 }
189
190 #[must_use]
192 pub fn canonical_error_data(&self) -> Value {
193 json!({
194 "supported": self.supported_versions(),
195 "requested": self.requested(),
196 })
197 }
198}
199
200#[derive(Clone, Copy, Debug, Eq, PartialEq)]
202pub enum RequiredCapabilitiesError {
203 NotAnObject,
205 TooLarge,
207 Encoding,
209}
210
211#[derive(Clone, Debug, PartialEq)]
213pub struct MissingRequiredClientCapabilityError {
214 required_capabilities: Map<String, Value>,
215}
216
217impl MissingRequiredClientCapabilityError {
218 pub fn from_client_capabilities(
224 required_capabilities: &ClientCapabilities,
225 ) -> Result<Self, RequiredCapabilitiesError> {
226 let required_capabilities = serde_json::to_value(required_capabilities)
227 .map_err(|_| RequiredCapabilitiesError::Encoding)?;
228 Self::new(required_capabilities)
229 }
230
231 pub fn new(required_capabilities: Value) -> Result<Self, RequiredCapabilitiesError> {
236 let Value::Object(required_capabilities) = required_capabilities else {
237 return Err(RequiredCapabilitiesError::NotAnObject);
238 };
239 let encoded_len = serde_json::to_vec(&required_capabilities)
240 .map_err(|_| RequiredCapabilitiesError::Encoding)?
241 .len();
242 if encoded_len > MAX_REQUIRED_CAPABILITIES_ERROR_DATA_BYTES {
243 return Err(RequiredCapabilitiesError::TooLarge);
244 }
245 Ok(Self {
246 required_capabilities,
247 })
248 }
249
250 #[must_use]
252 pub const fn required_capabilities(&self) -> &Map<String, Value> {
253 &self.required_capabilities
254 }
255
256 #[must_use]
258 pub const fn jsonrpc_error_code(&self) -> i32 {
259 MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE
260 }
261
262 #[must_use]
264 pub const fn http_status(&self) -> u16 {
265 400
266 }
267
268 #[must_use]
270 pub fn canonical_error_data(&self) -> Value {
271 json!({"requiredCapabilities": self.required_capabilities})
272 }
273}
274
275#[derive(Clone, Debug, Eq, PartialEq)]
277pub enum RequestAdmissionError {
278 HeaderMismatch(HeaderMismatchError),
280 UnsupportedProtocolVersion(UnsupportedProtocolVersionError),
282}
283
284impl RequestAdmissionError {
285 #[must_use]
287 pub const fn http_status(&self) -> u16 {
288 match self {
289 Self::HeaderMismatch(error) => error.http_status(),
290 Self::UnsupportedProtocolVersion(error) => error.http_status(),
291 }
292 }
293
294 #[must_use]
296 pub const fn jsonrpc_error_code(&self) -> i32 {
297 match self {
298 Self::HeaderMismatch(error) => error.jsonrpc_error_code(),
299 Self::UnsupportedProtocolVersion(error) => error.jsonrpc_error_code(),
300 }
301 }
302}
303
304#[derive(Clone, Debug, Eq, PartialEq)]
306pub enum ProtocolVersionError {
307 HeaderMismatch,
309 UnsupportedProtocolVersion { requested: String },
311}
312
313impl ProtocolVersionError {
314 #[must_use]
316 pub const fn http_status(&self) -> u16 {
317 400
318 }
319
320 #[must_use]
322 pub const fn jsonrpc_error_code(&self) -> i32 {
323 match self {
324 Self::HeaderMismatch => HEADER_MISMATCH_ERROR_CODE,
325 Self::UnsupportedProtocolVersion { .. } => UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE,
326 }
327 }
328}
329
330pub fn validate_final_protocol_version(
338 header_version: Option<&str>,
339 body_version: Option<&str>,
340) -> Result<FinalProtocolVersion, ProtocolVersionError> {
341 admit_final_request(RequestVersionMetadata {
342 header_version,
343 body_version,
344 })
345 .map(|admission| admission.protocol_version())
346 .map_err(|error| match error {
347 RequestAdmissionError::HeaderMismatch(_) => ProtocolVersionError::HeaderMismatch,
348 RequestAdmissionError::UnsupportedProtocolVersion(error) => {
349 ProtocolVersionError::UnsupportedProtocolVersion {
350 requested: error.requested,
351 }
352 }
353 })
354}
355
356pub fn admit_final_request(
362 metadata: RequestVersionMetadata<'_>,
363) -> Result<FinalRequestAdmission, RequestAdmissionError> {
364 let header_version = metadata
365 .header_version
366 .ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
367 reason: HeaderMismatchReason::MissingHeader,
368 }))?;
369 let body_version = metadata
370 .body_version
371 .ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
372 reason: HeaderMismatchReason::MissingBodyVersion,
373 }))?;
374
375 if header_version.is_empty() {
376 return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
377 reason: HeaderMismatchReason::EmptyHeader,
378 }));
379 }
380 if body_version.is_empty() {
381 return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
382 reason: HeaderMismatchReason::EmptyBodyVersion,
383 }));
384 }
385 if header_version != body_version {
386 return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
387 reason: HeaderMismatchReason::HeaderBodyVersionMismatch,
388 }));
389 }
390 if header_version != FINAL_PROTOCOL_VERSION {
391 return Err(RequestAdmissionError::UnsupportedProtocolVersion(
392 UnsupportedProtocolVersionError {
393 requested: header_version.to_owned(),
394 },
395 ));
396 }
397
398 Ok(FinalRequestAdmission {
399 version: FinalProtocolVersion,
400 })
401}
402
403pub fn admit_final_http_request(
411 metadata: FinalHttpRequestMetadata<'_>,
412) -> Result<FinalRequestAdmission, RequestAdmissionError> {
413 let admission = admit_final_request(metadata.version)?;
414 let method = exact_nonempty_mirror(
415 metadata.header_method,
416 metadata.body_method,
417 HeaderMismatchReason::MissingMethodHeader,
418 HeaderMismatchReason::MissingBodyMethod,
419 HeaderMismatchReason::EmptyMethodHeader,
420 HeaderMismatchReason::EmptyBodyMethod,
421 HeaderMismatchReason::HeaderBodyMethodMismatch,
422 )?;
423 if requires_mcp_name(method) {
424 let _ = exact_nonempty_mirror(
425 metadata.header_name,
426 metadata.body_name,
427 HeaderMismatchReason::MissingNameHeader,
428 HeaderMismatchReason::MissingBodyName,
429 HeaderMismatchReason::EmptyNameHeader,
430 HeaderMismatchReason::EmptyBodyName,
431 HeaderMismatchReason::HeaderBodyNameMismatch,
432 )?;
433 }
434 Ok(admission)
435}
436
437fn exact_nonempty_mirror<'a>(
438 header: Option<&'a str>,
439 body: Option<&'a str>,
440 missing_header: HeaderMismatchReason,
441 missing_body: HeaderMismatchReason,
442 empty_header: HeaderMismatchReason,
443 empty_body: HeaderMismatchReason,
444 mismatch: HeaderMismatchReason,
445) -> Result<&'a str, RequestAdmissionError> {
446 let header = header.ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
447 reason: missing_header,
448 }))?;
449 let body = body.ok_or(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
450 reason: missing_body,
451 }))?;
452 if header.is_empty() {
453 return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
454 reason: empty_header,
455 }));
456 }
457 if body.is_empty() {
458 return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
459 reason: empty_body,
460 }));
461 }
462 if header != body {
463 return Err(RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
464 reason: mismatch,
465 }));
466 }
467 Ok(header)
468}
469
470fn requires_mcp_name(method: &str) -> bool {
471 matches!(
472 method,
473 "tools/call"
474 | "resources/read"
475 | "prompts/get"
476 | "tasks/get"
477 | "tasks/update"
478 | "tasks/cancel"
479 )
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485
486 #[test]
487 fn prt_03_a_positive() {
488 let admission = admit_final_http_request(FinalHttpRequestMetadata {
489 version: RequestVersionMetadata {
490 header_version: Some(FINAL_PROTOCOL_VERSION),
491 body_version: Some(FINAL_PROTOCOL_VERSION),
492 },
493 header_method: Some("tools/call"),
494 body_method: Some("tools/call"),
495 header_name: Some("weather"),
496 body_name: Some("weather"),
497 })
498 .expect("matching final standard headers and body values must be admitted");
499
500 assert_eq!(
501 admission.protocol_version().as_str(),
502 FINAL_PROTOCOL_VERSION
503 );
504 assert_eq!(MCP_PROTOCOL_VERSION_HEADER, "MCP-Protocol-Version");
505 assert_eq!(MCP_METHOD_HEADER, "Mcp-Method");
506 assert_eq!(MCP_NAME_HEADER, "Mcp-Name");
507 }
508
509 #[test]
510 fn prt_03_a_planted_negative() {
511 let body_name = Some("weather");
512 let error = admit_final_http_request(FinalHttpRequestMetadata {
513 version: RequestVersionMetadata {
514 header_version: Some(FINAL_PROTOCOL_VERSION),
515 body_version: Some(FINAL_PROTOCOL_VERSION),
516 },
517 header_method: Some("tools/call"),
518 body_method: Some("tools/call"),
519 header_name: Some("other-weather"),
520 body_name,
521 })
522 .expect_err("changing only the name header must reject the request");
523
524 assert_eq!(
525 error,
526 RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
527 reason: HeaderMismatchReason::HeaderBodyNameMismatch,
528 })
529 );
530 assert_eq!(error.http_status(), 400);
531 assert_eq!(error.jsonrpc_error_code(), HEADER_MISMATCH_ERROR_CODE);
532 assert_eq!(body_name, Some("weather"));
533 }
534
535 #[test]
536 fn official_tasks_methods_require_the_same_mcp_name_mirror() {
537 for method in ["tasks/get", "tasks/update", "tasks/cancel"] {
538 let admitted = admit_final_http_request(FinalHttpRequestMetadata {
539 version: RequestVersionMetadata {
540 header_version: Some(FINAL_PROTOCOL_VERSION),
541 body_version: Some(FINAL_PROTOCOL_VERSION),
542 },
543 header_method: Some(method),
544 body_method: Some(method),
545 header_name: Some("task-42"),
546 body_name: Some("task-42"),
547 });
548 assert!(
549 admitted.is_ok(),
550 "{method} accepts an exact task identifier mirror"
551 );
552 }
553
554 let body_name = Some("task-42");
555 let error = admit_final_http_request(FinalHttpRequestMetadata {
556 version: RequestVersionMetadata {
557 header_version: Some(FINAL_PROTOCOL_VERSION),
558 body_version: Some(FINAL_PROTOCOL_VERSION),
559 },
560 header_method: Some("tasks/get"),
561 body_method: Some("tasks/get"),
562 header_name: Some("other-task"),
563 body_name,
564 })
565 .expect_err("changing only a Tasks name mirror rejects final admission");
566 assert_eq!(
567 error,
568 RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
569 reason: HeaderMismatchReason::HeaderBodyNameMismatch,
570 })
571 );
572 assert_eq!(body_name, Some("task-42"));
573 }
574
575 #[test]
576 fn matching_unsupported_version_reports_the_requested_value() {
577 let error = validate_final_protocol_version(Some("2025-11-25"), Some("2025-11-25"))
578 .expect_err("matching unsupported versions must not be accepted");
579
580 assert_eq!(
581 error,
582 ProtocolVersionError::UnsupportedProtocolVersion {
583 requested: "2025-11-25".to_owned(),
584 }
585 );
586 assert_eq!(error.http_status(), 400);
587 assert_eq!(
588 error.jsonrpc_error_code(),
589 UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
590 );
591 }
592
593 #[test]
594 fn missing_or_empty_version_is_a_header_mismatch() {
595 for (header, body) in [
596 (None, Some(FINAL_PROTOCOL_VERSION)),
597 (Some(FINAL_PROTOCOL_VERSION), None),
598 (Some(""), Some(FINAL_PROTOCOL_VERSION)),
599 (Some(FINAL_PROTOCOL_VERSION), Some("")),
600 ] {
601 assert_eq!(
602 validate_final_protocol_version(header, body),
603 Err(ProtocolVersionError::HeaderMismatch)
604 );
605 }
606 }
607
608 #[test]
609 fn prt_03_b_positive() {
610 let admission = admit_final_request(RequestVersionMetadata {
611 header_version: Some(FINAL_PROTOCOL_VERSION),
612 body_version: Some(FINAL_PROTOCOL_VERSION),
613 })
614 .expect("matching supported header and body versions must admit the request");
615
616 assert_eq!(
617 admission.protocol_version().as_str(),
618 FINAL_PROTOCOL_VERSION
619 );
620 assert_eq!(SUPPORTED_FINAL_PROTOCOL_VERSIONS, [FINAL_PROTOCOL_VERSION]);
621 }
622
623 #[test]
624 fn prt_03_b_planted_negative() {
625 let body_version = Some(FINAL_PROTOCOL_VERSION);
626 let changed_header_version = Some("2025-11-25");
627
628 let error = admit_final_request(RequestVersionMetadata {
629 header_version: changed_header_version,
630 body_version,
631 })
632 .expect_err("changing only the header must retain header-mismatch precedence");
633
634 assert_eq!(
635 error,
636 RequestAdmissionError::HeaderMismatch(HeaderMismatchError {
637 reason: HeaderMismatchReason::HeaderBodyVersionMismatch,
638 })
639 );
640 assert_eq!(error.jsonrpc_error_code(), HEADER_MISMATCH_ERROR_CODE);
641 assert_eq!(error.http_status(), 400);
642 assert_eq!(body_version, Some(FINAL_PROTOCOL_VERSION));
643 }
644
645 #[test]
646 fn matching_unsupported_version_is_classified_after_the_mirror_check() {
647 let error = admit_final_request(RequestVersionMetadata {
648 header_version: Some("2025-11-25"),
649 body_version: Some("2025-11-25"),
650 })
651 .expect_err("matching unsupported version must reject after mirror validation");
652
653 let RequestAdmissionError::UnsupportedProtocolVersion(error) = error else {
654 panic!("matching values must not use the header-mismatch error");
655 };
656 assert_eq!(error.requested(), "2025-11-25");
657 assert_eq!(error.supported_versions(), [FINAL_PROTOCOL_VERSION]);
658 assert_eq!(
659 error.jsonrpc_error_code(),
660 UNSUPPORTED_PROTOCOL_VERSION_ERROR_CODE
661 );
662 assert_eq!(error.http_status(), 400);
663 }
664
665 #[test]
666 fn typed_errors_preserve_only_their_final_peer_data_shapes() {
667 let mismatch = HeaderMismatchError {
668 reason: HeaderMismatchReason::MissingHeader,
669 };
670 assert_eq!(mismatch.canonical_error_data(), None);
671
672 let unsupported = UnsupportedProtocolVersionError {
673 requested: "2025-11-25".to_owned(),
674 };
675 assert_eq!(
676 unsupported.canonical_error_data(),
677 json!({"supported": [FINAL_PROTOCOL_VERSION], "requested": "2025-11-25"})
678 );
679
680 let missing = MissingRequiredClientCapabilityError::new(json!({
681 "roots": {"listChanged": true},
682 "sampling": {"context": {}}
683 }))
684 .expect("bounded capability object is valid typed peer data");
685 assert_eq!(missing.http_status(), 400);
686 assert_eq!(
687 missing.jsonrpc_error_code(),
688 MISSING_REQUIRED_CLIENT_CAPABILITY_ERROR_CODE
689 );
690 assert_eq!(
691 missing.canonical_error_data(),
692 json!({
693 "requiredCapabilities": {
694 "roots": {"listChanged": true},
695 "sampling": {"context": {}}
696 }
697 })
698 );
699
700 let typed_missing =
701 MissingRequiredClientCapabilityError::from_client_capabilities(&ClientCapabilities {
702 roots: Some(crate::RootsCapability { list_changed: true }),
703 ..ClientCapabilities::default()
704 })
705 .expect("typed capabilities serialize as a bounded required-capabilities object");
706 assert_eq!(
707 typed_missing.canonical_error_data(),
708 json!({"requiredCapabilities": {"roots": {"listChanged": true}}})
709 );
710 }
711}