1use std::collections::BTreeMap;
8use std::fmt;
9
10use base64::Engine as _;
11use serde::ser::SerializeMap;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use serde_json::value::RawValue;
15
16#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum CommonTypeError {
19 Invalid(&'static str),
21 TooLong(&'static str),
23}
24
25impl fmt::Display for CommonTypeError {
26 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
27 match self {
28 Self::Invalid(field) => write!(formatter, "invalid {field}"),
29 Self::TooLong(field) => write!(formatter, "{field} exceeds its wire limit"),
30 }
31 }
32}
33
34impl std::error::Error for CommonTypeError {}
35
36pub const MAX_ABSOLUTE_URI_BYTES: usize = 64 * 1024;
38pub const MAX_ICON_DATA_URI_PREFIX_BYTES: usize = 1024;
40pub const MAX_ICON_DATA_URI_DECODED_BYTES: usize = 8 * 1024 * 1024;
42pub const MAX_ICON_DATA_URI_ENCODED_BYTES: usize =
44 4 * MAX_ICON_DATA_URI_DECODED_BYTES.div_ceil(3) + MAX_ICON_DATA_URI_PREFIX_BYTES;
45pub const MAX_CANCELLATION_REASON_BYTES: usize = 4 * 1024;
50pub const MAX_METADATA_ENTRIES: usize = 128;
52pub const MAX_METADATA_KEY_BYTES: usize = 512;
54pub const MAX_METADATA_VALUE_BYTES: usize = 16 * 1024;
56pub const MAX_CURSOR_BYTES: usize = 4 * 1024;
58pub const MAX_ICON_SIZE_ENTRIES: usize = 32;
60pub const MAX_ICON_SIZE_BYTES: usize = 128;
62pub const MAX_CONTENT_ENCODED_BYTES: usize = 1024 * 1024;
64pub const MAX_TRACE_FIELD_BYTES: usize = 4 * 1024;
66pub const MAX_EXACT_PROGRESS_NUMBER_BYTES: usize = 256;
68pub const MAX_EXACT_PROGRESS_EXPONENT_ABS: i32 = 9_999;
70pub const MAX_JSON_INTEGER_BYTES: usize = 4 * 1024;
72pub const MAX_JSON_INTEGER_EXPONENT_ABS: i32 = 10_000;
74
75#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
80#[serde(transparent)]
81pub struct AbsoluteUri(String);
82
83impl AbsoluteUri {
84 pub fn parse(value: impl Into<String>) -> Result<Self, CommonTypeError> {
86 let value = value.into();
87 if value.is_empty() || value.len() > MAX_ABSOLUTE_URI_BYTES {
88 return Err(if value.len() > MAX_ABSOLUTE_URI_BYTES {
89 CommonTypeError::TooLong("URI")
90 } else {
91 CommonTypeError::Invalid("absolute URI")
92 });
93 }
94 if !value.is_ascii() || value.bytes().any(|byte| byte <= 0x20 || byte == 0x7f) {
95 return Err(CommonTypeError::Invalid("absolute URI"));
96 }
97 let Some(colon) = value.find(':') else {
98 return Err(CommonTypeError::Invalid("URI scheme"));
99 };
100 let scheme = &value[..colon];
101 if scheme.is_empty()
102 || !scheme.as_bytes()[0].is_ascii_alphabetic()
103 || !scheme
104 .bytes()
105 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
106 || !valid_uri_remainder(&value[colon + 1..])
107 {
108 return Err(CommonTypeError::Invalid("absolute URI"));
109 }
110 Ok(Self(value))
111 }
112
113 #[must_use]
115 pub fn as_str(&self) -> &str {
116 &self.0
117 }
118
119 #[must_use]
121 pub fn scheme(&self) -> &str {
122 &self.0[..self.0.find(':').expect("validated URI has a scheme")]
123 }
124
125 #[must_use]
127 pub fn has_scheme(&self, scheme: &str) -> bool {
128 self.scheme().eq_ignore_ascii_case(scheme)
129 }
130}
131
132impl<'de> Deserialize<'de> for AbsoluteUri {
133 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
134 where
135 D: serde::Deserializer<'de>,
136 {
137 String::deserialize(deserializer)
138 .and_then(|value| Self::parse(value).map_err(serde::de::Error::custom))
139 }
140}
141
142#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
147#[serde(transparent)]
148pub struct RawIconSourceUri(String);
149
150impl RawIconSourceUri {
151 pub fn parse(value: impl Into<String>) -> Result<Self, CommonTypeError> {
153 let value = value.into();
154 let Some(colon) = value.find(':') else {
155 return Err(CommonTypeError::Invalid("URI scheme"));
156 };
157 let scheme = &value[..colon];
158 let limit = if scheme.eq_ignore_ascii_case("data") {
159 MAX_ICON_DATA_URI_ENCODED_BYTES
160 } else {
161 MAX_ABSOLUTE_URI_BYTES
162 };
163 if value.is_empty() || value.len() > limit {
164 return Err(if value.len() > limit {
165 CommonTypeError::TooLong("icon source URI")
166 } else {
167 CommonTypeError::Invalid("absolute URI")
168 });
169 }
170 if !value.is_ascii()
171 || value.bytes().any(|byte| byte <= 0x20 || byte == 0x7f)
172 || scheme.is_empty()
173 || !scheme.as_bytes()[0].is_ascii_alphabetic()
174 || !scheme
175 .bytes()
176 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'-' | b'.'))
177 || !valid_uri_remainder(&value[colon + 1..])
178 {
179 return Err(CommonTypeError::Invalid("absolute URI"));
180 }
181 if scheme.eq_ignore_ascii_case("data") {
182 let before_fragment = value
183 .split_once('#')
184 .map_or(value.as_str(), |(before_fragment, _)| before_fragment);
185 let structural_data_uri = before_fragment
186 .split_once('?')
187 .map_or(before_fragment, |(before_query, _)| before_query);
188 validate_icon_data_uri(structural_data_uri)?;
189 }
190 Ok(Self(value))
191 }
192
193 #[must_use]
195 pub fn as_str(&self) -> &str {
196 &self.0
197 }
198}
199
200impl<'de> Deserialize<'de> for RawIconSourceUri {
201 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
202 where
203 D: serde::Deserializer<'de>,
204 {
205 String::deserialize(deserializer)
206 .and_then(|value| Self::parse(value).map_err(serde::de::Error::custom))
207 }
208}
209
210fn validate_icon_data_uri(value: &str) -> Result<(), CommonTypeError> {
211 let data = &value["data:".len()..];
212 let Some((prefix, payload)) = data.split_once(',') else {
213 return Err(CommonTypeError::Invalid("icon data URI"));
214 };
215 if prefix.len() + 1 > MAX_ICON_DATA_URI_PREFIX_BYTES {
216 return Err(CommonTypeError::TooLong("icon data URI prefix"));
217 }
218 let Some(media_type) = prefix.strip_suffix(";base64") else {
219 return Err(CommonTypeError::Invalid("icon data URI"));
220 };
221 if !media_type
222 .split_once('/')
223 .is_some_and(|(kind, _)| kind.eq_ignore_ascii_case("image"))
224 || !valid_mime_type(media_type)
225 {
226 return Err(CommonTypeError::Invalid("icon data MIME type"));
227 }
228 let decoded_upper_bound = base64_decoded_upper_bound(payload)?;
229 if decoded_upper_bound > MAX_ICON_DATA_URI_DECODED_BYTES {
230 return Err(CommonTypeError::TooLong("icon data URI"));
231 }
232 validate_standard_base64(payload)
233}
234
235fn base64_decoded_upper_bound(value: &str) -> Result<usize, CommonTypeError> {
236 let unpadded = value.trim_end_matches('=');
237 if unpadded.len() % 4 == 1 || value[..unpadded.len()].contains('=') {
238 return Err(CommonTypeError::Invalid("base64 content"));
239 }
240 let groups = unpadded.len() / 4;
241 let remainder = unpadded.len() % 4;
242 groups
243 .checked_mul(3)
244 .and_then(|size| size.checked_add(if remainder == 0 { 0 } else { remainder - 1 }))
245 .ok_or(CommonTypeError::TooLong("base64 content"))
246}
247
248fn valid_uri_remainder(value: &str) -> bool {
249 let (before_fragment, fragment) = match value.split_once('#') {
250 Some((before_fragment, fragment)) if !fragment.contains('#') => {
251 (before_fragment, Some(fragment))
252 }
253 Some(_) => return false,
254 None => (value, None),
255 };
256 let (hier_part, query) = match before_fragment.split_once('?') {
257 Some((hier_part, query)) if !query.contains('?') => (hier_part, Some(query)),
258 Some((hier_part, query)) => (hier_part, Some(query)),
259 None => (before_fragment, None),
260 };
261 valid_hier_part(hier_part)
262 && query.is_none_or(valid_query_or_fragment)
263 && fragment.is_none_or(valid_query_or_fragment)
264}
265
266fn valid_hier_part(value: &str) -> bool {
267 if let Some(authority_and_path) = value.strip_prefix("//") {
268 let (authority, path) = authority_and_path
269 .split_once('/')
270 .map_or((authority_and_path, ""), |(authority, suffix)| {
271 (authority, suffix)
272 });
273 valid_authority(authority) && valid_path(path)
274 } else {
275 valid_path(value)
276 }
277}
278
279fn valid_authority(value: &str) -> bool {
280 let (userinfo, host_and_port) = match value.rsplit_once('@') {
281 Some((userinfo, host_and_port)) if !userinfo.contains('@') && valid_userinfo(userinfo) => {
282 (Some(userinfo), host_and_port)
283 }
284 Some(_) => return false,
285 None => (None, value),
286 };
287 let _ = userinfo;
288 if let Some(host) = host_and_port.strip_prefix('[') {
289 let Some((literal, port)) = host.split_once(']') else {
290 return false;
291 };
292 if port.contains(']') || !valid_port(port) {
293 return false;
294 }
295 return valid_ip_literal(literal);
296 }
297 if host_and_port.contains('[') || host_and_port.contains(']') {
298 return false;
299 }
300 let (host, port) = host_and_port
301 .rsplit_once(':')
302 .map_or((host_and_port, None), |(host, port)| (host, Some(port)));
303 valid_reg_name(host) && port.is_none_or(|port| port.bytes().all(|byte| byte.is_ascii_digit()))
304}
305
306fn valid_ip_literal(value: &str) -> bool {
307 value.parse::<std::net::Ipv6Addr>().is_ok()
308 || value
309 .strip_prefix('v')
310 .or_else(|| value.strip_prefix('V'))
311 .is_some_and(|future| {
312 let Some((version, address)) = future.split_once('.') else {
313 return false;
314 };
315 !version.is_empty()
316 && version.bytes().all(|byte| byte.is_ascii_hexdigit())
317 && !address.is_empty()
318 && address.bytes().all(is_ip_future_character)
319 })
320}
321
322fn is_ip_future_character(byte: u8) -> bool {
323 is_unreserved(byte) || is_sub_delim(byte) || byte == b':'
324}
325
326fn valid_port(value: &str) -> bool {
327 value.is_empty()
328 || (value.starts_with(':') && value[1..].bytes().all(|byte| byte.is_ascii_digit()))
329}
330
331fn valid_userinfo(value: &str) -> bool {
332 valid_component(value, |byte| is_pchar(byte) || byte == b':')
333}
334
335fn valid_reg_name(value: &str) -> bool {
336 valid_component(value, |byte| is_unreserved(byte) || is_sub_delim(byte))
337}
338
339fn valid_path(value: &str) -> bool {
340 valid_component(value, |byte| is_pchar(byte) || byte == b'/')
341}
342
343fn valid_query_or_fragment(value: &str) -> bool {
344 valid_component(value, |byte| is_pchar(byte) || matches!(byte, b'/' | b'?'))
345}
346
347fn valid_component(value: &str, permits: impl Fn(u8) -> bool) -> bool {
348 let bytes = value.as_bytes();
349 let mut index = 0;
350 while index < bytes.len() {
351 if bytes[index] == b'%' {
352 if index + 2 >= bytes.len()
353 || !bytes[index + 1].is_ascii_hexdigit()
354 || !bytes[index + 2].is_ascii_hexdigit()
355 {
356 return false;
357 }
358 index += 3;
359 } else if permits(bytes[index]) {
360 index += 1;
361 } else {
362 return false;
363 }
364 }
365 true
366}
367
368fn is_pchar(byte: u8) -> bool {
369 is_unreserved(byte) || is_sub_delim(byte) || matches!(byte, b':' | b'@')
370}
371
372fn is_unreserved(byte: u8) -> bool {
373 byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
374}
375
376fn is_sub_delim(byte: u8) -> bool {
377 matches!(
378 byte,
379 b'!' | b'$' | b'&' | b'\'' | b'(' | b')' | b'*' | b'+' | b',' | b';' | b'='
380 )
381}
382
383#[derive(Clone, Debug, Eq, PartialEq)]
385pub enum OpaqueCursor {
386 Absent,
388 Present(String),
390}
391
392impl Serialize for OpaqueCursor {
393 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
394 where
395 S: serde::Serializer,
396 {
397 match self {
398 Self::Absent => Err(serde::ser::Error::custom(
402 "an absent cursor must be omitted from its enclosing object",
403 )),
404 Self::Present(value) => serializer.serialize_str(value),
405 }
406 }
407}
408
409impl<'de> Deserialize<'de> for OpaqueCursor {
410 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
411 where
412 D: serde::Deserializer<'de>,
413 {
414 String::deserialize(deserializer).and_then(|value| {
415 Self::try_from_presence(Some(value)).map_err(serde::de::Error::custom)
416 })
417 }
418}
419
420impl OpaqueCursor {
421 #[must_use]
423 pub fn from_presence(value: Option<String>) -> Self {
424 value.map_or(Self::Absent, Self::Present)
425 }
426
427 pub fn try_from_presence(value: Option<String>) -> Result<Self, CommonTypeError> {
429 if value
430 .as_ref()
431 .is_some_and(|cursor| cursor.len() > MAX_CURSOR_BYTES)
432 {
433 return Err(CommonTypeError::TooLong("pagination cursor"));
434 }
435 Ok(Self::from_presence(value))
436 }
437
438 #[must_use]
440 pub fn as_present(&self) -> Option<&str> {
441 match self {
442 Self::Absent => None,
443 Self::Present(value) => Some(value),
444 }
445 }
446}
447
448#[derive(Clone, Debug, Eq, PartialEq)]
455pub struct JsonInteger(serde_json::Number);
456
457impl JsonInteger {
458 pub fn try_from_number(value: serde_json::Number) -> Result<Self, CommonTypeError> {
460 validate_json_integer(value.as_str())?;
461 Ok(Self(value))
462 }
463
464 #[must_use]
466 pub fn as_str(&self) -> &str {
467 self.0.as_str()
468 }
469
470 #[must_use]
476 pub fn to_number(&self) -> serde_json::Number {
477 self.0.clone()
478 }
479
480 #[must_use]
487 pub fn as_i32(&self) -> Option<i32> {
488 json_integer_as_i32(self.as_str())
489 }
490}
491
492impl std::str::FromStr for JsonInteger {
493 type Err = CommonTypeError;
494
495 fn from_str(value: &str) -> Result<Self, Self::Err> {
497 validate_json_integer(value)?;
498
499 Ok(Self(serde_json::Number::from_string_unchecked(
504 value.to_owned(),
505 )))
506 }
507}
508
509impl TryFrom<&str> for JsonInteger {
510 type Error = CommonTypeError;
511
512 fn try_from(value: &str) -> Result<Self, Self::Error> {
513 value.parse()
514 }
515}
516
517impl fmt::Display for JsonInteger {
518 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
519 formatter.write_str(self.as_str())
520 }
521}
522
523impl From<i32> for JsonInteger {
524 fn from(value: i32) -> Self {
525 Self(serde_json::Number::from(value))
526 }
527}
528
529impl From<fastmcp_core::McpErrorCode> for JsonInteger {
530 fn from(value: fastmcp_core::McpErrorCode) -> Self {
531 Self::from(i32::from(value))
532 }
533}
534
535impl From<i64> for JsonInteger {
536 fn from(value: i64) -> Self {
537 Self(serde_json::Number::from(value))
538 }
539}
540
541impl From<u64> for JsonInteger {
542 fn from(value: u64) -> Self {
543 Self(serde_json::Number::from(value))
544 }
545}
546
547impl Serialize for JsonInteger {
548 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
549 where
550 S: serde::Serializer,
551 {
552 self.0.serialize(serializer)
553 }
554}
555
556const SERDE_JSON_RAW_VALUE_TOKEN: &str = "$serde_json::private::RawValue";
559const SERDE_JSON_NUMBER_TOKEN: &str = "$serde_json::private::Number";
561
562impl<'de> Deserialize<'de> for JsonInteger {
563 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
564 where
565 D: serde::Deserializer<'de>,
566 {
567 struct JsonIntegerVisitor;
576
577 impl<'de> serde::de::Visitor<'de> for JsonIntegerVisitor {
578 type Value = JsonInteger;
579
580 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
581 formatter.write_str("a mathematically integral JSON number")
582 }
583
584 fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<JsonInteger, E> {
585 Ok(JsonInteger::from(value))
586 }
587
588 fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<JsonInteger, E> {
589 Ok(JsonInteger::from(value))
590 }
591
592 fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<JsonInteger, E> {
593 value.to_string().parse().map_err(E::custom)
594 }
595
596 fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<JsonInteger, E> {
597 value.to_string().parse().map_err(E::custom)
598 }
599
600 fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<JsonInteger, E> {
601 serde_json::Number::from_f64(value)
602 .ok_or_else(|| E::custom("JSON integer must be finite"))
603 .and_then(|number| JsonInteger::try_from_number(number).map_err(E::custom))
604 }
605
606 fn visit_newtype_struct<D2>(self, deserializer: D2) -> Result<JsonInteger, D2::Error>
607 where
608 D2: serde::Deserializer<'de>,
609 {
610 deserializer.deserialize_any(self)
611 }
612
613 fn visit_map<A>(self, mut map: A) -> Result<JsonInteger, A::Error>
614 where
615 A: serde::de::MapAccess<'de>,
616 {
617 let Some(key) = map.next_key::<std::borrow::Cow<'_, str>>()? else {
618 return Err(serde::de::Error::custom("JSON integer cannot be an object"));
619 };
620 if key != SERDE_JSON_RAW_VALUE_TOKEN && key != SERDE_JSON_NUMBER_TOKEN {
621 return Err(serde::de::Error::custom("JSON integer cannot be an object"));
622 }
623 let lexeme = map.next_value::<std::borrow::Cow<'_, str>>()?;
624 lexeme.parse().map_err(serde::de::Error::custom)
625 }
626 }
627
628 deserializer.deserialize_newtype_struct(SERDE_JSON_RAW_VALUE_TOKEN, JsonIntegerVisitor)
629 }
630}
631
632#[derive(Clone, Debug)]
637pub struct ExactNonNegativeJsonNumber {
638 raw: Box<RawValue>,
639 negative: bool,
640 significant_digits: String,
641 decimal_point: i32,
642}
643
644impl ExactNonNegativeJsonNumber {
645 pub fn try_from_number(number: serde_json::Number) -> Result<Self, CommonTypeError> {
647 Self::parse(number.as_str())
648 }
649
650 pub fn parse(lexeme: &str) -> Result<Self, CommonTypeError> {
652 let raw = RawValue::from_string(lexeme.to_owned())
653 .map_err(|_| CommonTypeError::Invalid("JSON progress number"))?;
654 Self::from_raw(raw)
655 }
656
657 fn from_raw(raw: Box<RawValue>) -> Result<Self, CommonTypeError> {
658 let lexeme = raw.get();
659 if lexeme.len() > MAX_EXACT_PROGRESS_NUMBER_BYTES {
660 return Err(CommonTypeError::TooLong("exact progress number"));
661 }
662 let (negative, unsigned_lexeme) = match lexeme.strip_prefix('-') {
663 Some(unsigned_lexeme) => (true, unsigned_lexeme),
664 None => (false, lexeme),
665 };
666
667 let (mantissa, exponent) =
668 match unsigned_lexeme.find(|character| matches!(character, 'e' | 'E')) {
669 Some(index) => (
670 &unsigned_lexeme[..index],
671 parse_bounded_progress_exponent(&unsigned_lexeme[index + 1..])?,
672 ),
673 None => (unsigned_lexeme, 0),
674 };
675 let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
676 if whole.is_empty()
677 || !whole.bytes().all(|byte| byte.is_ascii_digit())
678 || !fraction.bytes().all(|byte| byte.is_ascii_digit())
679 {
680 return Err(CommonTypeError::Invalid("JSON progress number"));
681 }
682 let digits = [whole, fraction].concat();
683 let first_significant = digits
684 .bytes()
685 .position(|byte| byte != b'0')
686 .unwrap_or(digits.len());
687 let significant_digits = if first_significant == digits.len() {
688 "0".to_owned()
689 } else {
690 digits[first_significant..].to_owned()
691 };
692 let decimal_point = i32::try_from(whole.len())
693 .map_err(|_| CommonTypeError::TooLong("exact progress number"))?
694 .checked_sub(
695 i32::try_from(first_significant)
696 .map_err(|_| CommonTypeError::TooLong("exact progress number"))?,
697 )
698 .and_then(|point| point.checked_add(exponent))
699 .ok_or(CommonTypeError::TooLong("exact progress number"))?;
700
701 Ok(Self {
702 raw,
703 negative,
704 significant_digits,
705 decimal_point,
706 })
707 }
708
709 #[must_use]
711 pub fn as_str(&self) -> &str {
712 self.raw.get()
713 }
714
715 fn is_zero(&self) -> bool {
716 self.significant_digits == "0"
717 }
718
719 fn cmp_magnitude(&self, other: &Self) -> std::cmp::Ordering {
720 match self.decimal_point.cmp(&other.decimal_point) {
721 std::cmp::Ordering::Equal => {}
722 order => return order,
723 }
724 for index in 0..self
725 .significant_digits
726 .len()
727 .max(other.significant_digits.len())
728 {
729 match self
730 .significant_digits
731 .as_bytes()
732 .get(index)
733 .copied()
734 .unwrap_or(b'0')
735 .cmp(
736 &other
737 .significant_digits
738 .as_bytes()
739 .get(index)
740 .copied()
741 .unwrap_or(b'0'),
742 ) {
743 std::cmp::Ordering::Equal => {}
744 order => return order,
745 }
746 }
747 std::cmp::Ordering::Equal
748 }
749}
750
751impl PartialEq for ExactNonNegativeJsonNumber {
752 fn eq(&self, other: &Self) -> bool {
753 self.cmp(other).is_eq()
754 }
755}
756
757impl Eq for ExactNonNegativeJsonNumber {}
758
759impl PartialOrd for ExactNonNegativeJsonNumber {
760 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
761 Some(self.cmp(other))
762 }
763}
764
765impl Ord for ExactNonNegativeJsonNumber {
766 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
767 match (self.is_zero(), other.is_zero()) {
768 (true, true) => return std::cmp::Ordering::Equal,
769 (true, false) => return std::cmp::Ordering::Less,
770 (false, true) => return std::cmp::Ordering::Greater,
771 (false, false) => {}
772 }
773 match (self.negative, other.negative) {
774 (true, false) => std::cmp::Ordering::Less,
775 (false, true) => std::cmp::Ordering::Greater,
776 (true, true) => other.cmp_magnitude(self),
777 (false, false) => self.cmp_magnitude(other),
778 }
779 }
780}
781
782impl Serialize for ExactNonNegativeJsonNumber {
783 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
784 where
785 S: serde::Serializer,
786 {
787 self.raw.serialize(serializer)
788 }
789}
790
791impl<'de> Deserialize<'de> for ExactNonNegativeJsonNumber {
792 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
793 where
794 D: serde::Deserializer<'de>,
795 {
796 struct ExactJsonNumberVisitor;
797
798 impl<'de> serde::de::Visitor<'de> for ExactJsonNumberVisitor {
799 type Value = ExactNonNegativeJsonNumber;
800
801 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
802 formatter.write_str("a bounded finite JSON progress number")
803 }
804
805 fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
806 ExactNonNegativeJsonNumber::parse(&value.to_string()).map_err(E::custom)
807 }
808
809 fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
810 ExactNonNegativeJsonNumber::parse(&value.to_string()).map_err(E::custom)
811 }
812
813 fn visit_i128<E: serde::de::Error>(self, value: i128) -> Result<Self::Value, E> {
814 ExactNonNegativeJsonNumber::parse(&value.to_string()).map_err(E::custom)
815 }
816
817 fn visit_u128<E: serde::de::Error>(self, value: u128) -> Result<Self::Value, E> {
818 ExactNonNegativeJsonNumber::parse(&value.to_string()).map_err(E::custom)
819 }
820
821 fn visit_f64<E: serde::de::Error>(self, value: f64) -> Result<Self::Value, E> {
822 serde_json::Number::from_f64(value)
823 .ok_or_else(|| E::custom("JSON progress number must be finite"))
824 .and_then(|number| {
825 ExactNonNegativeJsonNumber::try_from_number(number).map_err(E::custom)
826 })
827 }
828
829 fn visit_newtype_struct<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
830 where
831 D2: serde::Deserializer<'de>,
832 {
833 deserializer.deserialize_any(self)
834 }
835
836 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
837 where
838 A: serde::de::MapAccess<'de>,
839 {
840 let Some(key) = map.next_key::<std::borrow::Cow<'_, str>>()? else {
841 return Err(serde::de::Error::custom(
842 "JSON progress number cannot be an object",
843 ));
844 };
845 if key != SERDE_JSON_RAW_VALUE_TOKEN && key != SERDE_JSON_NUMBER_TOKEN {
846 return Err(serde::de::Error::custom(
847 "JSON progress number cannot be an object",
848 ));
849 }
850 let lexeme = map.next_value::<std::borrow::Cow<'_, str>>()?;
851 ExactNonNegativeJsonNumber::parse(&lexeme).map_err(serde::de::Error::custom)
852 }
853 }
854
855 deserializer.deserialize_newtype_struct(SERDE_JSON_RAW_VALUE_TOKEN, ExactJsonNumberVisitor)
856 }
857}
858
859fn parse_bounded_progress_exponent(value: &str) -> Result<i32, CommonTypeError> {
860 let (negative, digits) = match value.strip_prefix('-') {
861 Some(digits) => (true, digits),
862 None => (false, value.strip_prefix('+').unwrap_or(value)),
863 };
864 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
865 return Err(CommonTypeError::Invalid("JSON progress number"));
866 }
867 let magnitude = digits.bytes().try_fold(0_i32, |value, byte| {
868 value
869 .checked_mul(10)
870 .and_then(|value| value.checked_add(i32::from(byte - b'0')))
871 });
872 let Some(magnitude) = magnitude else {
873 return Err(CommonTypeError::TooLong("progress number exponent"));
874 };
875 if magnitude > MAX_EXACT_PROGRESS_EXPONENT_ABS {
876 return Err(CommonTypeError::TooLong("progress number exponent"));
877 }
878 Ok(if negative { -magnitude } else { magnitude })
879}
880
881fn validate_json_integer(value: &str) -> Result<(), CommonTypeError> {
882 if value.len() > MAX_JSON_INTEGER_BYTES {
883 return Err(CommonTypeError::TooLong("JSON integer"));
884 }
885 let (mantissa, exponent) = match value.find(|character| matches!(character, 'e' | 'E')) {
886 Some(index) => (
887 &value[..index],
888 parse_bounded_json_integer_exponent(&value[index + 1..])?,
889 ),
890 None => (value, 0),
891 };
892 let mantissa = mantissa.strip_prefix('-').unwrap_or(mantissa);
893 let (whole, fraction) = match mantissa.split_once('.') {
894 Some((whole, fraction)) => (whole, Some(fraction)),
895 None => (mantissa, None),
896 };
897 if whole.is_empty()
898 || !(whole == "0"
899 || (whole.as_bytes()[0].is_ascii_digit()
900 && whole.as_bytes()[0] != b'0'
901 && whole.bytes().all(|byte| byte.is_ascii_digit())))
902 || fraction.is_some_and(|fraction| {
903 fraction.is_empty() || !fraction.bytes().all(|byte| byte.is_ascii_digit())
904 })
905 {
906 return Err(CommonTypeError::Invalid("JSON integer"));
907 }
908 let fraction = fraction.unwrap_or("");
909 let digits = [whole, fraction].concat();
910 if digits.bytes().all(|byte| byte == b'0') {
911 return Ok(());
912 }
913 let scale = (fraction.len() as isize)
914 .checked_sub(exponent)
915 .ok_or(CommonTypeError::TooLong("JSON integer exponent"))?;
916 if scale <= 0
917 || digits
918 .bytes()
919 .rev()
920 .take_while(|byte| *byte == b'0')
921 .count()
922 >= usize::try_from(scale).unwrap_or(usize::MAX)
923 {
924 Ok(())
925 } else {
926 Err(CommonTypeError::Invalid("JSON integer"))
927 }
928}
929
930fn json_integer_as_i32(value: &str) -> Option<i32> {
931 let (mantissa, exponent) = match value.find(|character| matches!(character, 'e' | 'E')) {
932 Some(index) => (
933 &value[..index],
934 parse_bounded_json_integer_exponent(&value[index + 1..]).ok()?,
935 ),
936 None => (value, 0),
937 };
938 let (negative, mantissa) = match mantissa.strip_prefix('-') {
939 Some(mantissa) => (true, mantissa),
940 None => (false, mantissa),
941 };
942 let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
943 if whole
944 .bytes()
945 .chain(fraction.bytes())
946 .all(|digit| digit == b'0')
947 {
948 return Some(0);
949 }
950 let scale = (fraction.len() as isize).checked_sub(exponent)?;
951 let source_length = whole.len().checked_add(fraction.len())?;
952 let retained_source_length = if scale.is_positive() {
953 source_length.saturating_sub(usize::try_from(scale).ok()?)
954 } else {
955 source_length
956 };
957 let appended_zeroes = if scale.is_negative() {
958 scale.unsigned_abs()
959 } else {
960 0
961 };
962 let maximum = if negative {
963 i64::from(i32::MAX) + 1
964 } else {
965 i64::from(i32::MAX)
966 };
967
968 let mut magnitude = 0_i64;
969 let mut saw_nonzero = false;
970 for digit in whole
971 .bytes()
972 .chain(fraction.bytes())
973 .take(retained_source_length)
974 {
975 if !saw_nonzero && digit == b'0' {
976 continue;
977 }
978 saw_nonzero = true;
979 magnitude = magnitude
980 .checked_mul(10)?
981 .checked_add(i64::from(digit - b'0'))?;
982 if magnitude > maximum {
983 return None;
984 }
985 }
986
987 if !saw_nonzero {
988 return Some(0);
989 }
990 if appended_zeroes >= 10 {
991 return None;
992 }
993 for _ in 0..appended_zeroes {
994 magnitude = magnitude.checked_mul(10)?;
995 if magnitude > maximum {
996 return None;
997 }
998 }
999
1000 if negative {
1001 if magnitude == i64::from(i32::MAX) + 1 {
1002 Some(i32::MIN)
1003 } else {
1004 Some(-(magnitude as i32))
1005 }
1006 } else {
1007 Some(magnitude as i32)
1008 }
1009}
1010
1011fn parse_bounded_json_integer_exponent(value: &str) -> Result<isize, CommonTypeError> {
1012 let (negative, digits) = match value.strip_prefix('-') {
1013 Some(digits) => (true, digits),
1014 None => (false, value.strip_prefix('+').unwrap_or(value)),
1015 };
1016 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
1017 return Err(CommonTypeError::Invalid("JSON integer"));
1018 }
1019 let magnitude = digits.bytes().try_fold(0_i32, |value, byte| {
1020 value
1021 .checked_mul(10)
1022 .and_then(|value| value.checked_add(i32::from(byte - b'0')))
1023 });
1024 let Some(magnitude) = magnitude else {
1025 return Err(CommonTypeError::TooLong("JSON integer exponent"));
1026 };
1027 if magnitude > MAX_JSON_INTEGER_EXPONENT_ABS {
1028 return Err(CommonTypeError::TooLong("JSON integer exponent"));
1029 }
1030 let exponent = isize::try_from(magnitude)
1031 .map_err(|_| CommonTypeError::TooLong("JSON integer exponent"))?;
1032 Ok(if negative { -exponent } else { exponent })
1033}
1034
1035#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1037#[serde(rename_all = "camelCase")]
1038pub struct Implementation {
1039 pub name: String,
1041 pub version: String,
1043 #[serde(skip_serializing_if = "Option::is_none")]
1045 pub title: Option<String>,
1046 #[serde(skip_serializing_if = "Option::is_none")]
1048 pub description: Option<String>,
1049 #[serde(skip_serializing_if = "Option::is_none")]
1051 pub website_url: Option<AbsoluteUri>,
1052 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1054 pub icons: Vec<RawIcon>,
1055 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1057 pub additional: BTreeMap<String, Value>,
1058}
1059
1060#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1062#[serde(rename_all = "lowercase")]
1063pub enum LoggingLevel {
1064 Debug,
1066 Info,
1068 Notice,
1070 Warning,
1072 Error,
1074 Critical,
1076 Alert,
1078 Emergency,
1080}
1081
1082impl Implementation {
1083 pub fn try_new(
1085 name: impl Into<String>,
1086 version: impl Into<String>,
1087 ) -> Result<Self, CommonTypeError> {
1088 let name = name.into();
1089 let version = version.into();
1090 if name.is_empty() || version.is_empty() {
1091 return Err(CommonTypeError::Invalid("implementation identity"));
1092 }
1093 Ok(Self {
1094 name,
1095 version,
1096 title: None,
1097 description: None,
1098 website_url: None,
1099 icons: Vec::new(),
1100 additional: BTreeMap::new(),
1101 })
1102 }
1103
1104 #[must_use]
1106 pub fn display_name(&self) -> &str {
1107 self.title.as_deref().unwrap_or(&self.name)
1108 }
1109}
1110
1111#[derive(Deserialize)]
1112#[serde(rename_all = "camelCase")]
1113struct ImplementationWire {
1114 name: String,
1115 version: String,
1116 #[serde(default)]
1117 title: Option<String>,
1118 #[serde(default)]
1119 description: Option<String>,
1120 #[serde(default)]
1121 website_url: Option<AbsoluteUri>,
1122 #[serde(default)]
1123 icons: Vec<RawIcon>,
1124 #[serde(flatten, default)]
1125 additional: BTreeMap<String, Value>,
1126}
1127
1128impl<'de> Deserialize<'de> for Implementation {
1129 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1130 where
1131 D: serde::Deserializer<'de>,
1132 {
1133 let value = Value::deserialize(deserializer)?;
1134 reject_explicit_null_fields(&value, &["title", "description", "websiteUrl", "icons"])
1135 .map_err(serde::de::Error::custom)?;
1136 let wire: ImplementationWire =
1137 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
1138 let mut implementation =
1139 Self::try_new(wire.name, wire.version).map_err(serde::de::Error::custom)?;
1140 implementation.title = wire.title;
1141 implementation.description = wire.description;
1142 implementation.website_url = wire.website_url;
1143 implementation.icons = wire.icons;
1144 implementation.additional = wire.additional;
1145 Ok(implementation)
1146 }
1147}
1148
1149#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
1151#[serde(transparent)]
1152pub struct OpenMetadata(BTreeMap<String, Value>);
1153
1154impl OpenMetadata {
1155 pub fn try_from_entries(
1157 entries: impl IntoIterator<Item = (String, Value)>,
1158 ) -> Result<Self, CommonTypeError> {
1159 let metadata = Self::try_from_open_entries(entries, valid_metadata_key)?;
1160 metadata.validate_reserved_values()?;
1161 Ok(metadata)
1162 }
1163
1164 pub fn try_from_notification_entries(
1171 entries: impl IntoIterator<Item = (String, Value)>,
1172 ) -> Result<Self, CommonTypeError> {
1173 let metadata = Self::try_from_open_entries(entries, valid_open_metadata_key)?;
1174 metadata.validate_notification_values()?;
1175 Ok(metadata)
1176 }
1177
1178 fn try_from_open_entries(
1179 entries: impl IntoIterator<Item = (String, Value)>,
1180 valid_key: fn(&str) -> bool,
1181 ) -> Result<Self, CommonTypeError> {
1182 let mut values = BTreeMap::new();
1183 for (key, value) in entries {
1184 let value_bytes = serde_json::to_vec(&value)
1185 .map_err(|_| CommonTypeError::Invalid("metadata value"))?
1186 .len();
1187 if values.len() == MAX_METADATA_ENTRIES
1188 || key.len() > MAX_METADATA_KEY_BYTES
1189 || value_bytes > MAX_METADATA_VALUE_BYTES
1190 || !valid_key(&key)
1191 || values.insert(key, value).is_some()
1192 {
1193 return Err(CommonTypeError::Invalid("metadata key"));
1194 }
1195 }
1196 Ok(Self(values))
1197 }
1198
1199 #[must_use]
1201 pub fn get(&self, key: &str) -> Option<&Value> {
1202 self.0.get(key)
1203 }
1204
1205 #[must_use]
1207 pub fn entries(&self) -> &BTreeMap<String, Value> {
1208 &self.0
1209 }
1210
1211 pub fn protocol_version(&self) -> Result<Option<&str>, CommonTypeError> {
1213 self.optional_string("io.modelcontextprotocol/protocolVersion")
1214 }
1215
1216 pub fn client_capabilities(
1218 &self,
1219 ) -> Result<Option<&serde_json::Map<String, Value>>, CommonTypeError> {
1220 match self.0.get("io.modelcontextprotocol/clientCapabilities") {
1221 None => Ok(None),
1222 Some(Value::Object(value)) => Ok(Some(value)),
1223 Some(_) => Err(CommonTypeError::Invalid("client capabilities")),
1224 }
1225 }
1226
1227 pub fn client_info(&self) -> Result<Option<Implementation>, CommonTypeError> {
1229 self.typed_implementation("io.modelcontextprotocol/clientInfo")
1230 }
1231
1232 pub fn server_info(&self) -> Result<Option<Implementation>, CommonTypeError> {
1237 self.typed_implementation("io.modelcontextprotocol/serverInfo")
1238 }
1239
1240 pub fn log_level(&self) -> Result<Option<LoggingLevel>, CommonTypeError> {
1242 self.0
1243 .get("io.modelcontextprotocol/logLevel")
1244 .map_or(Ok(None), |value| {
1245 serde_json::from_value(value.clone())
1246 .map(Some)
1247 .map_err(|_| CommonTypeError::Invalid("logging level metadata"))
1248 })
1249 }
1250
1251 fn optional_string(&self, key: &str) -> Result<Option<&str>, CommonTypeError> {
1252 match self.0.get(key) {
1253 None => Ok(None),
1254 Some(Value::String(value)) => Ok(Some(value)),
1255 Some(_) => Err(CommonTypeError::Invalid("metadata string")),
1256 }
1257 }
1258
1259 fn typed_implementation(&self, key: &str) -> Result<Option<Implementation>, CommonTypeError> {
1260 self.0.get(key).map_or(Ok(None), |value| {
1261 serde_json::from_value(value.clone())
1262 .map(Some)
1263 .map_err(|_| CommonTypeError::Invalid("implementation metadata"))
1264 })
1265 }
1266
1267 fn validate_reserved_values(&self) -> Result<(), CommonTypeError> {
1268 if self.protocol_version()?.is_some() && self.client_capabilities()?.is_none() {
1269 return Err(CommonTypeError::Invalid("client capabilities"));
1270 }
1271 for key in [
1272 "io.modelcontextprotocol/clientInfo",
1273 "io.modelcontextprotocol/serverInfo",
1274 ] {
1275 if self.0.contains_key(key) && self.typed_implementation(key)?.is_none() {
1276 return Err(CommonTypeError::Invalid("implementation metadata"));
1277 }
1278 }
1279 let _ = self.log_level()?;
1280 if let Some(value) = self.0.get("io.modelcontextprotocol/subscriptionId") {
1281 let valid = matches!(value, Value::String(_))
1282 || matches!(value, Value::Number(number) if JsonInteger::try_from_number(number.clone()).is_ok());
1283 if !valid {
1284 return Err(CommonTypeError::Invalid("subscription ID metadata"));
1285 }
1286 }
1287 Ok(())
1288 }
1289
1290 fn validate_notification_values(&self) -> Result<(), CommonTypeError> {
1291 if let Some(value) = self.0.get("io.modelcontextprotocol/subscriptionId") {
1292 let valid = matches!(value, Value::String(_))
1293 || matches!(value, Value::Number(number) if JsonInteger::try_from_number(number.clone()).is_ok());
1294 if !valid {
1295 return Err(CommonTypeError::Invalid("subscription ID metadata"));
1296 }
1297 }
1298 Ok(())
1299 }
1300}
1301
1302impl<'de> Deserialize<'de> for OpenMetadata {
1303 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1304 where
1305 D: serde::Deserializer<'de>,
1306 {
1307 BTreeMap::<String, Value>::deserialize(deserializer)
1308 .and_then(|entries| Self::try_from_entries(entries).map_err(serde::de::Error::custom))
1309 }
1310}
1311
1312fn valid_metadata_key(key: &str) -> bool {
1313 let Some((prefix, name)) = split_metadata_key(key) else {
1314 return false;
1315 };
1316 if prefix == Some("io.modelcontextprotocol")
1317 && !matches!(
1318 name,
1319 "protocolVersion"
1320 | "clientCapabilities"
1321 | "clientInfo"
1322 | "logLevel"
1323 | "serverInfo"
1324 | "subscriptionId"
1325 )
1326 {
1327 return false;
1328 }
1329 valid_metadata_name(name)
1330}
1331
1332fn valid_open_metadata_key(key: &str) -> bool {
1333 split_metadata_key(key).is_some_and(|(_, name)| valid_metadata_name(name))
1334}
1335
1336fn split_metadata_key(key: &str) -> Option<(Option<&str>, &str)> {
1337 let (prefix, name) = match key.split_once('/') {
1338 Some((prefix, name)) if !name.contains('/') => (Some(prefix), name),
1339 Some(_) => return None,
1340 None => (None, key),
1341 };
1342 if let Some(prefix) = prefix {
1343 if !valid_reverse_dns_prefix(prefix) {
1344 return None;
1345 }
1346 }
1347 Some((prefix, name))
1348}
1349
1350fn valid_metadata_name(name: &str) -> bool {
1351 name.is_empty()
1352 || (name.as_bytes()[0].is_ascii_alphanumeric()
1353 && name.as_bytes()[name.len() - 1].is_ascii_alphanumeric()
1354 && name
1355 .bytes()
1356 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')))
1357}
1358
1359fn reject_bare_unknown_members(value: &Value, known: &[&str]) -> Result<(), CommonTypeError> {
1360 let object = value
1361 .as_object()
1362 .ok_or(CommonTypeError::Invalid("wire object"))?;
1363 for key in object.keys() {
1364 let qualified = matches!(split_metadata_key(key), Some((Some(_), _)));
1365 if !known.contains(&key.as_str()) && !qualified {
1366 return Err(CommonTypeError::Invalid("unrecognized bare wire member"));
1367 }
1368 }
1369 Ok(())
1370}
1371
1372fn reject_explicit_null_fields(value: &Value, fields: &[&str]) -> Result<(), CommonTypeError> {
1373 let object = value
1374 .as_object()
1375 .ok_or(CommonTypeError::Invalid("wire object"))?;
1376 if fields
1377 .iter()
1378 .any(|field| object.get(*field).is_some_and(Value::is_null))
1379 {
1380 return Err(CommonTypeError::Invalid("optional non-null field"));
1381 }
1382 Ok(())
1383}
1384
1385fn valid_reverse_dns_prefix(prefix: &str) -> bool {
1386 let labels = prefix.split('.').collect::<Vec<_>>();
1387 !labels.is_empty()
1388 && labels.into_iter().all(|label| {
1389 !label.is_empty()
1390 && label.as_bytes()[0].is_ascii_alphabetic()
1391 && label.as_bytes()[label.len() - 1].is_ascii_alphanumeric()
1392 && label
1393 .bytes()
1394 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
1395 })
1396}
1397
1398#[derive(Clone, Debug, Default, Eq, PartialEq)]
1400pub struct TraceContext {
1401 pub traceparent: Option<String>,
1403 pub tracestate: Option<String>,
1405 pub baggage: Option<String>,
1407}
1408
1409impl TraceContext {
1410 pub fn try_from_metadata(metadata: &OpenMetadata) -> Result<Self, CommonTypeError> {
1412 let field = |name| {
1413 metadata.optional_string(name).and_then(|value| {
1414 if value.is_some_and(|field| {
1415 field.len() > MAX_TRACE_FIELD_BYTES
1416 || !field.is_ascii()
1417 || field.bytes().any(|byte| byte <= 0x20 || byte == 0x7f)
1418 }) {
1419 Err(CommonTypeError::TooLong("trace context"))
1420 } else {
1421 Ok(value.map(ToOwned::to_owned))
1422 }
1423 })
1424 };
1425 let traceparent = field("traceparent")?;
1426 if traceparent
1427 .as_deref()
1428 .is_some_and(|value| !valid_traceparent(value))
1429 {
1430 return Err(CommonTypeError::Invalid("traceparent"));
1431 }
1432 Ok(Self {
1433 traceparent,
1434 tracestate: field("tracestate")?,
1435 baggage: field("baggage")?,
1436 })
1437 }
1438}
1439
1440fn valid_traceparent(value: &str) -> bool {
1441 let mut fields = value.split('-');
1442 let (Some(version), Some(trace_id), Some(parent_id), Some(flags), None) = (
1443 fields.next(),
1444 fields.next(),
1445 fields.next(),
1446 fields.next(),
1447 fields.next(),
1448 ) else {
1449 return false;
1450 };
1451 valid_lower_hex(version, 2)
1452 && version != "ff"
1453 && valid_lower_hex(trace_id, 32)
1454 && trace_id.bytes().any(|byte| byte != b'0')
1455 && valid_lower_hex(parent_id, 16)
1456 && parent_id.bytes().any(|byte| byte != b'0')
1457 && valid_lower_hex(flags, 2)
1458}
1459
1460fn valid_lower_hex(value: &str, length: usize) -> bool {
1461 value.len() == length
1462 && value
1463 .bytes()
1464 .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
1465}
1466
1467#[derive(Clone, Debug, Eq, PartialEq)]
1469pub enum CancellationRequestId {
1470 String(String),
1472 Integer(i64),
1474 IntegerExact(JsonInteger),
1476}
1477
1478impl Serialize for CancellationRequestId {
1479 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1480 where
1481 S: serde::Serializer,
1482 {
1483 match self {
1484 Self::String(value) => value.serialize(serializer),
1485 Self::Integer(value) => value.serialize(serializer),
1486 Self::IntegerExact(value) => value.serialize(serializer),
1487 }
1488 }
1489}
1490
1491impl<'de> Deserialize<'de> for CancellationRequestId {
1492 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1493 where
1494 D: serde::Deserializer<'de>,
1495 {
1496 match Value::deserialize(deserializer)? {
1497 Value::String(value) => Ok(Self::String(value)),
1498 Value::Number(value) => {
1499 let integer =
1500 JsonInteger::try_from_number(value).map_err(serde::de::Error::custom)?;
1501 match integer.as_str().parse::<i64>() {
1502 Ok(value) if integer.as_str() == value.to_string() => Ok(Self::Integer(value)),
1503 _ => Ok(Self::IntegerExact(integer)),
1504 }
1505 }
1506 _ => Err(serde::de::Error::custom(
1507 "cancellation request ID must be a string or mathematical integer",
1508 )),
1509 }
1510 }
1511}
1512
1513#[derive(Clone, Eq, PartialEq)]
1516pub struct UntrustedCancellationReason(String);
1517
1518#[derive(Clone, Eq, PartialEq)]
1520pub struct CancellationNotification {
1521 pub request_id: CancellationRequestId,
1523 reason: Option<UntrustedCancellationReason>,
1524}
1525
1526impl CancellationNotification {
1527 pub fn try_new(
1530 request_id: CancellationRequestId,
1531 reason: Option<String>,
1532 ) -> Result<Self, CommonTypeError> {
1533 let reason = reason.map(UntrustedCancellationReason);
1534 Ok(Self { request_id, reason })
1535 }
1536
1537 #[must_use]
1539 pub fn has_untrusted_reason(&self) -> bool {
1540 self.reason.is_some()
1541 }
1542}
1543
1544#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1546#[serde(rename_all = "lowercase")]
1547pub enum IconTheme {
1548 Light,
1550 Dark,
1552}
1553
1554#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1556#[serde(rename_all = "camelCase")]
1557pub struct RawIcon {
1558 pub src: RawIconSourceUri,
1560 #[serde(skip_serializing_if = "Option::is_none")]
1562 pub mime_type: Option<String>,
1563 #[serde(skip_serializing_if = "Option::is_none")]
1565 pub sizes: Option<Vec<String>>,
1566 #[serde(skip_serializing_if = "Option::is_none")]
1568 pub theme: Option<IconTheme>,
1569 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1571 pub additional: BTreeMap<String, Value>,
1572}
1573
1574impl RawIcon {
1575 pub fn try_new(src: impl Into<String>) -> Result<Self, CommonTypeError> {
1577 Ok(Self {
1578 src: RawIconSourceUri::parse(src)?,
1579 mime_type: None,
1580 sizes: None,
1581 theme: None,
1582 additional: BTreeMap::new(),
1583 })
1584 }
1585
1586 pub fn try_with_details(
1588 src: impl Into<String>,
1589 mime_type: Option<String>,
1590 sizes: Option<Vec<String>>,
1591 theme: Option<IconTheme>,
1592 ) -> Result<Self, CommonTypeError> {
1593 if sizes.as_ref().is_some_and(|values| {
1594 values.len() > MAX_ICON_SIZE_ENTRIES
1595 || values.iter().any(|value| value.len() > MAX_ICON_SIZE_BYTES)
1596 }) {
1597 return Err(CommonTypeError::TooLong("icon sizes"));
1598 }
1599 Ok(Self {
1600 src: RawIconSourceUri::parse(src)?,
1601 mime_type,
1602 sizes,
1603 theme,
1604 additional: BTreeMap::new(),
1605 })
1606 }
1607
1608 #[must_use]
1610 pub fn effective_any_size(&self) -> bool {
1611 self.sizes.is_none()
1612 }
1613}
1614
1615#[derive(Deserialize)]
1616#[serde(rename_all = "camelCase")]
1617struct RawIconWire {
1618 src: RawIconSourceUri,
1619 #[serde(default)]
1620 mime_type: Option<String>,
1621 #[serde(default)]
1622 sizes: Option<Vec<String>>,
1623 #[serde(default)]
1624 theme: Option<IconTheme>,
1625 #[serde(flatten, default)]
1626 additional: BTreeMap<String, Value>,
1627}
1628
1629impl<'de> Deserialize<'de> for RawIcon {
1630 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1631 where
1632 D: serde::Deserializer<'de>,
1633 {
1634 let value = Value::deserialize(deserializer)?;
1635 reject_explicit_null_fields(&value, &["mimeType", "sizes", "theme"])
1636 .map_err(serde::de::Error::custom)?;
1637 let wire: RawIconWire = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
1638 let mut icon =
1639 Self::try_with_details(wire.src.as_str(), wire.mime_type, wire.sizes, wire.theme)
1640 .map_err(serde::de::Error::custom)?;
1641 icon.additional = wire.additional;
1642 Ok(icon)
1643 }
1644}
1645
1646#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
1648#[serde(rename_all = "lowercase")]
1649pub enum AnnotationAudience {
1650 User,
1652 Assistant,
1654}
1655
1656#[derive(Clone, Debug, Default, PartialEq, Serialize)]
1658#[serde(rename_all = "camelCase")]
1659pub struct Annotations {
1660 #[serde(skip_serializing_if = "Option::is_none")]
1662 pub audience: Option<Vec<AnnotationAudience>>,
1663 #[serde(skip_serializing_if = "Option::is_none")]
1665 pub priority: Option<f64>,
1666 #[serde(skip_serializing_if = "Option::is_none")]
1668 pub last_modified: Option<String>,
1669 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
1671 pub additional: BTreeMap<String, Value>,
1672}
1673
1674impl Annotations {
1675 pub fn try_with_priority(priority: f64) -> Result<Self, CommonTypeError> {
1677 if !priority.is_finite() || !(0.0..=1.0).contains(&priority) {
1678 return Err(CommonTypeError::Invalid("annotation priority"));
1679 }
1680 Ok(Self {
1681 priority: Some(priority),
1682 ..Self::default()
1683 })
1684 }
1685}
1686
1687#[derive(Deserialize)]
1688#[serde(rename_all = "camelCase")]
1689struct AnnotationsWire {
1690 #[serde(default)]
1691 audience: Option<Vec<AnnotationAudience>>,
1692 #[serde(default)]
1693 priority: Option<f64>,
1694 #[serde(default)]
1695 last_modified: Option<String>,
1696 #[serde(flatten, default)]
1697 additional: BTreeMap<String, Value>,
1698}
1699
1700impl<'de> Deserialize<'de> for Annotations {
1701 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1702 where
1703 D: serde::Deserializer<'de>,
1704 {
1705 let value = Value::deserialize(deserializer)?;
1706 reject_explicit_null_fields(&value, &["audience", "priority", "lastModified"])
1707 .map_err(serde::de::Error::custom)?;
1708 let wire: AnnotationsWire =
1709 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
1710 if let Some(priority) = wire.priority {
1711 Self::try_with_priority(priority).map_err(serde::de::Error::custom)?;
1712 }
1713 Ok(Self {
1714 audience: wire.audience,
1715 priority: wire.priority,
1716 last_modified: wire.last_modified,
1717 additional: wire.additional,
1718 })
1719 }
1720}
1721
1722#[derive(Clone, Debug, PartialEq)]
1724pub struct ResourceLink {
1725 pub icons: Option<Vec<RawIcon>>,
1727 pub name: String,
1729 pub title: Option<String>,
1731 pub uri: AbsoluteUri,
1733 pub description: Option<String>,
1735 pub mime_type: Option<String>,
1737 pub annotations: Option<Annotations>,
1739 pub size: Option<JsonInteger>,
1741 pub meta: Option<OpenMetadata>,
1743 pub additional: BTreeMap<String, Value>,
1745}
1746
1747impl Serialize for ResourceLink {
1748 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1749 where
1750 S: serde::Serializer,
1751 {
1752 let field_count = 3
1753 + usize::from(self.icons.is_some())
1754 + usize::from(self.title.is_some())
1755 + usize::from(self.description.is_some())
1756 + usize::from(self.mime_type.is_some())
1757 + usize::from(self.annotations.is_some())
1758 + usize::from(self.size.is_some())
1759 + usize::from(self.meta.is_some())
1760 + self.additional.len();
1761 let mut state = serializer.serialize_map(Some(field_count))?;
1762 state.serialize_entry("type", "resource_link")?;
1763 if let Some(icons) = &self.icons {
1764 state.serialize_entry("icons", icons)?;
1765 }
1766 state.serialize_entry("name", &self.name)?;
1767 if let Some(title) = &self.title {
1768 state.serialize_entry("title", title)?;
1769 }
1770 state.serialize_entry("uri", &self.uri)?;
1771 if let Some(description) = &self.description {
1772 state.serialize_entry("description", description)?;
1773 }
1774 if let Some(mime_type) = &self.mime_type {
1775 state.serialize_entry("mimeType", mime_type)?;
1776 }
1777 if let Some(annotations) = &self.annotations {
1778 state.serialize_entry("annotations", annotations)?;
1779 }
1780 if let Some(size) = &self.size {
1781 state.serialize_entry("size", size)?;
1782 }
1783 if let Some(meta) = &self.meta {
1784 state.serialize_entry("_meta", meta)?;
1785 }
1786 for (name, value) in &self.additional {
1787 state.serialize_entry(name, value)?;
1788 }
1789 state.end()
1790 }
1791}
1792
1793#[derive(Deserialize)]
1794#[serde(rename_all = "camelCase")]
1795struct ResourceLinkWire {
1796 #[serde(rename = "type")]
1797 kind: ResourceLinkKind,
1798 #[serde(default)]
1799 icons: Option<Vec<RawIcon>>,
1800 name: String,
1801 #[serde(default)]
1802 title: Option<String>,
1803 uri: AbsoluteUri,
1804 #[serde(default)]
1805 description: Option<String>,
1806 #[serde(rename = "mimeType", default)]
1807 mime_type: Option<String>,
1808 #[serde(default)]
1809 annotations: Option<Annotations>,
1810 #[serde(default)]
1811 size: Option<JsonInteger>,
1812 #[serde(rename = "_meta", default)]
1813 meta: Option<OpenMetadata>,
1814 #[serde(flatten, default)]
1815 additional: BTreeMap<String, Value>,
1816}
1817
1818#[derive(Deserialize)]
1819enum ResourceLinkKind {
1820 #[serde(rename = "resource_link")]
1821 ResourceLink,
1822}
1823
1824impl<'de> Deserialize<'de> for ResourceLink {
1825 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1826 where
1827 D: serde::Deserializer<'de>,
1828 {
1829 let value = Value::deserialize(deserializer)?;
1830 reject_explicit_null_fields(
1831 &value,
1832 &[
1833 "icons",
1834 "title",
1835 "description",
1836 "mimeType",
1837 "annotations",
1838 "size",
1839 "_meta",
1840 ],
1841 )
1842 .map_err(serde::de::Error::custom)?;
1843 let wire: ResourceLinkWire =
1844 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
1845 let ResourceLinkKind::ResourceLink = wire.kind;
1846 Ok(Self {
1847 icons: wire.icons,
1848 name: wire.name,
1849 title: wire.title,
1850 uri: wire.uri,
1851 description: wire.description,
1852 mime_type: wire.mime_type,
1853 annotations: wire.annotations,
1854 size: wire.size,
1855 meta: wire.meta,
1856 additional: wire.additional,
1857 })
1858 }
1859}
1860
1861#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
1863#[serde(rename_all = "camelCase")]
1864#[serde(untagged)]
1865pub enum EmbeddedResourceContents {
1866 Text {
1868 uri: AbsoluteUri,
1869 text: String,
1870 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
1871 mime_type: Option<String>,
1872 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1873 meta: Option<OpenMetadata>,
1874 #[serde(flatten)]
1875 additional: BTreeMap<String, Value>,
1876 },
1877 Blob {
1879 uri: AbsoluteUri,
1880 blob: String,
1881 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
1882 mime_type: Option<String>,
1883 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1884 meta: Option<OpenMetadata>,
1885 #[serde(flatten)]
1886 additional: BTreeMap<String, Value>,
1887 },
1888}
1889
1890#[derive(Deserialize)]
1891#[serde(rename_all = "camelCase")]
1892#[serde(untagged)]
1893enum EmbeddedResourceContentsWire {
1894 Text {
1895 uri: AbsoluteUri,
1896 text: String,
1897 #[serde(rename = "mimeType", default)]
1898 mime_type: Option<String>,
1899 #[serde(rename = "_meta", default)]
1900 meta: Option<OpenMetadata>,
1901 #[serde(flatten, default)]
1902 additional: BTreeMap<String, Value>,
1903 },
1904 Blob {
1905 uri: AbsoluteUri,
1906 blob: String,
1907 #[serde(rename = "mimeType", default)]
1908 mime_type: Option<String>,
1909 #[serde(rename = "_meta", default)]
1910 meta: Option<OpenMetadata>,
1911 #[serde(flatten, default)]
1912 additional: BTreeMap<String, Value>,
1913 },
1914}
1915
1916impl<'de> Deserialize<'de> for EmbeddedResourceContents {
1917 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1918 where
1919 D: serde::Deserializer<'de>,
1920 {
1921 let value = Value::deserialize(deserializer)?;
1922 let object = value
1923 .as_object()
1924 .ok_or_else(|| serde::de::Error::custom("embedded resource must be an object"))?;
1925 let has_text = object.contains_key("text");
1926 let has_blob = object.contains_key("blob");
1927 if has_text == has_blob {
1928 return Err(serde::de::Error::custom(
1929 "embedded resource requires exactly one of text or blob",
1930 ));
1931 }
1932 reject_bare_unknown_members(&value, &["uri", "text", "blob", "mimeType", "_meta"])
1936 .map_err(serde::de::Error::custom)?;
1937 reject_explicit_null_fields(&value, &["mimeType", "_meta"])
1938 .map_err(serde::de::Error::custom)?;
1939 let wire: EmbeddedResourceContentsWire =
1940 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
1941 let resource = match wire {
1942 EmbeddedResourceContentsWire::Text {
1943 uri,
1944 text,
1945 mime_type,
1946 meta,
1947 additional,
1948 } => Self::Text {
1949 uri,
1950 text,
1951 mime_type,
1952 meta,
1953 additional,
1954 },
1955 EmbeddedResourceContentsWire::Blob {
1956 uri,
1957 blob,
1958 mime_type,
1959 meta,
1960 additional,
1961 } => Self::Blob {
1962 uri,
1963 blob,
1964 mime_type,
1965 meta,
1966 additional,
1967 },
1968 };
1969 validate_embedded_resource(&resource).map_err(serde::de::Error::custom)?;
1970 Ok(resource)
1971 }
1972}
1973
1974#[derive(Clone, Debug, PartialEq, Serialize)]
1976#[serde(tag = "type", rename_all = "snake_case")]
1977pub enum ContentBlock {
1978 Text {
1980 text: String,
1981 #[serde(skip_serializing_if = "Option::is_none")]
1982 annotations: Option<Annotations>,
1983 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1984 meta: Option<OpenMetadata>,
1985 #[serde(flatten)]
1986 additional: BTreeMap<String, Value>,
1987 },
1988 Image {
1990 data: String,
1991 #[serde(rename = "mimeType")]
1992 mime_type: String,
1993 #[serde(skip_serializing_if = "Option::is_none")]
1994 annotations: Option<Annotations>,
1995 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
1996 meta: Option<OpenMetadata>,
1997 #[serde(flatten)]
1998 additional: BTreeMap<String, Value>,
1999 },
2000 Audio {
2002 data: String,
2003 #[serde(rename = "mimeType")]
2004 mime_type: String,
2005 #[serde(skip_serializing_if = "Option::is_none")]
2006 annotations: Option<Annotations>,
2007 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
2008 meta: Option<OpenMetadata>,
2009 #[serde(flatten)]
2010 additional: BTreeMap<String, Value>,
2011 },
2012 ResourceLink {
2014 #[serde(skip_serializing_if = "Option::is_none")]
2015 icons: Option<Vec<RawIcon>>,
2016 name: String,
2017 #[serde(skip_serializing_if = "Option::is_none")]
2018 title: Option<String>,
2019 uri: AbsoluteUri,
2020 #[serde(skip_serializing_if = "Option::is_none")]
2021 description: Option<String>,
2022 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
2023 mime_type: Option<String>,
2024 #[serde(skip_serializing_if = "Option::is_none")]
2025 annotations: Option<Annotations>,
2026 #[serde(skip_serializing_if = "Option::is_none")]
2027 size: Option<JsonInteger>,
2028 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
2029 meta: Option<OpenMetadata>,
2030 #[serde(flatten)]
2031 additional: BTreeMap<String, Value>,
2032 },
2033 Resource {
2035 resource: EmbeddedResourceContents,
2036 #[serde(skip_serializing_if = "Option::is_none")]
2037 annotations: Option<Annotations>,
2038 #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
2039 meta: Option<OpenMetadata>,
2040 #[serde(flatten)]
2041 additional: BTreeMap<String, Value>,
2042 },
2043}
2044
2045#[derive(Deserialize)]
2046#[serde(tag = "type", rename_all = "snake_case")]
2047enum ContentBlockWire {
2048 Text {
2049 text: String,
2050 #[serde(default)]
2051 annotations: Option<Annotations>,
2052 #[serde(rename = "_meta", default)]
2053 meta: Option<OpenMetadata>,
2054 #[serde(flatten, default)]
2055 additional: BTreeMap<String, Value>,
2056 },
2057 Image {
2058 data: String,
2059 #[serde(rename = "mimeType")]
2060 mime_type: String,
2061 #[serde(default)]
2062 annotations: Option<Annotations>,
2063 #[serde(rename = "_meta", default)]
2064 meta: Option<OpenMetadata>,
2065 #[serde(flatten, default)]
2066 additional: BTreeMap<String, Value>,
2067 },
2068 Audio {
2069 data: String,
2070 #[serde(rename = "mimeType")]
2071 mime_type: String,
2072 #[serde(default)]
2073 annotations: Option<Annotations>,
2074 #[serde(rename = "_meta", default)]
2075 meta: Option<OpenMetadata>,
2076 #[serde(flatten, default)]
2077 additional: BTreeMap<String, Value>,
2078 },
2079 ResourceLink {
2080 #[serde(default)]
2081 icons: Option<Vec<RawIcon>>,
2082 name: String,
2083 #[serde(default)]
2084 title: Option<String>,
2085 uri: AbsoluteUri,
2086 #[serde(default)]
2087 description: Option<String>,
2088 #[serde(rename = "mimeType", default)]
2089 mime_type: Option<String>,
2090 #[serde(default)]
2091 annotations: Option<Annotations>,
2092 #[serde(default)]
2093 size: Option<JsonInteger>,
2094 #[serde(rename = "_meta", default)]
2095 meta: Option<OpenMetadata>,
2096 #[serde(flatten, default)]
2097 additional: BTreeMap<String, Value>,
2098 },
2099 Resource {
2100 resource: EmbeddedResourceContents,
2101 #[serde(default)]
2102 annotations: Option<Annotations>,
2103 #[serde(rename = "_meta", default)]
2104 meta: Option<OpenMetadata>,
2105 #[serde(flatten, default)]
2106 additional: BTreeMap<String, Value>,
2107 },
2108}
2109
2110impl<'de> Deserialize<'de> for ContentBlock {
2111 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
2112 where
2113 D: serde::Deserializer<'de>,
2114 {
2115 let value = Value::deserialize(deserializer)?;
2116 let kind = value
2117 .get("type")
2118 .and_then(Value::as_str)
2119 .ok_or_else(|| serde::de::Error::custom("missing content discriminator"))?;
2120 if !matches!(
2121 kind,
2122 "text" | "image" | "audio" | "resource_link" | "resource"
2123 ) {
2124 return Err(serde::de::Error::custom("content discriminator"));
2125 }
2126 let known_members: &[&str] = match kind {
2130 "text" => &["type", "text", "annotations", "_meta"],
2131 "image" | "audio" => &["type", "data", "mimeType", "annotations", "_meta"],
2132 "resource_link" => &[
2133 "type",
2134 "icons",
2135 "name",
2136 "title",
2137 "uri",
2138 "description",
2139 "mimeType",
2140 "annotations",
2141 "size",
2142 "_meta",
2143 ],
2144 _ => &["type", "resource", "annotations", "_meta"],
2145 };
2146 reject_bare_unknown_members(&value, known_members).map_err(serde::de::Error::custom)?;
2147 let optional_non_null_fields = match kind {
2148 "resource_link" => &[
2149 "icons",
2150 "title",
2151 "description",
2152 "mimeType",
2153 "annotations",
2154 "size",
2155 "_meta",
2156 ][..],
2157 _ => &["annotations", "_meta"][..],
2158 };
2159 reject_explicit_null_fields(&value, optional_non_null_fields)
2160 .map_err(serde::de::Error::custom)?;
2161 if kind == "resource" {
2162 let resource = value
2163 .get("resource")
2164 .ok_or_else(|| serde::de::Error::custom("missing embedded resource"))?;
2165 let _ = serde_json::from_value::<EmbeddedResourceContents>(resource.clone())
2166 .map_err(serde::de::Error::custom)?;
2167 }
2168 let wire: ContentBlockWire =
2169 serde_json::from_value(value).map_err(serde::de::Error::custom)?;
2170 let content = match wire {
2171 ContentBlockWire::Text {
2172 text,
2173 annotations,
2174 meta,
2175 additional,
2176 } => Self::Text {
2177 text,
2178 annotations,
2179 meta,
2180 additional,
2181 },
2182 ContentBlockWire::Image {
2183 data,
2184 mime_type,
2185 annotations,
2186 meta,
2187 additional,
2188 } => {
2189 valid_binary_content(&data, &mime_type, "image/")
2190 .map_err(serde::de::Error::custom)?;
2191 Self::Image {
2192 data,
2193 mime_type,
2194 annotations,
2195 meta,
2196 additional,
2197 }
2198 }
2199 ContentBlockWire::Audio {
2200 data,
2201 mime_type,
2202 annotations,
2203 meta,
2204 additional,
2205 } => {
2206 valid_binary_content(&data, &mime_type, "audio/")
2207 .map_err(serde::de::Error::custom)?;
2208 Self::Audio {
2209 data,
2210 mime_type,
2211 annotations,
2212 meta,
2213 additional,
2214 }
2215 }
2216 ContentBlockWire::ResourceLink {
2217 icons,
2218 name,
2219 title,
2220 uri,
2221 description,
2222 mime_type,
2223 annotations,
2224 size,
2225 meta,
2226 additional,
2227 } => Self::ResourceLink {
2228 icons,
2229 name,
2230 title,
2231 uri,
2232 description,
2233 mime_type,
2234 annotations,
2235 size,
2236 meta,
2237 additional,
2238 },
2239 ContentBlockWire::Resource {
2240 resource,
2241 annotations,
2242 meta,
2243 additional,
2244 } => {
2245 validate_embedded_resource(&resource).map_err(serde::de::Error::custom)?;
2246 Self::Resource {
2247 resource,
2248 annotations,
2249 meta,
2250 additional,
2251 }
2252 }
2253 };
2254 FinalCommonTypesSchema::validate_content(&content).map_err(serde::de::Error::custom)?;
2255 Ok(content)
2256 }
2257}
2258
2259impl ContentBlock {
2260 #[must_use]
2262 pub fn text(text: impl Into<String>) -> Self {
2263 Self::Text {
2264 text: text.into(),
2265 annotations: None,
2266 meta: None,
2267 additional: BTreeMap::new(),
2268 }
2269 }
2270
2271 pub fn image(
2273 data: impl Into<String>,
2274 mime_type: impl Into<String>,
2275 ) -> Result<Self, CommonTypeError> {
2276 let data = data.into();
2277 let mime_type = mime_type.into();
2278 valid_binary_content(&data, &mime_type, "image/")?;
2279 Ok(Self::Image {
2280 data,
2281 mime_type,
2282 annotations: None,
2283 meta: None,
2284 additional: BTreeMap::new(),
2285 })
2286 }
2287
2288 pub fn audio(
2290 data: impl Into<String>,
2291 mime_type: impl Into<String>,
2292 ) -> Result<Self, CommonTypeError> {
2293 let data = data.into();
2294 let mime_type = mime_type.into();
2295 valid_binary_content(&data, &mime_type, "audio/")?;
2296 Ok(Self::Audio {
2297 data,
2298 mime_type,
2299 annotations: None,
2300 meta: None,
2301 additional: BTreeMap::new(),
2302 })
2303 }
2304
2305 pub fn resource_link(
2307 uri: impl Into<String>,
2308 name: impl Into<String>,
2309 ) -> Result<Self, CommonTypeError> {
2310 Ok(Self::ResourceLink {
2311 icons: None,
2312 name: name.into(),
2313 title: None,
2314 uri: AbsoluteUri::parse(uri)?,
2315 description: None,
2316 mime_type: None,
2317 annotations: None,
2318 size: None,
2319 meta: None,
2320 additional: BTreeMap::new(),
2321 })
2322 }
2323
2324 pub fn resource(
2326 uri: impl Into<String>,
2327 text: impl Into<String>,
2328 mime_type: Option<String>,
2329 ) -> Result<Self, CommonTypeError> {
2330 Ok(Self::Resource {
2331 resource: EmbeddedResourceContents::Text {
2332 uri: AbsoluteUri::parse(uri)?,
2333 text: text.into(),
2334 mime_type,
2335 meta: None,
2336 additional: BTreeMap::new(),
2337 },
2338 annotations: None,
2339 meta: None,
2340 additional: BTreeMap::new(),
2341 })
2342 }
2343}
2344
2345#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
2352#[serde(tag = "type", rename_all = "snake_case")]
2353pub enum SamplingContentBlock {
2354 Text {
2356 text: String,
2357 #[serde(default, skip_serializing_if = "Option::is_none")]
2358 annotations: Option<Annotations>,
2359 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2360 meta: Option<OpenMetadata>,
2361 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
2362 additional: BTreeMap<String, Value>,
2363 },
2364 Image {
2366 data: String,
2367 #[serde(rename = "mimeType")]
2368 mime_type: String,
2369 #[serde(default, skip_serializing_if = "Option::is_none")]
2370 annotations: Option<Annotations>,
2371 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2372 meta: Option<OpenMetadata>,
2373 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
2374 additional: BTreeMap<String, Value>,
2375 },
2376 Audio {
2378 data: String,
2379 #[serde(rename = "mimeType")]
2380 mime_type: String,
2381 #[serde(default, skip_serializing_if = "Option::is_none")]
2382 annotations: Option<Annotations>,
2383 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2384 meta: Option<OpenMetadata>,
2385 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
2386 additional: BTreeMap<String, Value>,
2387 },
2388 ToolUse {
2390 id: String,
2391 name: String,
2392 input: serde_json::Map<String, Value>,
2393 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2394 meta: Option<OpenMetadata>,
2395 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
2396 additional: BTreeMap<String, Value>,
2397 },
2398 ToolResult {
2400 #[serde(rename = "toolUseId")]
2401 tool_use_id: String,
2402 content: Vec<ContentBlock>,
2403 #[serde(rename = "isError", default, skip_serializing_if = "Option::is_none")]
2405 is_error: Option<bool>,
2406 #[serde(
2407 rename = "structuredContent",
2408 default,
2409 skip_serializing_if = "Option::is_none",
2410 deserialize_with = "deserialize_present_json_value"
2411 )]
2412 structured_content: Option<Value>,
2413 #[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
2414 meta: Option<OpenMetadata>,
2415 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
2416 additional: BTreeMap<String, Value>,
2417 },
2418}
2419
2420fn deserialize_present_json_value<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
2421where
2422 D: serde::Deserializer<'de>,
2423{
2424 Value::deserialize(deserializer).map(Some)
2425}
2426
2427fn valid_binary_content(
2428 data: &str,
2429 mime_type: &str,
2430 required_prefix: &str,
2431) -> Result<(), CommonTypeError> {
2432 if data.len() > MAX_CONTENT_ENCODED_BYTES {
2433 return Err(CommonTypeError::TooLong("binary content"));
2434 }
2435 if !mime_type.starts_with(required_prefix)
2436 || mime_type.len() == required_prefix.len()
2437 || !valid_mime_type(mime_type)
2438 {
2439 return Err(CommonTypeError::Invalid("binary MIME type"));
2440 }
2441 validate_standard_base64(data)
2442}
2443
2444fn validate_embedded_resource(resource: &EmbeddedResourceContents) -> Result<(), CommonTypeError> {
2445 match resource {
2446 EmbeddedResourceContents::Text { mime_type, .. } => {
2447 if mime_type
2448 .as_deref()
2449 .is_some_and(|value| !valid_mime_type(value))
2450 {
2451 return Err(CommonTypeError::Invalid("resource MIME type"));
2452 }
2453 }
2454 EmbeddedResourceContents::Blob {
2455 blob, mime_type, ..
2456 } => {
2457 if blob.len() > MAX_CONTENT_ENCODED_BYTES {
2458 return Err(CommonTypeError::TooLong("binary content"));
2459 }
2460 validate_standard_base64(blob)?;
2461 if mime_type
2462 .as_deref()
2463 .is_some_and(|value| !valid_mime_type(value))
2464 {
2465 return Err(CommonTypeError::Invalid("resource MIME type"));
2466 }
2467 }
2468 }
2469 Ok(())
2470}
2471
2472fn validate_standard_base64(value: &str) -> Result<(), CommonTypeError> {
2473 base64::engine::general_purpose::STANDARD
2474 .decode(value)
2475 .or_else(|_| base64::engine::general_purpose::STANDARD_NO_PAD.decode(value))
2476 .map(|_| ())
2477 .map_err(|_| CommonTypeError::Invalid("base64 content"))
2478}
2479
2480fn valid_mime_type(value: &str) -> bool {
2481 let Some((kind, subtype)) = value.split_once('/') else {
2482 return false;
2483 };
2484 !kind.is_empty()
2485 && !subtype.is_empty()
2486 && !subtype.contains('/')
2487 && kind.bytes().all(is_mime_token)
2488 && subtype.bytes().all(is_mime_token)
2489}
2490
2491fn is_mime_token(byte: u8) -> bool {
2492 byte.is_ascii_alphanumeric()
2493 || matches!(
2494 byte,
2495 b'!' | b'#' | b'$' | b'&' | b'^' | b'_' | b'-' | b'.' | b'+'
2496 )
2497}
2498
2499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2501pub enum CommonWireDirection {
2502 Request,
2504 Notification,
2506 Result,
2508}
2509
2510#[derive(Clone, Copy, Debug, Default)]
2515pub struct FinalCommonTypesSchema;
2516
2517impl FinalCommonTypesSchema {
2518 pub const FINAL_URI_OWNERS: [&'static str; 12] = [
2520 "BlobResourceContents.uri",
2521 "ElicitRequestURLParams.url",
2522 "Icon.src",
2523 "Implementation.websiteUrl",
2524 "ReadResourceRequestParams.uri",
2525 "Resource.uri",
2526 "ResourceContents.uri",
2527 "ResourceLink.uri",
2528 "ResourceRequestParams.uri",
2529 "ResourceUpdatedNotificationParams.uri",
2530 "Root.uri",
2531 "TextResourceContents.uri",
2532 ];
2533
2534 pub fn validate(direction: CommonWireDirection, wire: &Value) -> Result<(), CommonTypeError> {
2536 let object = wire
2537 .as_object()
2538 .ok_or(CommonTypeError::Invalid("common wire object"))?;
2539 match (direction, object.get("_meta")) {
2540 (CommonWireDirection::Request, Some(meta)) => Self::validate_request_metadata(meta)?,
2541 (CommonWireDirection::Request, None) => {
2542 return Err(CommonTypeError::Invalid("request metadata"));
2543 }
2544 (_, Some(meta)) => {
2545 let metadata = Self::validate_open_metadata(meta)?;
2546 let _ = TraceContext::try_from_metadata(&metadata)?;
2547 }
2548 (_, None) => {}
2549 }
2550 if let Some(kind) = object.get("type") {
2551 let kind = kind
2552 .as_str()
2553 .ok_or(CommonTypeError::Invalid("content discriminator"))?;
2554 if !matches!(
2555 kind,
2556 "text" | "image" | "audio" | "resource_link" | "resource"
2557 ) {
2558 return Err(CommonTypeError::Invalid("content discriminator"));
2559 }
2560 let content: ContentBlock = serde_json::from_value(wire.clone())
2561 .map_err(|_| CommonTypeError::Invalid("content block"))?;
2562 Self::validate_content(&content)?;
2563 }
2564 if object.contains_key("src") {
2565 let _ = Self::validate_icon(wire)?;
2566 }
2567 if object.get("method").and_then(Value::as_str) == Some("notifications/cancelled") {
2568 if direction != CommonWireDirection::Notification {
2569 return Err(CommonTypeError::Invalid("cancellation direction"));
2570 }
2571 Self::validate_cancellation_params(
2572 object
2573 .get("params")
2574 .ok_or(CommonTypeError::Invalid("cancellation params"))?,
2575 )?;
2576 }
2577 Ok(())
2578 }
2579
2580 pub fn validate_icon(wire: &Value) -> Result<RawIcon, CommonTypeError> {
2582 let object = wire
2583 .as_object()
2584 .ok_or(CommonTypeError::Invalid("icon object"))?;
2585 for field in ["mimeType", "sizes", "theme"] {
2586 if object.get(field).is_some_and(Value::is_null) {
2587 return Err(CommonTypeError::Invalid("icon optional field"));
2588 }
2589 }
2590 let icon: RawIcon =
2591 serde_json::from_value(wire.clone()).map_err(|_| CommonTypeError::Invalid("icon"))?;
2592 RawIcon::try_with_details(
2593 icon.src.as_str(),
2594 icon.mime_type.clone(),
2595 icon.sizes.clone(),
2596 icon.theme,
2597 )?;
2598 Ok(icon)
2599 }
2600
2601 pub fn canonical_json(wire: &Value) -> Result<String, CommonTypeError> {
2603 serde_json::to_string(wire).map_err(|_| CommonTypeError::Invalid("canonical JSON"))
2604 }
2605
2606 pub fn validate_golden(
2608 direction: CommonWireDirection,
2609 wire: &Value,
2610 golden: &str,
2611 ) -> Result<(), CommonTypeError> {
2612 Self::validate(direction, wire)?;
2613 if Self::canonical_json(wire)? != golden {
2614 return Err(CommonTypeError::Invalid("golden wire"));
2615 }
2616 Ok(())
2617 }
2618
2619 fn validate_open_metadata(meta: &Value) -> Result<OpenMetadata, CommonTypeError> {
2620 let entries = meta
2621 .as_object()
2622 .ok_or(CommonTypeError::Invalid("metadata object"))?
2623 .iter()
2624 .map(|(key, value)| (key.clone(), value.clone()));
2625 OpenMetadata::try_from_entries(entries)
2626 }
2627
2628 fn validate_request_metadata(meta: &Value) -> Result<(), CommonTypeError> {
2629 let metadata = Self::validate_open_metadata(meta)?;
2630 if metadata.protocol_version()?.is_none() || metadata.client_capabilities()?.is_none() {
2631 return Err(CommonTypeError::Invalid("required request metadata"));
2632 }
2633 let _ = metadata.client_info()?;
2634 let _ = TraceContext::try_from_metadata(&metadata)?;
2635 Ok(())
2636 }
2637
2638 fn validate_content(content: &ContentBlock) -> Result<(), CommonTypeError> {
2639 match content {
2640 ContentBlock::Image {
2641 data,
2642 mime_type,
2643 annotations,
2644 ..
2645 } => {
2646 Self::validate_annotations(annotations)?;
2647 valid_binary_content(data, mime_type, "image/")
2648 }
2649 ContentBlock::Audio {
2650 data,
2651 mime_type,
2652 annotations,
2653 ..
2654 } => {
2655 Self::validate_annotations(annotations)?;
2656 valid_binary_content(data, mime_type, "audio/")
2657 }
2658 ContentBlock::ResourceLink {
2659 uri,
2660 icons,
2661 annotations,
2662 ..
2663 } => {
2664 Self::validate_annotations(annotations)?;
2665 Self::validate_icons(icons)?;
2666 AbsoluteUri::parse(uri.as_str()).map(|_| ())
2667 }
2668 ContentBlock::Resource {
2669 resource,
2670 annotations,
2671 ..
2672 } => {
2673 Self::validate_annotations(annotations)?;
2674 validate_embedded_resource(resource)?;
2675 match resource {
2676 EmbeddedResourceContents::Text { uri, .. }
2677 | EmbeddedResourceContents::Blob { uri, .. } => {
2678 AbsoluteUri::parse(uri.as_str()).map(|_| ())
2679 }
2680 }
2681 }
2682 ContentBlock::Text { annotations, .. } => Self::validate_annotations(annotations),
2683 }
2684 }
2685
2686 fn validate_annotations(annotations: &Option<Annotations>) -> Result<(), CommonTypeError> {
2687 if annotations
2688 .as_ref()
2689 .and_then(|value| value.priority)
2690 .is_some_and(|priority| !priority.is_finite() || !(0.0..=1.0).contains(&priority))
2691 {
2692 return Err(CommonTypeError::Invalid("annotation priority"));
2693 }
2694 Ok(())
2695 }
2696
2697 fn validate_icons(icons: &Option<Vec<RawIcon>>) -> Result<(), CommonTypeError> {
2698 if let Some(icons) = icons {
2699 for icon in icons {
2700 let _ = RawIcon::try_with_details(
2701 icon.src.as_str(),
2702 icon.mime_type.clone(),
2703 icon.sizes.clone(),
2704 icon.theme,
2705 )?;
2706 }
2707 }
2708 Ok(())
2709 }
2710
2711 fn validate_cancellation_params(params: &Value) -> Result<(), CommonTypeError> {
2712 let params = params
2713 .as_object()
2714 .ok_or(CommonTypeError::Invalid("cancellation params"))?;
2715 let request_id = params
2716 .get("requestId")
2717 .ok_or(CommonTypeError::Invalid("cancellation request ID"))?;
2718 let request_id = serde_json::from_value::<CancellationRequestId>(request_id.clone())
2719 .map_err(|_| CommonTypeError::Invalid("cancellation request ID"))?;
2720 let reason = match params.get("reason") {
2721 None => None,
2722 Some(Value::String(value)) => Some(value.clone()),
2723 Some(_) => return Err(CommonTypeError::Invalid("cancellation reason")),
2724 };
2725 match params.get("_meta") {
2726 None => {}
2727 Some(Value::Object(entries)) => {
2728 let _ = OpenMetadata::try_from_notification_entries(
2729 entries.clone().into_iter().collect::<BTreeMap<_, _>>(),
2730 )?;
2731 }
2732 Some(_) => return Err(CommonTypeError::Invalid("cancellation metadata")),
2733 }
2734 let _ = CancellationNotification::try_new(request_id, reason)?;
2735 Ok(())
2736 }
2737}
2738
2739#[cfg(test)]
2740mod tests {
2741 use serde_json::json;
2742
2743 use super::*;
2744
2745 fn assert_json_integer_rejected_by_public_constructors(
2746 source: &str,
2747 expected: CommonTypeError,
2748 ) {
2749 assert_eq!(source.parse::<JsonInteger>(), Err(expected.clone()));
2750 assert_eq!(JsonInteger::try_from(source), Err(expected.clone()));
2751 let number = serde_json::from_str::<serde_json::Number>(source)
2752 .expect("bounded test token is valid JSON");
2753 assert_eq!(JsonInteger::try_from_number(number), Err(expected));
2754 assert!(
2755 serde_json::from_str::<JsonInteger>(source).is_err(),
2756 "deserialization must apply the same admission bound"
2757 );
2758 }
2759
2760 #[test]
2761 fn json_integer_bounded_i32_adapters_accept_equivalent_integral_spellings() {
2762 for (source, expected) in [
2763 ("-32600.0", -32_600),
2764 ("-326e2", -32_600),
2765 ("2147483647.0", i32::MAX),
2766 ("-2147483648e0", i32::MIN),
2767 ] {
2768 let value = JsonInteger::try_from(source).expect("integral JSON integer");
2769
2770 assert_eq!(value.as_i32(), Some(expected));
2771 assert_eq!(value.as_str(), source, "the input lexeme remains exact");
2772 assert_eq!(
2773 serde_json::to_string(&value).expect("integer serializes"),
2774 source,
2775 "serialization does not normalize the input lexeme"
2776 );
2777 }
2778 }
2779
2780 #[test]
2781 fn json_integer_from_value_accepts_full_width_integer_visitors() {
2782 for source in ["9007199254740993123456789", "-9007199254740993123456789"] {
2783 let value = serde_json::from_str::<Value>(source).expect("valid arbitrary-width JSON");
2784 let integer = serde_json::from_value::<JsonInteger>(value)
2785 .expect("Value replay retains an arbitrary-width mathematical integer");
2786
2787 assert_eq!(integer.as_str(), source);
2788 }
2789 }
2790
2791 #[test]
2792 fn json_integer_bounded_i32_adapters_reject_fractional_and_out_of_range_values() {
2793 for source in ["-32600.1", "2147483647.1"] {
2794 assert_eq!(
2795 JsonInteger::try_from(source),
2796 Err(CommonTypeError::Invalid("JSON integer")),
2797 "changing only the nonzero fractional digit rejects {source}"
2798 );
2799 }
2800
2801 for source in ["2147483648.0", "-2147483649e0"] {
2802 let value = source
2803 .parse::<JsonInteger>()
2804 .expect("exact out-of-range integer");
2805
2806 assert_eq!(value.as_i32(), None);
2807 assert_eq!(
2808 value.as_str(),
2809 source,
2810 "the out-of-range lexeme remains exact"
2811 );
2812 }
2813 }
2814
2815 #[test]
2816 fn json_integer_public_constructors_enforce_token_and_exponent_bounds() {
2817 let at_token_limit = "1".repeat(MAX_JSON_INTEGER_BYTES);
2818 for value in [
2819 at_token_limit
2820 .parse::<JsonInteger>()
2821 .expect("token at the retention bound parses"),
2822 JsonInteger::try_from(at_token_limit.as_str())
2823 .expect("TryFrom accepts token at the retention bound"),
2824 JsonInteger::try_from_number(
2825 serde_json::from_str::<serde_json::Number>(&at_token_limit)
2826 .expect("token at the retention bound is JSON"),
2827 )
2828 .expect("number constructor accepts token at the retention bound"),
2829 serde_json::from_str::<JsonInteger>(&at_token_limit)
2830 .expect("deserialization accepts token at the retention bound"),
2831 ] {
2832 assert_eq!(value.as_str(), at_token_limit);
2833 }
2834 assert_json_integer_rejected_by_public_constructors(
2835 &format!("{at_token_limit}0"),
2836 CommonTypeError::TooLong("JSON integer"),
2837 );
2838
2839 let at_positive_exponent_limit = format!("1e{MAX_JSON_INTEGER_EXPONENT_ABS}");
2840 let at_negative_exponent_limit = format!("0e-{MAX_JSON_INTEGER_EXPONENT_ABS}");
2841 for source in [&at_positive_exponent_limit, &at_negative_exponent_limit] {
2842 assert!(source.parse::<JsonInteger>().is_ok(), "{source}");
2843 assert!(JsonInteger::try_from(source.as_str()).is_ok(), "{source}");
2844 assert!(
2845 JsonInteger::try_from_number(
2846 serde_json::from_str::<serde_json::Number>(source)
2847 .expect("exponent-bound token is JSON"),
2848 )
2849 .is_ok(),
2850 "{source}"
2851 );
2852 assert!(
2853 serde_json::from_str::<JsonInteger>(source).is_ok(),
2854 "{source}"
2855 );
2856 }
2857 assert_json_integer_rejected_by_public_constructors(
2858 &format!("1e{}", MAX_JSON_INTEGER_EXPONENT_ABS + 1),
2859 CommonTypeError::TooLong("JSON integer exponent"),
2860 );
2861 assert_json_integer_rejected_by_public_constructors(
2862 &format!("0e-{}", MAX_JSON_INTEGER_EXPONENT_ABS + 1),
2863 CommonTypeError::TooLong("JSON integer exponent"),
2864 );
2865 }
2866
2867 #[test]
2868 fn json_integer_from_str_and_try_from_preserve_huge_lexemes() {
2869 const HUGE: &str = "12345678901234567890123456789012345678901234567890";
2870
2871 let parsed = HUGE.parse::<JsonInteger>().expect("huge integer parses");
2872 let converted = JsonInteger::try_from(HUGE).expect("huge integer converts");
2873
2874 assert_eq!(parsed.as_str(), HUGE);
2875 assert_eq!(converted.as_str(), HUGE);
2876 assert_eq!(
2877 serde_json::to_string(&parsed).expect("huge integer serializes"),
2878 HUGE
2879 );
2880 let exponent = serde_json::from_str::<JsonInteger>("-326e2")
2881 .expect("integral exponent JSON token deserializes");
2882 assert_eq!(exponent.as_str(), "-326e2");
2883 assert_eq!(
2884 serde_json::to_string(&exponent).expect("deserialized exponent serializes"),
2885 "-326e2"
2886 );
2887 for invalid_json_number in ["01", "1."] {
2888 assert_eq!(
2889 JsonInteger::try_from(invalid_json_number),
2890 Err(CommonTypeError::Invalid("JSON integer")),
2891 "the string conversion only admits JSON number grammar"
2892 );
2893 }
2894 }
2895
2896 #[test]
2897 fn exact_finite_json_numbers_preserve_signed_lexemes_and_compare_mathematically() {
2898 let large = ExactNonNegativeJsonNumber::parse("123456789012345678901234567890")
2899 .expect("large integer exact progress number");
2900 let decimal = ExactNonNegativeJsonNumber::parse("1.20e+4")
2901 .expect("decimal exponent exact progress number");
2902 let equivalent = ExactNonNegativeJsonNumber::parse("12000.0")
2903 .expect("equivalent decimal exact progress number");
2904 let greater =
2905 ExactNonNegativeJsonNumber::parse("12000.0001").expect("greater exact progress number");
2906 let negative =
2907 ExactNonNegativeJsonNumber::parse("-1.20e+4").expect("negative exact progress number");
2908 let more_negative = ExactNonNegativeJsonNumber::parse("-12000.0001")
2909 .expect("more negative exact progress number");
2910
2911 assert_eq!(large.as_str(), "123456789012345678901234567890");
2912 assert_eq!(decimal.as_str(), "1.20e+4");
2913 assert_eq!(decimal, equivalent);
2914 assert!(greater > decimal);
2915 assert!(more_negative < negative);
2916 assert!(negative < decimal);
2917 assert_eq!(
2918 serde_json::to_string(&decimal).expect("exact progress number serializes"),
2919 "1.20e+4",
2920 "the decimal/exponent lexeme re-encodes without an IEEE-754 conversion"
2921 );
2922 assert_eq!(
2923 serde_json::to_string(&negative).expect("negative exact progress number serializes"),
2924 "-1.20e+4",
2925 "the signed decimal/exponent lexeme re-encodes without an IEEE-754 conversion"
2926 );
2927 assert_eq!(
2928 ExactNonNegativeJsonNumber::parse("1e10000"),
2929 Err(CommonTypeError::TooLong("progress number exponent")),
2930 "the exact comparison representation bounds decimal exponents"
2931 );
2932 }
2933
2934 #[test]
2935 fn exact_finite_json_number_deserialization_retains_direct_wire_exponents() {
2936 for source in ["1e400", "1.20e+4", "-7.30E-12"] {
2937 let number = serde_json::from_str::<ExactNonNegativeJsonNumber>(source)
2938 .expect("bounded exact progress number deserializes");
2939
2940 assert_eq!(number.as_str(), source);
2941 assert_eq!(
2942 serde_json::to_string(&number).expect("exact progress number re-serializes"),
2943 source
2944 );
2945 }
2946 }
2947
2948 #[test]
2949 fn exact_finite_json_number_deserialization_rejects_only_the_bound_violation() {
2950 let accepted = serde_json::from_str::<ExactNonNegativeJsonNumber>("1e9999")
2951 .expect("largest admitted exponent deserializes");
2952 assert_eq!(accepted.as_str(), "1e9999");
2953
2954 assert!(
2955 serde_json::from_str::<ExactNonNegativeJsonNumber>("1e10000").is_err(),
2956 "only the exponent bound changes from the accepted token"
2957 );
2958 assert_eq!(
2959 serde_json::to_string(&accepted).expect("accepted boundary re-serializes"),
2960 "1e9999",
2961 "rejecting the adjacent exponent cannot mutate the accepted value"
2962 );
2963
2964 let at_byte_limit = "1".repeat(MAX_EXACT_PROGRESS_NUMBER_BYTES);
2965 let bounded = serde_json::from_str::<ExactNonNegativeJsonNumber>(&at_byte_limit)
2966 .expect("an exact progress number at the byte ceiling deserializes");
2967 assert_eq!(bounded.as_str(), at_byte_limit);
2968 let oversized = format!("1{}", "0".repeat(MAX_EXACT_PROGRESS_NUMBER_BYTES));
2969 assert_eq!(oversized.len(), MAX_EXACT_PROGRESS_NUMBER_BYTES + 1);
2970 assert!(
2971 serde_json::from_str::<ExactNonNegativeJsonNumber>(&oversized).is_err(),
2972 "adding one byte is the only changed dimension and exceeds the byte ceiling"
2973 );
2974 assert_eq!(
2975 serde_json::to_string(&bounded).expect("bounded number re-serializes"),
2976 at_byte_limit,
2977 "the over-limit rejection cannot alter the admitted 256-byte lexeme"
2978 );
2979 }
2980
2981 #[test]
2982 fn exact_finite_json_number_deserialization_handles_all_finite_number_visitors() {
2983 let from_i64 = <ExactNonNegativeJsonNumber as serde::Deserialize>::deserialize(
2984 serde::de::value::I64Deserializer::<serde::de::value::Error>::new(i64::MIN),
2985 )
2986 .expect("i64 visitor admits a finite JSON number");
2987 let from_u64 = <ExactNonNegativeJsonNumber as serde::Deserialize>::deserialize(
2988 serde::de::value::U64Deserializer::<serde::de::value::Error>::new(u64::MAX),
2989 )
2990 .expect("u64 visitor admits a finite JSON number");
2991 let from_i128 = <ExactNonNegativeJsonNumber as serde::Deserialize>::deserialize(
2992 serde::de::value::I128Deserializer::<serde::de::value::Error>::new(i128::MIN),
2993 )
2994 .expect("i128 visitor admits a finite JSON number");
2995 let from_u128 = <ExactNonNegativeJsonNumber as serde::Deserialize>::deserialize(
2996 serde::de::value::U128Deserializer::<serde::de::value::Error>::new(u128::MAX),
2997 )
2998 .expect("u128 visitor admits a finite JSON number");
2999 let from_f64 = <ExactNonNegativeJsonNumber as serde::Deserialize>::deserialize(
3000 serde::de::value::F64Deserializer::<serde::de::value::Error>::new(1.25),
3001 )
3002 .expect("finite f64 visitor admits a JSON number");
3003
3004 assert_eq!(from_i64.as_str(), "-9223372036854775808");
3005 assert_eq!(from_u64.as_str(), "18446744073709551615");
3006 assert_eq!(
3007 from_i128.as_str(),
3008 "-170141183460469231731687303715884105728"
3009 );
3010 assert_eq!(
3011 from_u128.as_str(),
3012 "340282366920938463463374607431768211455"
3013 );
3014 assert_eq!(from_f64.as_str(), "1.25");
3015
3016 assert!(
3017 <ExactNonNegativeJsonNumber as serde::Deserialize>::deserialize(
3018 serde::de::value::F64Deserializer::<serde::de::value::Error>::new(f64::NAN),
3019 )
3020 .is_err(),
3021 "changing only the finite f64 to NaN must fail closed"
3022 );
3023 assert_eq!(
3024 serde_json::to_string(&from_f64).expect("finite f64 result re-serializes"),
3025 "1.25",
3026 "the rejected non-finite visitor cannot change the prior finite result"
3027 );
3028 }
3029
3030 #[test]
3031 fn exact_finite_json_number_value_replay_uses_the_value_number_representation() {
3032 let direct = serde_json::from_str::<ExactNonNegativeJsonNumber>("1.20e+4")
3033 .expect("direct raw wire lexeme is admitted");
3034 let value = serde_json::from_str::<Value>("1.20e+4")
3035 .expect("the same raw number enters a serde Value replay");
3036 let expected = value
3037 .as_number()
3038 .expect("Value remains a number")
3039 .as_str()
3040 .to_owned();
3041 let replayed = serde_json::from_value::<ExactNonNegativeJsonNumber>(value)
3042 .expect("Value replay admits the finite number representation");
3043
3044 assert_eq!(direct.as_str(), "1.20e+4");
3045 assert_eq!(replayed.as_str(), expected);
3046 assert_eq!(
3047 serde_json::to_string(&replayed).expect("replayed number re-serializes"),
3048 replayed.as_str(),
3049 "replay must not apply another numeric normalization"
3050 );
3051 }
3052
3053 #[test]
3054 fn prt_02_a_positive() {
3055 let implementation = Implementation::try_new("fastmcp", "0.1.0").expect("implementation");
3056 let metadata = OpenMetadata::try_from_entries([
3057 ("".to_owned(), json!("empty name is valid")),
3058 ("com.example/".to_owned(), json!({"future": true})),
3059 (
3060 "io.modelcontextprotocol/protocolVersion".to_owned(),
3061 json!("2026-07-28"),
3062 ),
3063 (
3064 "io.modelcontextprotocol/clientCapabilities".to_owned(),
3065 json!({}),
3066 ),
3067 (
3068 "io.modelcontextprotocol/clientInfo".to_owned(),
3069 serde_json::to_value(&implementation).expect("identity JSON"),
3070 ),
3071 (
3072 "traceparent".to_owned(),
3073 json!("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00"),
3074 ),
3075 ])
3076 .expect("metadata");
3077 assert_eq!(
3078 metadata.protocol_version().expect("version"),
3079 Some("2026-07-28")
3080 );
3081 assert_eq!(
3082 TraceContext::try_from_metadata(&metadata)
3083 .expect("trace")
3084 .traceparent
3085 .as_deref(),
3086 Some("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00")
3087 );
3088
3089 let icon = RawIcon::try_new("https://example.test/icon.png?variant=1#exact").expect("icon");
3090 assert!(icon.effective_any_size());
3091 assert_eq!(
3092 OpaqueCursor::from_presence(Some(String::new())).as_present(),
3093 Some("")
3094 );
3095 let content = ContentBlock::image("aGVsbG8=", "image/png").expect("image");
3096 let encoded = serde_json::to_value(&content).expect("serialize content");
3097 assert_eq!(encoded["type"], "image");
3098 assert_eq!(
3099 serde_json::from_value::<ContentBlock>(encoded).expect("round trip"),
3100 content
3101 );
3102 }
3103
3104 #[test]
3105 fn prt_02_a_planted_negative() {
3106 let accepted = OpenMetadata::try_from_entries([(
3107 "com.example/valid".to_owned(),
3108 json!({"kept": true}),
3109 )])
3110 .expect("accepted baseline");
3111 let baseline = accepted.clone();
3112 let rejection = OpenMetadata::try_from_entries([(
3113 "com..example/valid".to_owned(),
3114 json!({"kept": true}),
3115 )]);
3116 assert_eq!(rejection, Err(CommonTypeError::Invalid("metadata key")));
3117 assert_eq!(
3118 accepted, baseline,
3119 "the rejected one-variable key change cannot mutate accepted state"
3120 );
3121 }
3122
3123 #[test]
3124 fn notification_metadata_remains_schema_open_but_bounded() {
3125 let at_entry_limit =
3126 (0..MAX_METADATA_ENTRIES).map(|index| (format!("com.example/key{index}"), Value::Null));
3127 OpenMetadata::try_from_notification_entries(at_entry_limit)
3128 .expect("notification metadata accepts N entries");
3129 let over_entry_limit = (0..=MAX_METADATA_ENTRIES)
3130 .map(|index| (format!("com.example/key{index}"), Value::Null));
3131 assert_eq!(
3132 OpenMetadata::try_from_notification_entries(over_entry_limit),
3133 Err(CommonTypeError::Invalid("metadata key"))
3134 );
3135
3136 let at_key_limit = "a".repeat(MAX_METADATA_KEY_BYTES);
3137 OpenMetadata::try_from_notification_entries([(at_key_limit, Value::Null)])
3138 .expect("notification metadata accepts an N-byte key");
3139 let over_key_limit = "a".repeat(MAX_METADATA_KEY_BYTES + 1);
3140 assert_eq!(
3141 OpenMetadata::try_from_notification_entries([(over_key_limit, Value::Null)]),
3142 Err(CommonTypeError::Invalid("metadata key"))
3143 );
3144
3145 let at_value_limit = json!("x".repeat(MAX_METADATA_VALUE_BYTES - 2));
3146 assert_eq!(
3147 serde_json::to_vec(&at_value_limit)
3148 .expect("bounded metadata value serializes")
3149 .len(),
3150 MAX_METADATA_VALUE_BYTES
3151 );
3152 OpenMetadata::try_from_notification_entries([("future".to_owned(), at_value_limit)])
3153 .expect("notification metadata accepts an N-byte value");
3154 let over_value_limit = json!("x".repeat(MAX_METADATA_VALUE_BYTES - 1));
3155 assert_eq!(
3156 serde_json::to_vec(&over_value_limit)
3157 .expect("oversized metadata value serializes")
3158 .len(),
3159 MAX_METADATA_VALUE_BYTES + 1
3160 );
3161 assert_eq!(
3162 OpenMetadata::try_from_notification_entries([("future".to_owned(), over_value_limit,)]),
3163 Err(CommonTypeError::Invalid("metadata key"))
3164 );
3165
3166 OpenMetadata::try_from_notification_entries([(
3167 "io.modelcontextprotocol/futureCancellationHint".to_owned(),
3168 json!({"schemaOpen": true}),
3169 )])
3170 .expect("unknown reserved notification metadata remains inert and admitted");
3171 }
3172
3173 #[test]
3174 fn prt_02_b_positive() {
3175 let request = json!({
3176 "_meta": {
3177 "com.example/future": {"nullIsData": null},
3178 "io.modelcontextprotocol/clientCapabilities": {},
3179 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
3180 "traceparent": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00"
3181 }
3182 });
3183 FinalCommonTypesSchema::validate(CommonWireDirection::Request, &request)
3184 .expect("request metadata schema");
3185 let golden = "{\"_meta\":{\"com.example/future\":{\"nullIsData\":null},\"io.modelcontextprotocol/clientCapabilities\":{},\"io.modelcontextprotocol/protocolVersion\":\"2026-07-28\",\"traceparent\":\"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00\"}}";
3186 assert_eq!(
3187 FinalCommonTypesSchema::canonical_json(&request).expect("canonical JSON"),
3188 golden
3189 );
3190 FinalCommonTypesSchema::validate_golden(CommonWireDirection::Request, &request, golden)
3191 .expect("exact request golden");
3192
3193 let content = ContentBlock::image("aGVsbG8=", "image/png").expect("image content");
3194 let wire = serde_json::to_value(&content).expect("content wire");
3195 FinalCommonTypesSchema::validate(CommonWireDirection::Result, &wire)
3196 .expect("content schema");
3197 assert_eq!(wire["type"], "image");
3198 assert_eq!(
3199 serde_json::from_value::<ContentBlock>(wire).expect("content round trip"),
3200 content
3201 );
3202 assert_eq!(
3203 OpaqueCursor::try_from_presence(Some(String::new()))
3204 .expect("bounded empty cursor")
3205 .as_present(),
3206 Some("")
3207 );
3208 let icon = json!({
3209 "src": "HTTPS://example.test/icon.svg?variant=1",
3210 "sizes": [],
3211 "theme": "dark"
3212 });
3213 let icon = FinalCommonTypesSchema::validate_icon(&icon).expect("icon schema");
3214 assert!(
3215 !icon.effective_any_size(),
3216 "present empty sizes stay present"
3217 );
3218 let cancellation = json!({
3219 "method": "notifications/cancelled",
3220 "params": {"requestId": 9, "reason": "bounded"}
3221 });
3222 FinalCommonTypesSchema::validate(CommonWireDirection::Notification, &cancellation)
3223 .expect("notification-only cancellation");
3224 let bounded_cursor = OpaqueCursor::try_from_presence(Some("x".repeat(MAX_CURSOR_BYTES)))
3225 .expect("cursor at exact bound");
3226 assert_eq!(
3227 bounded_cursor.as_present().map(str::len),
3228 Some(MAX_CURSOR_BYTES)
3229 );
3230 assert_eq!(FinalCommonTypesSchema::FINAL_URI_OWNERS.len(), 12);
3231 }
3232
3233 #[test]
3234 fn final_common_content_bridge_round_trips_complete_resource_link() {
3235 let icon = RawIcon::try_with_details(
3236 "https://example.test/icons/report.svg",
3237 Some("image/svg+xml".to_owned()),
3238 Some(vec!["48x48".to_owned(), "any".to_owned()]),
3239 Some(IconTheme::Dark),
3240 )
3241 .expect("final icon");
3242 let annotations = Annotations {
3243 audience: Some(vec![
3244 AnnotationAudience::User,
3245 AnnotationAudience::Assistant,
3246 ]),
3247 priority: Some(0.75),
3248 last_modified: Some("2026-07-28T15:00:58Z".to_owned()),
3249 additional: BTreeMap::new(),
3250 };
3251 let metadata = OpenMetadata::try_from_entries([(
3252 "com.example/renderHint".to_owned(),
3253 json!({"preserve": true}),
3254 )])
3255 .expect("content metadata");
3256 let resource_link = ResourceLink {
3257 icons: Some(vec![icon.clone()]),
3258 name: "report".to_owned(),
3259 title: Some("Quarterly report".to_owned()),
3260 uri: AbsoluteUri::parse("https://example.test/reports/q3").expect("resource URI"),
3261 description: Some("Raw quarterly figures".to_owned()),
3262 mime_type: Some("text/markdown".to_owned()),
3263 annotations: Some(annotations.clone()),
3264 size: Some(JsonInteger::from(4096_i64)),
3265 meta: Some(metadata.clone()),
3266 additional: BTreeMap::new(),
3267 };
3268 let resource_link_wire = serde_json::to_value(&resource_link).expect("resource link");
3269 assert_eq!(resource_link_wire["type"], "resource_link");
3270 assert_eq!(
3271 resource_link_wire["icons"][0]["sizes"],
3272 json!(["48x48", "any"])
3273 );
3274 assert_eq!(resource_link_wire["icons"][0]["theme"], "dark");
3275 assert_eq!(
3276 resource_link_wire["annotations"]["audience"],
3277 json!(["user", "assistant"])
3278 );
3279 assert_eq!(
3280 resource_link_wire["_meta"]["com.example/renderHint"]["preserve"],
3281 true
3282 );
3283 assert_eq!(
3284 serde_json::from_value::<ResourceLink>(resource_link_wire.clone())
3285 .expect("resource link round trip"),
3286 resource_link
3287 );
3288 FinalCommonTypesSchema::validate(CommonWireDirection::Result, &resource_link_wire)
3289 .expect("final resource link schema");
3290
3291 let content = ContentBlock::ResourceLink {
3292 icons: Some(vec![icon]),
3293 name: "report".to_owned(),
3294 title: Some("Quarterly report".to_owned()),
3295 uri: AbsoluteUri::parse("https://example.test/reports/q3").expect("content URI"),
3296 description: Some("Raw quarterly figures".to_owned()),
3297 mime_type: Some("text/markdown".to_owned()),
3298 annotations: Some(annotations),
3299 size: Some(JsonInteger::from(4096_i64)),
3300 meta: Some(metadata),
3301 additional: BTreeMap::new(),
3302 };
3303 let content_wire = serde_json::to_value(&content).expect("content wire");
3304 assert_eq!(
3305 serde_json::from_value::<ContentBlock>(content_wire).expect("content round trip"),
3306 content
3307 );
3308
3309 for (level, wire) in [
3310 (LoggingLevel::Debug, "debug"),
3311 (LoggingLevel::Info, "info"),
3312 (LoggingLevel::Notice, "notice"),
3313 (LoggingLevel::Warning, "warning"),
3314 (LoggingLevel::Error, "error"),
3315 (LoggingLevel::Critical, "critical"),
3316 (LoggingLevel::Alert, "alert"),
3317 (LoggingLevel::Emergency, "emergency"),
3318 ] {
3319 assert_eq!(serde_json::to_value(level).expect("logging level"), wire);
3320 assert_eq!(
3321 serde_json::from_value::<LoggingLevel>(json!(wire)).expect("logging level"),
3322 level
3323 );
3324 }
3325 }
3326
3327 #[test]
3328 fn final_resource_link_rejects_legacy_icon_sizes_without_mutating_accepted_wire() {
3329 let accepted = json!({
3330 "type": "resource_link",
3331 "icons": [{
3332 "src": "https://example.test/icons/report.svg",
3333 "sizes": ["48x48"],
3334 "theme": "dark"
3335 }],
3336 "name": "report",
3337 "uri": "https://example.test/reports/q3"
3338 });
3339 FinalCommonTypesSchema::validate(CommonWireDirection::Result, &accepted)
3340 .expect("accepted final resource link");
3341 let baseline = accepted.clone();
3342 let mut planted = accepted.clone();
3343 planted["icons"][0]["sizes"] = json!("48x48");
3344 assert_eq!(
3345 FinalCommonTypesSchema::validate(CommonWireDirection::Result, &planted),
3346 Err(CommonTypeError::Invalid("content block"))
3347 );
3348 assert_eq!(
3349 accepted, baseline,
3350 "the rejected one-field legacy size spelling cannot mutate final wire state"
3351 );
3352 }
3353
3354 #[test]
3355 fn final_resource_link_size_is_an_optional_integer() {
3356 let accepted: Value = serde_json::from_str(
3357 r#"{
3358 "type":"resource_link",
3359 "name":"report",
3360 "uri":"https://example.test/reports/q3",
3361 "size":922337203685477580812345678901234567890
3362 }"#,
3363 )
3364 .expect("large integer wire parses");
3365 let resource_link: ResourceLink = serde_json::from_value(accepted.clone())
3366 .expect("integer resource-link size is admitted");
3367 assert_eq!(
3368 resource_link.size.as_ref().map(JsonInteger::as_str),
3369 Some("922337203685477580812345678901234567890")
3370 );
3371 assert_eq!(
3372 serde_json::to_value(&resource_link).expect("integer resource-link size encodes"),
3373 accepted
3374 );
3375
3376 let negative: Value = serde_json::from_str(
3377 r#"{
3378 "type":"resource_link",
3379 "name":"report",
3380 "uri":"https://example.test/reports/q3",
3381 "size":-922337203685477580812345678901234567890
3382 }"#,
3383 )
3384 .expect("large negative integer wire parses");
3385 let negative_link: ResourceLink = serde_json::from_value(negative.clone())
3386 .expect("a schema-integer resource-link size may be negative");
3387 assert_eq!(
3388 negative_link.size.as_ref().map(JsonInteger::as_str),
3389 Some("-922337203685477580812345678901234567890")
3390 );
3391 assert_eq!(
3392 serde_json::to_value(&negative_link)
3393 .expect("negative integer resource-link size encodes"),
3394 negative
3395 );
3396
3397 let negative_content: ContentBlock = serde_json::from_value(negative.clone())
3398 .expect("content resource links preserve schema-integer sizes");
3399 assert_eq!(
3400 serde_json::to_value(&negative_content)
3401 .expect("negative content resource-link size encodes"),
3402 negative
3403 );
3404
3405 let missing = json!({
3406 "type": "resource_link",
3407 "name": "report",
3408 "uri": "https://example.test/reports/q3"
3409 });
3410 let missing_size: ResourceLink =
3411 serde_json::from_value(missing.clone()).expect("resource-link size is optional");
3412 assert_eq!(missing_size.size, None);
3413 assert_eq!(
3414 serde_json::to_value(missing_size).expect("absent size remains absent"),
3415 missing
3416 );
3417
3418 let wrong_type = json!({
3419 "type": "resource_link",
3420 "name": "report",
3421 "uri": "https://example.test/reports/q3",
3422 "size": 4096.5
3423 });
3424 assert!(
3425 serde_json::from_value::<ResourceLink>(wrong_type).is_err(),
3426 "a fractional resource-link size is not an integer"
3427 );
3428 }
3429
3430 #[test]
3431 fn final_common_types_preserve_schema_allowed_additional_properties() {
3432 let implementation = json!({
3433 "name": "FastMCP",
3434 "version": "0.1",
3435 "com.example/implementation": {"stable": true}
3436 });
3437 let implementation: Implementation = serde_json::from_value(implementation.clone())
3438 .expect("schema-allowed implementation property is retained");
3439 assert_eq!(
3440 implementation.additional.get("com.example/implementation"),
3441 Some(&json!({"stable": true}))
3442 );
3443 assert_eq!(
3444 serde_json::to_value(&implementation).expect("implementation property re-emits"),
3445 json!({
3446 "name": "FastMCP",
3447 "version": "0.1",
3448 "com.example/implementation": {"stable": true}
3449 })
3450 );
3451
3452 let resource_link = json!({
3453 "type": "resource_link",
3454 "name": "report",
3455 "uri": "https://example.test/reports/q3",
3456 "com.example/resourceLink": ["preserved"]
3457 });
3458 let resource_link: ResourceLink = serde_json::from_value(resource_link.clone())
3459 .expect("schema-allowed resource-link property is retained");
3460 assert_eq!(
3461 resource_link.additional.get("com.example/resourceLink"),
3462 Some(&json!(["preserved"]))
3463 );
3464 assert_eq!(
3465 serde_json::to_value(&resource_link).expect("resource-link property re-emits"),
3466 json!({
3467 "type": "resource_link",
3468 "name": "report",
3469 "uri": "https://example.test/reports/q3",
3470 "com.example/resourceLink": ["preserved"]
3471 })
3472 );
3473 FinalCommonTypesSchema::validate(
3474 CommonWireDirection::Result,
3475 &serde_json::to_value(&resource_link).expect("resource-link validation wire"),
3476 )
3477 .expect("schema-allowed resource-link property remains valid");
3478
3479 let content = json!({
3480 "type": "text",
3481 "text": "report ready",
3482 "com.example/content": {"priority": "display"}
3483 });
3484 let content: ContentBlock = serde_json::from_value(content.clone())
3485 .expect("schema-allowed content property is retained");
3486 assert_eq!(
3487 serde_json::to_value(&content).expect("content property re-emits"),
3488 json!({
3489 "type": "text",
3490 "text": "report ready",
3491 "com.example/content": {"priority": "display"}
3492 })
3493 );
3494 FinalCommonTypesSchema::validate(
3495 CommonWireDirection::Result,
3496 &serde_json::to_value(&content).expect("content validation wire"),
3497 )
3498 .expect("schema-allowed content property remains valid");
3499 }
3500
3501 #[test]
3502 fn final_common_nested_open_fields_and_subscription_integer_round_trip() {
3503 let resource_link = json!({
3504 "type": "resource_link",
3505 "icons": [{
3506 "src": "https://example.test/icons/report.svg",
3507 "com.example/icon": {"retained": true}
3508 }],
3509 "name": "report",
3510 "uri": "https://example.test/reports/q3",
3511 "annotations": {
3512 "com.example/annotation": ["retained"]
3513 }
3514 });
3515 let resource_link: ResourceLink = serde_json::from_value(resource_link.clone())
3516 .expect("schema-open icon and annotation fields decode");
3517 assert_eq!(
3518 serde_json::to_value(&resource_link).expect("nested extensions re-encode"),
3519 json!({
3520 "type": "resource_link",
3521 "icons": [{
3522 "src": "https://example.test/icons/report.svg",
3523 "com.example/icon": {"retained": true}
3524 }],
3525 "name": "report",
3526 "uri": "https://example.test/reports/q3",
3527 "annotations": {
3528 "com.example/annotation": ["retained"]
3529 }
3530 })
3531 );
3532
3533 let embedded = json!({
3534 "type": "resource",
3535 "resource": {
3536 "uri": "https://example.test/resources/report",
3537 "text": "ready",
3538 "_meta": {"com.example/source": "cache"},
3539 "com.example/resource": {"retained": true}
3540 }
3541 });
3542 let embedded_content: ContentBlock = serde_json::from_value(embedded.clone())
3543 .expect("embedded resource metadata and open fields decode");
3544 assert_eq!(
3545 serde_json::to_value(&embedded_content).expect("embedded resource re-encodes"),
3546 embedded
3547 );
3548
3549 let sampling = json!({
3550 "type": "tool_result",
3551 "toolUseId": "call-7",
3552 "content": [{"type": "text", "text": "done"}],
3553 "com.example/sampling": {"retained": true}
3554 });
3555 let sampling_content: SamplingContentBlock =
3556 serde_json::from_value(sampling.clone()).expect("sampling extension decodes");
3557 assert_eq!(
3558 serde_json::to_value(&sampling_content).expect("sampling extension re-encodes"),
3559 sampling
3560 );
3561
3562 let subscription: Value = serde_json::from_str(
3563 r#"{
3564 "io.modelcontextprotocol/subscriptionId":922337203685477580812345678901234567890
3565 }"#,
3566 )
3567 .expect("large subscription ID wire parses");
3568 let metadata: OpenMetadata = serde_json::from_value(subscription.clone())
3569 .expect("arbitrary-precision subscription ID decodes");
3570 assert_eq!(
3571 serde_json::to_value(metadata).expect("subscription ID re-encodes"),
3572 subscription
3573 );
3574
3575 let cancellation: Value = serde_json::from_str(
3576 r#"{
3577 "method":"notifications/cancelled",
3578 "params":{"requestId":922337203685477580812345678901234567890}
3579 }"#,
3580 )
3581 .expect("large cancellation ID wire parses");
3582 FinalCommonTypesSchema::validate(CommonWireDirection::Notification, &cancellation)
3583 .expect("arbitrary-precision cancellation ID is admitted");
3584 assert!(matches!(
3585 serde_json::from_value::<CancellationRequestId>(
3586 cancellation["params"]["requestId"].clone()
3587 )
3588 .expect("large cancellation ID decodes"),
3589 CancellationRequestId::IntegerExact(value)
3590 if value.as_str() == "922337203685477580812345678901234567890"
3591 ));
3592 }
3593
3594 #[test]
3595 fn prt_02_b_planted_negative() {
3596 let accepted = json!({
3597 "_meta": {
3598 "com.example/future": {"kept": true},
3599 "io.modelcontextprotocol/clientCapabilities": {},
3600 "io.modelcontextprotocol/protocolVersion": "2026-07-28"
3601 }
3602 });
3603 FinalCommonTypesSchema::validate(CommonWireDirection::Request, &accepted)
3604 .expect("accepted baseline");
3605 let baseline = accepted.clone();
3606 let mut planted = accepted.clone();
3607 let meta = planted
3608 .get_mut("_meta")
3609 .and_then(Value::as_object_mut)
3610 .expect("metadata object");
3611 let preserved = meta
3612 .remove("com.example/future")
3613 .expect("one valid open key");
3614 meta.insert("io.modelcontextprotocol/future".to_owned(), preserved);
3615 assert_eq!(
3616 FinalCommonTypesSchema::validate(CommonWireDirection::Request, &planted),
3617 Err(CommonTypeError::Invalid("metadata key"))
3618 );
3619 assert_eq!(
3620 accepted, baseline,
3621 "the one-key rejection cannot mutate retained wire state"
3622 );
3623 }
3624
3625 #[test]
3626 fn cancellation_request_id_preserves_integer_lexemes_and_rejects_fractional_values() {
3627 let accepted: Value = serde_json::from_str(
3628 r#"{"method":"notifications/cancelled","params":{"requestId":-0}}"#,
3629 )
3630 .expect("negative-zero cancellation wire parses");
3631 FinalCommonTypesSchema::validate(CommonWireDirection::Notification, &accepted)
3632 .expect("a schema-valid negative-zero cancellation ID is admitted");
3633 let typed: CancellationRequestId =
3634 serde_json::from_value(accepted["params"]["requestId"].clone())
3635 .expect("negative-zero cancellation ID decodes");
3636 assert!(matches!(&typed, CancellationRequestId::Integer(0)));
3641 assert_eq!(
3642 serde_json::to_value(&typed).expect("negative-zero ID re-encodes"),
3643 serde_json::json!(0),
3644 "a natively representable cancellation ID round-trips by value"
3645 );
3646
3647 let baseline = accepted.clone();
3648 let mut planted = accepted.clone();
3649 planted
3650 .get_mut("params")
3651 .and_then(Value::as_object_mut)
3652 .expect("cancellation parameter object")
3653 .insert(
3654 "requestId".to_owned(),
3655 serde_json::from_str("-0.5").expect("fractional JSON value parses"),
3656 );
3657 assert_eq!(
3658 FinalCommonTypesSchema::validate(CommonWireDirection::Notification, &planted),
3659 Err(CommonTypeError::Invalid("cancellation request ID")),
3660 "changing only requestId to a fractional value rejects cancellation"
3661 );
3662 assert!(
3663 serde_json::from_value::<CancellationRequestId>(planted["params"]["requestId"].clone())
3664 .is_err(),
3665 "typed cancellation ID decoding rejects the same fractional field"
3666 );
3667 assert_eq!(
3668 serde_json::to_value(&typed).expect("accepted ID remains serializable"),
3669 baseline["params"]["requestId"].clone(),
3670 "fractional rejection cannot mutate the admitted cancellation ID"
3671 );
3672 }
3673
3674 #[test]
3675 fn final_sampling_tool_content_round_trips_without_widening_general_content() {
3676 let wire = json!({
3677 "type": "tool_result",
3678 "toolUseId": "call-7",
3679 "content": [{"type": "text", "text": "done"}],
3680 "structuredContent": {"ok": true},
3681 "_meta": {"com.example/cache": "hit"}
3682 });
3683 let content: SamplingContentBlock =
3684 serde_json::from_value(wire.clone()).expect("final tool-result content is admitted");
3685 assert!(matches!(content, SamplingContentBlock::ToolResult { .. }));
3686 assert_eq!(
3687 serde_json::to_value(&content).expect("tool-result re-encodes"),
3688 wire
3689 );
3690
3691 assert!(
3692 serde_json::from_value::<ContentBlock>(wire).is_err(),
3693 "sampling-only tool_result never widens the general content union"
3694 );
3695 }
3696
3697 #[test]
3698 fn final_sampling_tool_result_preserves_absent_and_explicit_null_structured_content() {
3699 let absent_wire = json!({
3700 "type": "tool_result",
3701 "toolUseId": "call-8",
3702 "content": []
3703 });
3704 let absent: SamplingContentBlock =
3705 serde_json::from_value(absent_wire.clone()).expect("absent structuredContent is valid");
3706 assert_eq!(
3707 serde_json::to_value(absent).expect("absent structuredContent re-encodes"),
3708 absent_wire
3709 );
3710
3711 let null_wire = json!({
3712 "type": "tool_result",
3713 "toolUseId": "call-8",
3714 "content": [],
3715 "structuredContent": null
3716 });
3717 let explicit_null: SamplingContentBlock = serde_json::from_value(null_wire.clone())
3718 .expect("explicit-null structuredContent is a present JSON value");
3719 assert!(matches!(
3720 &explicit_null,
3721 SamplingContentBlock::ToolResult {
3722 structured_content: Some(Value::Null),
3723 ..
3724 }
3725 ));
3726 assert_eq!(
3727 serde_json::to_value(explicit_null).expect("explicit null re-encodes"),
3728 null_wire
3729 );
3730 }
3731}