Skip to main content

fastmcp_protocol/
jsonrpc.rs

1//! JSON-RPC 2.0 message types.
2
3use std::borrow::Cow;
4use std::collections::BTreeSet;
5
6use serde::de::Error as _;
7use serde::ser::Error as _;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10use serde_json::value::RawValue;
11
12use crate::common_types::JsonInteger;
13
14/// The JSON-RPC version string. Used as a static reference to avoid allocations.
15pub const JSONRPC_VERSION: &str = "2.0";
16
17/// Maximum encoded bytes in one JSON-RPC string ID, including quotes.
18pub const MAX_JSONRPC_STRING_ID_ENCODED_BYTES: usize = 256;
19
20/// Default maximum nesting depth for raw JSON admission.
21pub const MAX_RAW_JSON_NESTING_DEPTH: usize = 64;
22/// Default maximum aggregate object members and array elements for raw JSON admission.
23pub const MAX_RAW_JSON_CONTAINER_ENTRIES: usize = 100_000;
24/// Maximum encoded bytes in one JSON number token before typed decoding.
25pub const MAX_RAW_JSON_NUMBER_BYTES: usize = 4 * 1024;
26/// Maximum aggregate encoded number bytes in one admitted JSON document.
27pub const MAX_RAW_JSON_AGGREGATE_NUMBER_BYTES: usize = 256 * 1024;
28/// Maximum absolute decimal exponent accepted by raw JSON admission.
29pub const MAX_RAW_JSON_EXPONENT: usize = 10_000;
30
31/// A stable reason why raw JSON was rejected before typed decoding.
32#[derive(Clone, Copy, Debug, Eq, PartialEq)]
33pub enum RawJsonAdmissionError {
34    DocumentTooLarge,
35    InvalidUtf8,
36    ByteOrderMark,
37    InvalidSyntax,
38    TopLevelBatch,
39    TopLevelNotObject,
40    DuplicateObjectMember,
41    NestingTooDeep,
42    TooManyContainerEntries,
43    NumberTooLong,
44    TooManyNumberBytes,
45    ExponentTooLarge,
46    TooManyDecodedStringBytes,
47}
48
49impl std::fmt::Display for RawJsonAdmissionError {
50    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        let message = match self {
52            Self::DocumentTooLarge => "JSON document exceeds the configured byte limit",
53            Self::InvalidUtf8 => "JSON document is not strict UTF-8",
54            Self::ByteOrderMark => "JSON document contains a UTF-8 byte-order mark",
55            Self::InvalidSyntax => "invalid JSON syntax during raw admission",
56            Self::TopLevelBatch => "JSON-RPC batch arrays are not supported",
57            Self::TopLevelNotObject => "JSON-RPC top-level value must be an object",
58            Self::DuplicateObjectMember => "duplicate JSON object member",
59            Self::NestingTooDeep => "JSON nesting limit exceeded",
60            Self::TooManyContainerEntries => "JSON container-entry limit exceeded",
61            Self::NumberTooLong => "JSON number-token limit exceeded",
62            Self::TooManyNumberBytes => "aggregate JSON number-byte limit exceeded",
63            Self::ExponentTooLarge => "JSON exponent limit exceeded",
64            Self::TooManyDecodedStringBytes => "decoded JSON string-byte limit exceeded",
65        };
66        formatter.write_str(message)
67    }
68}
69
70impl std::error::Error for RawJsonAdmissionError {}
71
72/// Admission failure for a complete strict JSON-RPC document.
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum JsonRpcAdmissionError {
75    /// The raw JSON boundary rejected the document before typed decoding.
76    Raw(RawJsonAdmissionError),
77    /// The raw document was valid JSON but not a valid JSON-RPC envelope.
78    InvalidEnvelope,
79}
80
81impl std::fmt::Display for JsonRpcAdmissionError {
82    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            Self::Raw(error) => error.fmt(formatter),
85            Self::InvalidEnvelope => formatter.write_str("invalid JSON-RPC envelope"),
86        }
87    }
88}
89
90impl std::error::Error for JsonRpcAdmissionError {}
91
92/// Admit one complete raw JSON-RPC document before any typed decoding.
93///
94/// The caller chooses the document/body byte bound. The fixed structural
95/// limits prevent duplicate-member ambiguity and bound recursive parsing,
96/// decoded string bytes, and numeric lexemes before `serde_json` receives the
97/// document. Only one top-level object is admitted; JSON-RPC batch arrays are
98/// rejected deliberately.
99pub fn admit_raw_jsonrpc_document(
100    bytes: &[u8],
101    document_byte_limit: usize,
102) -> Result<(), RawJsonAdmissionError> {
103    if bytes.len() > document_byte_limit {
104        return Err(RawJsonAdmissionError::DocumentTooLarge);
105    }
106    if bytes.windows(3).any(|window| window == [0xef, 0xbb, 0xbf]) {
107        return Err(RawJsonAdmissionError::ByteOrderMark);
108    }
109    let input = std::str::from_utf8(bytes).map_err(|_| RawJsonAdmissionError::InvalidUtf8)?;
110    let mut scanner = RawJsonScanner::new(input, document_byte_limit);
111    scanner.skip_whitespace();
112    match scanner.peek() {
113        Some(b'{') => scanner.parse_object(0)?,
114        Some(b'[') => return Err(RawJsonAdmissionError::TopLevelBatch),
115        _ => return Err(RawJsonAdmissionError::TopLevelNotObject),
116    }
117    scanner.skip_whitespace();
118    if scanner.position != scanner.bytes.len() {
119        return Err(RawJsonAdmissionError::InvalidSyntax);
120    }
121    Ok(())
122}
123
124/// Decode a complete JSON-RPC message only after raw-document admission.
125pub fn decode_strict_jsonrpc_message(
126    bytes: &[u8],
127    document_byte_limit: usize,
128) -> Result<JsonRpcMessage, JsonRpcAdmissionError> {
129    admit_raw_jsonrpc_document(bytes, document_byte_limit).map_err(JsonRpcAdmissionError::Raw)?;
130    match serde_json::from_slice::<JsonRpcRequest>(bytes) {
131        Ok(request) => Ok(JsonRpcMessage::Request(request)),
132        Err(_) => serde_json::from_slice::<JsonRpcResponse>(bytes)
133            .map(JsonRpcMessage::Response)
134            .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope),
135    }
136}
137
138impl JsonRpcRequest {
139    /// Strictly decodes one request while retaining an exact parameter-source
140    /// sidecar for method-specific ingress decoding.
141    ///
142    /// The returned `String`, when present, is the exact JSON source of the
143    /// request's `params` member. It is deliberately separate from
144    /// [`JsonRpcRequest`] so existing public struct literals and ordinary
145    /// typed request APIs retain their established shape. A consumer may only
146    /// attach this source to the returned request after comparing its
147    /// materialized `params` value; final core dispatch performs that equality
148    /// check before it uses the raw source.
149    ///
150    /// This applies the same bounded raw-document admission as
151    /// [`decode_strict_jsonrpc_message`], including duplicate-member
152    /// rejection, before producing either the typed request or its sidecar.
153    pub fn decode_strict_with_raw_params(
154        bytes: &[u8],
155        document_byte_limit: usize,
156    ) -> Result<(Self, Option<String>), JsonRpcAdmissionError> {
157        admit_raw_jsonrpc_document(bytes, document_byte_limit)
158            .map_err(JsonRpcAdmissionError::Raw)?;
159        let wire =
160            JsonRpcRequestRawWire::deserialize(&mut serde_json::Deserializer::from_slice(bytes))
161                .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)?;
162        let raw_params = wire.params.as_deref().map(RawValue::get).map(str::to_owned);
163        let params = raw_params
164            .as_deref()
165            .map(|source| {
166                crate::messages::validate_raw_final_completion_params(&wire.method, source)
167                    .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)?;
168                serde_json::from_str(source).map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)
169            })
170            .transpose()?;
171        let request = Self {
172            jsonrpc: wire.jsonrpc,
173            method: wire.method,
174            params,
175            id: wire.id,
176        };
177        request
178            .validate()
179            .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)?;
180        Ok((request, raw_params))
181    }
182}
183
184/// One strictly admitted JSON-RPC response paired with the exact source JSON
185/// of its result member.
186///
187/// `raw_result` is absent for an error response and present even when a success
188/// result is the explicit JSON value `null`. It is retained only for local
189/// method-specific result decoding; the public [`JsonRpcResponse`] remains
190/// unchanged and existing typed transport APIs keep their established shape.
191#[derive(Debug, Clone, PartialEq)]
192pub struct JsonRpcResponseAdmission {
193    response: JsonRpcResponse,
194    raw_result: Option<String>,
195}
196
197impl JsonRpcResponseAdmission {
198    /// Returns the ordinary typed response.
199    #[must_use]
200    pub const fn response(&self) -> &JsonRpcResponse {
201        &self.response
202    }
203
204    /// Returns the exact result-member source JSON, including member order and
205    /// number lexemes, when the response is successful.
206    #[must_use]
207    pub fn raw_result(&self) -> Option<&str> {
208        self.raw_result.as_deref()
209    }
210
211    /// Splits this admission into its typed response and exact result source.
212    #[must_use]
213    pub fn into_parts(self) -> (JsonRpcResponse, Option<String>) {
214        (self.response, self.raw_result)
215    }
216}
217
218/// Strictly decodes one JSON-RPC response while retaining its exact result
219/// member source for the final result algebra.
220///
221/// This applies the same bounded raw-document admission as
222/// [`decode_strict_jsonrpc_message`]. Callers that already decoded a frame may
223/// compare the returned typed response with that first decode before attaching
224/// `raw_result` to its correlation owner.
225pub fn decode_strict_jsonrpc_response(
226    bytes: &[u8],
227    document_byte_limit: usize,
228) -> Result<JsonRpcResponseAdmission, JsonRpcAdmissionError> {
229    admit_raw_jsonrpc_document(bytes, document_byte_limit).map_err(JsonRpcAdmissionError::Raw)?;
230    let wire =
231        JsonRpcResponseRawWire::deserialize(&mut serde_json::Deserializer::from_slice(bytes))
232            .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)?;
233    let raw_result = wire.result.map(|result| result.get().to_owned());
234    let result = raw_result
235        .as_deref()
236        .map(serde_json::from_str)
237        .transpose()
238        .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)?;
239    let response = JsonRpcResponse {
240        jsonrpc: wire.jsonrpc,
241        result,
242        error: wire.error,
243        id: wire.id,
244    };
245    response
246        .validate()
247        .map_err(|_| JsonRpcAdmissionError::InvalidEnvelope)?;
248    Ok(JsonRpcResponseAdmission {
249        response,
250        raw_result,
251    })
252}
253
254struct RawJsonScanner<'a> {
255    input: &'a str,
256    bytes: &'a [u8],
257    position: usize,
258    container_entries: usize,
259    number_bytes: usize,
260    decoded_string_bytes: usize,
261    decoded_string_byte_limit: usize,
262}
263
264impl<'a> RawJsonScanner<'a> {
265    fn new(input: &'a str, decoded_string_byte_limit: usize) -> Self {
266        Self {
267            input,
268            bytes: input.as_bytes(),
269            position: 0,
270            container_entries: 0,
271            number_bytes: 0,
272            decoded_string_bytes: 0,
273            decoded_string_byte_limit,
274        }
275    }
276
277    fn parse_value(&mut self, depth: usize) -> Result<(), RawJsonAdmissionError> {
278        match self.peek() {
279            Some(b'{') => self.parse_object(depth),
280            Some(b'[') => self.parse_array(depth),
281            Some(b'"') => self.parse_string(false).map(|_| ()),
282            Some(b't') => self.parse_literal(b"true"),
283            Some(b'f') => self.parse_literal(b"false"),
284            Some(b'n') => self.parse_literal(b"null"),
285            Some(b'-' | b'0'..=b'9') => self.parse_number(),
286            _ => Err(RawJsonAdmissionError::InvalidSyntax),
287        }
288    }
289
290    fn parse_object(&mut self, depth: usize) -> Result<(), RawJsonAdmissionError> {
291        let nested_depth = self.enter_container(depth)?;
292        self.position += 1;
293        self.skip_whitespace();
294        if self.consume(b'}') {
295            return Ok(());
296        }
297
298        let mut names = BTreeSet::new();
299        loop {
300            self.charge_container_entry()?;
301            let name = self
302                .parse_string(true)?
303                .ok_or(RawJsonAdmissionError::InvalidSyntax)?;
304            if !names.insert(name) {
305                return Err(RawJsonAdmissionError::DuplicateObjectMember);
306            }
307            self.skip_whitespace();
308            if !self.consume(b':') {
309                return Err(RawJsonAdmissionError::InvalidSyntax);
310            }
311            self.skip_whitespace();
312            self.parse_value(nested_depth)?;
313            self.skip_whitespace();
314            if self.consume(b'}') {
315                return Ok(());
316            }
317            if !self.consume(b',') {
318                return Err(RawJsonAdmissionError::InvalidSyntax);
319            }
320            self.skip_whitespace();
321        }
322    }
323
324    fn parse_array(&mut self, depth: usize) -> Result<(), RawJsonAdmissionError> {
325        let nested_depth = self.enter_container(depth)?;
326        self.position += 1;
327        self.skip_whitespace();
328        if self.consume(b']') {
329            return Ok(());
330        }
331        loop {
332            self.charge_container_entry()?;
333            self.parse_value(nested_depth)?;
334            self.skip_whitespace();
335            if self.consume(b']') {
336                return Ok(());
337            }
338            if !self.consume(b',') {
339                return Err(RawJsonAdmissionError::InvalidSyntax);
340            }
341            self.skip_whitespace();
342        }
343    }
344
345    fn enter_container(&self, depth: usize) -> Result<usize, RawJsonAdmissionError> {
346        let nested_depth = depth
347            .checked_add(1)
348            .ok_or(RawJsonAdmissionError::NestingTooDeep)?;
349        if nested_depth > MAX_RAW_JSON_NESTING_DEPTH {
350            Err(RawJsonAdmissionError::NestingTooDeep)
351        } else {
352            Ok(nested_depth)
353        }
354    }
355
356    fn charge_container_entry(&mut self) -> Result<(), RawJsonAdmissionError> {
357        self.container_entries = self
358            .container_entries
359            .checked_add(1)
360            .ok_or(RawJsonAdmissionError::TooManyContainerEntries)?;
361        if self.container_entries > MAX_RAW_JSON_CONTAINER_ENTRIES {
362            Err(RawJsonAdmissionError::TooManyContainerEntries)
363        } else {
364            Ok(())
365        }
366    }
367
368    fn parse_string(&mut self, capture: bool) -> Result<Option<String>, RawJsonAdmissionError> {
369        if !self.consume(b'"') {
370            return Err(RawJsonAdmissionError::InvalidSyntax);
371        }
372        let mut decoded = capture.then(String::new);
373        loop {
374            let byte = self.peek().ok_or(RawJsonAdmissionError::InvalidSyntax)?;
375            match byte {
376                b'"' => {
377                    self.position += 1;
378                    return Ok(decoded);
379                }
380                b'\\' => {
381                    self.position += 1;
382                    let character = self.parse_escape()?;
383                    self.charge_string_bytes(character.len_utf8())?;
384                    if let Some(value) = decoded.as_mut() {
385                        value.push(character);
386                    }
387                }
388                0x00..=0x1f => return Err(RawJsonAdmissionError::InvalidSyntax),
389                0x20..=0x7f => {
390                    self.position += 1;
391                    self.charge_string_bytes(1)?;
392                    if let Some(value) = decoded.as_mut() {
393                        value.push(char::from(byte));
394                    }
395                }
396                _ => {
397                    let character = self.input[self.position..]
398                        .chars()
399                        .next()
400                        .ok_or(RawJsonAdmissionError::InvalidSyntax)?;
401                    self.position += character.len_utf8();
402                    self.charge_string_bytes(character.len_utf8())?;
403                    if let Some(value) = decoded.as_mut() {
404                        value.push(character);
405                    }
406                }
407            }
408        }
409    }
410
411    fn parse_escape(&mut self) -> Result<char, RawJsonAdmissionError> {
412        let escape = self.peek().ok_or(RawJsonAdmissionError::InvalidSyntax)?;
413        self.position += 1;
414        match escape {
415            b'"' => Ok('"'),
416            b'\\' => Ok('\\'),
417            b'/' => Ok('/'),
418            b'b' => Ok('\u{0008}'),
419            b'f' => Ok('\u{000c}'),
420            b'n' => Ok('\n'),
421            b'r' => Ok('\r'),
422            b't' => Ok('\t'),
423            b'u' => self.parse_unicode_escape(),
424            _ => Err(RawJsonAdmissionError::InvalidSyntax),
425        }
426    }
427
428    fn parse_unicode_escape(&mut self) -> Result<char, RawJsonAdmissionError> {
429        let first = self.parse_hex_quad()?;
430        let scalar = if (0xd800..=0xdbff).contains(&first) {
431            if !self.consume(b'\\') || !self.consume(b'u') {
432                return Err(RawJsonAdmissionError::InvalidSyntax);
433            }
434            let second = self.parse_hex_quad()?;
435            if !(0xdc00..=0xdfff).contains(&second) {
436                return Err(RawJsonAdmissionError::InvalidSyntax);
437            }
438            0x1_0000 + ((u32::from(first) - 0xd800) << 10) + u32::from(second) - 0xdc00
439        } else if (0xdc00..=0xdfff).contains(&first) {
440            return Err(RawJsonAdmissionError::InvalidSyntax);
441        } else {
442            u32::from(first)
443        };
444        char::from_u32(scalar).ok_or(RawJsonAdmissionError::InvalidSyntax)
445    }
446
447    fn parse_hex_quad(&mut self) -> Result<u16, RawJsonAdmissionError> {
448        let end = self
449            .position
450            .checked_add(4)
451            .ok_or(RawJsonAdmissionError::InvalidSyntax)?;
452        let digits = self
453            .bytes
454            .get(self.position..end)
455            .ok_or(RawJsonAdmissionError::InvalidSyntax)?;
456        let mut value = 0_u16;
457        for digit in digits {
458            let nibble = match digit {
459                b'0'..=b'9' => u16::from(*digit - b'0'),
460                b'a'..=b'f' => u16::from(*digit - b'a' + 10),
461                b'A'..=b'F' => u16::from(*digit - b'A' + 10),
462                _ => return Err(RawJsonAdmissionError::InvalidSyntax),
463            };
464            value = (value << 4) | nibble;
465        }
466        self.position = end;
467        Ok(value)
468    }
469
470    fn parse_literal(&mut self, literal: &[u8]) -> Result<(), RawJsonAdmissionError> {
471        let end = self
472            .position
473            .checked_add(literal.len())
474            .ok_or(RawJsonAdmissionError::InvalidSyntax)?;
475        if self.bytes.get(self.position..end) == Some(literal) {
476            self.position = end;
477            Ok(())
478        } else {
479            Err(RawJsonAdmissionError::InvalidSyntax)
480        }
481    }
482
483    fn parse_number(&mut self) -> Result<(), RawJsonAdmissionError> {
484        let start = self.position;
485        self.consume(b'-');
486        match self.peek() {
487            Some(b'0') => {
488                self.position += 1;
489                if matches!(self.peek(), Some(b'0'..=b'9')) {
490                    return Err(RawJsonAdmissionError::InvalidSyntax);
491                }
492            }
493            Some(b'1'..=b'9') => {
494                self.position += 1;
495                self.consume_digits();
496            }
497            _ => return Err(RawJsonAdmissionError::InvalidSyntax),
498        }
499        if self.consume(b'.') {
500            if !matches!(self.peek(), Some(b'0'..=b'9')) {
501                return Err(RawJsonAdmissionError::InvalidSyntax);
502            }
503            self.consume_digits();
504        }
505        if matches!(self.peek(), Some(b'e' | b'E')) {
506            self.position += 1;
507            if matches!(self.peek(), Some(b'+' | b'-')) {
508                self.position += 1;
509            }
510            let exponent_start = self.position;
511            if !matches!(self.peek(), Some(b'0'..=b'9')) {
512                return Err(RawJsonAdmissionError::InvalidSyntax);
513            }
514            self.consume_digits();
515            if exponent_exceeds_raw_limit(&self.bytes[exponent_start..self.position]) {
516                return Err(RawJsonAdmissionError::ExponentTooLarge);
517            }
518        }
519        let length = self.position - start;
520        if length > MAX_RAW_JSON_NUMBER_BYTES {
521            return Err(RawJsonAdmissionError::NumberTooLong);
522        }
523        self.number_bytes = self
524            .number_bytes
525            .checked_add(length)
526            .ok_or(RawJsonAdmissionError::TooManyNumberBytes)?;
527        if self.number_bytes > MAX_RAW_JSON_AGGREGATE_NUMBER_BYTES {
528            Err(RawJsonAdmissionError::TooManyNumberBytes)
529        } else {
530            Ok(())
531        }
532    }
533
534    fn consume_digits(&mut self) {
535        while matches!(self.peek(), Some(b'0'..=b'9')) {
536            self.position += 1;
537        }
538    }
539    fn charge_string_bytes(&mut self, bytes: usize) -> Result<(), RawJsonAdmissionError> {
540        self.decoded_string_bytes = self
541            .decoded_string_bytes
542            .checked_add(bytes)
543            .ok_or(RawJsonAdmissionError::TooManyDecodedStringBytes)?;
544        if self.decoded_string_bytes > self.decoded_string_byte_limit {
545            Err(RawJsonAdmissionError::TooManyDecodedStringBytes)
546        } else {
547            Ok(())
548        }
549    }
550    fn skip_whitespace(&mut self) {
551        while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
552            self.position += 1;
553        }
554    }
555    fn consume(&mut self, expected: u8) -> bool {
556        if self.peek() == Some(expected) {
557            self.position += 1;
558            true
559        } else {
560            false
561        }
562    }
563    fn peek(&self) -> Option<u8> {
564        self.bytes.get(self.position).copied()
565    }
566}
567
568fn exponent_exceeds_raw_limit(digits: &[u8]) -> bool {
569    let first_significant = digits
570        .iter()
571        .position(|digit| *digit != b'0')
572        .unwrap_or(digits.len());
573    let significant = &digits[first_significant..];
574    significant.len() > 5
575        || significant.iter().fold(0_usize, |value, digit| {
576            value * 10 + usize::from(*digit - b'0')
577        }) > MAX_RAW_JSON_EXPONENT
578}
579
580/// Serializes the jsonrpc version field.
581fn serialize_jsonrpc_version<S>(value: &str, serializer: S) -> Result<S::Ok, S::Error>
582where
583    S: Serializer,
584{
585    if value == JSONRPC_VERSION {
586        serializer.serialize_str(JSONRPC_VERSION)
587    } else {
588        Err(S::Error::custom("jsonrpc must be exactly \"2.0\""))
589    }
590}
591
592/// Deserializes the required JSON-RPC version, rejecting every value but
593/// exactly `"2.0"`.
594fn deserialize_jsonrpc_version<'de, D>(deserializer: D) -> Result<Cow<'static, str>, D::Error>
595where
596    D: Deserializer<'de>,
597{
598    let s: Cow<'de, str> = Cow::deserialize(deserializer)?;
599    if s == JSONRPC_VERSION {
600        Ok(Cow::Borrowed(JSONRPC_VERSION))
601    } else {
602        Err(D::Error::custom("jsonrpc must be exactly \"2.0\""))
603    }
604}
605
606/// JSON-RPC request ID.
607#[derive(Debug, Clone, PartialEq, Eq, Hash)]
608pub enum RequestId {
609    /// Integer ID.
610    Number(i64),
611    /// An arbitrary-precision mathematical-integer ID preserving its admitted
612    /// JSON number lexeme for an exact response echo.
613    Integer(String),
614    /// String ID.
615    String(String),
616}
617
618/// Canonical map/registry key for a JSON-RPC request ID.
619///
620/// Numeric spellings are normalized by exact mathematical value; string IDs
621/// remain byte-for-byte distinct from numeric IDs.
622#[derive(Debug, Clone, PartialEq, Eq, Hash)]
623pub enum CorrelationKey {
624    /// A string request ID, retained byte-for-byte.
625    String(String),
626    /// A canonical decimal mathematical-integer value.
627    Integer(String),
628}
629
630impl RequestId {
631    /// Verifies that this ID can be represented within the JSON-RPC wire
632    /// limits enforced by this crate.
633    ///
634    /// # Errors
635    ///
636    /// Returns an error for a string ID whose canonical JSON encoding exceeds
637    /// [`MAX_JSONRPC_STRING_ID_ENCODED_BYTES`]. Raw decoders must additionally
638    /// enforce the byte length of the received token before escape decoding.
639    pub fn validate(&self) -> Result<(), &'static str> {
640        match self {
641            Self::String(value)
642                if encoded_json_string_len(value) > MAX_JSONRPC_STRING_ID_ENCODED_BYTES =>
643            {
644                return Err("JSON-RPC string id exceeds byte limit");
645            }
646            Self::Integer(lexeme) if !is_mathematical_integer(lexeme) => {
647                return Err("JSON-RPC numeric id must be a mathematical integer");
648            }
649            _ => {}
650        }
651        Ok(())
652    }
653
654    /// Produces the canonical key used by request registries and correlation.
655    pub fn correlation_key(&self) -> Result<CorrelationKey, &'static str> {
656        self.validate()?;
657        match self {
658            Self::Number(value) => Ok(CorrelationKey::Integer(value.to_string())),
659            Self::Integer(lexeme) => Ok(CorrelationKey::Integer(canonical_integer_lexeme(lexeme))),
660            Self::String(value) => Ok(CorrelationKey::String(value.clone())),
661        }
662    }
663
664    /// Returns whether two wire IDs identify the same JSON-RPC request.
665    ///
666    /// Numeric spellings compare by exact mathematical-integer value, while a
667    /// string remains distinct from every numeric ID.
668    #[must_use]
669    pub fn correlates_with(&self, other: &Self) -> bool {
670        matches!(
671            (self.correlation_key(), other.correlation_key()),
672            (Ok(left), Ok(right)) if left == right
673        )
674    }
675}
676
677impl Serialize for RequestId {
678    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
679    where
680        S: Serializer,
681    {
682        self.validate().map_err(S::Error::custom)?;
683        match self {
684            Self::Number(number) => serializer.serialize_i64(*number),
685            Self::Integer(lexeme) => serde_json::from_str::<serde_json::Number>(lexeme)
686                .map_err(S::Error::custom)?
687                .serialize(serializer),
688            Self::String(value) => serializer.serialize_str(value),
689        }
690    }
691}
692
693impl<'de> Deserialize<'de> for RequestId {
694    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
695    where
696        D: Deserializer<'de>,
697    {
698        match Value::deserialize(deserializer)? {
699            Value::Number(number) => {
700                let lexeme = number.to_string();
701                if !lexeme.contains(['.', 'e', 'E'])
702                    && lexeme != "-0"
703                    && let Ok(value) = lexeme.parse::<i64>()
704                {
705                    Ok(RequestId::Number(value))
706                } else if is_mathematical_integer(&lexeme) {
707                    Ok(RequestId::Integer(lexeme))
708                } else {
709                    Err(D::Error::custom(
710                        "JSON-RPC numeric id must be a mathematical integer",
711                    ))
712                }
713            }
714            Value::String(value) => {
715                if encoded_json_string_len(&value) > MAX_JSONRPC_STRING_ID_ENCODED_BYTES {
716                    return Err(D::Error::custom("JSON-RPC string id exceeds byte limit"));
717                }
718                Ok(RequestId::String(value))
719            }
720            _ => Err(D::Error::custom(
721                "JSON-RPC id must be a string or mathematical integer",
722            )),
723        }
724    }
725}
726
727fn is_mathematical_integer(lexeme: &str) -> bool {
728    if lexeme.len() > MAX_RAW_JSON_NUMBER_BYTES {
729        return false;
730    }
731    let bytes = lexeme.as_bytes();
732    let mut index = usize::from(matches!(bytes.first(), Some(b'-')));
733    if index == bytes.len() {
734        return false;
735    }
736    let integer_start = index;
737    if bytes.get(index) == Some(&b'0') {
738        index += 1;
739        if matches!(bytes.get(index), Some(b'0'..=b'9')) {
740            return false;
741        }
742    } else if matches!(bytes.get(index), Some(b'1'..=b'9')) {
743        index += 1;
744        while matches!(bytes.get(index), Some(b'0'..=b'9')) {
745            index += 1;
746        }
747    } else {
748        return false;
749    }
750    let mut fraction_digits = 0_usize;
751    let mut trailing_zeroes = 0_usize;
752    if bytes.get(index) == Some(&b'.') {
753        index += 1;
754        let fraction_start = index;
755        while matches!(bytes.get(index), Some(b'0'..=b'9')) {
756            index += 1;
757        }
758        fraction_digits = index - fraction_start;
759        if fraction_digits == 0 {
760            return false;
761        }
762    }
763    let coefficient_end = index;
764    let coefficient = &bytes[integer_start..coefficient_end];
765    for digit in coefficient.iter().rev() {
766        if *digit == b'0' {
767            trailing_zeroes += 1;
768        } else if *digit != b'.' {
769            break;
770        }
771    }
772    let exponent = if matches!(bytes.get(index), Some(b'e' | b'E')) {
773        index += 1;
774        let negative = if bytes.get(index) == Some(&b'-') {
775            index += 1;
776            true
777        } else {
778            if bytes.get(index) == Some(&b'+') {
779                index += 1;
780            }
781            false
782        };
783        let exponent_start = index;
784        while matches!(bytes.get(index), Some(b'0'..=b'9')) {
785            index += 1;
786        }
787        if index == exponent_start || index != bytes.len() {
788            return false;
789        }
790        let magnitude = std::str::from_utf8(&bytes[exponent_start..index])
791            .ok()
792            .and_then(|value| value.parse::<i64>().ok());
793        match magnitude {
794            Some(value) if value <= MAX_RAW_JSON_EXPONENT as i64 && negative => -value,
795            Some(value) if value <= MAX_RAW_JSON_EXPONENT as i64 => value,
796            _ => return false,
797        }
798    } else {
799        if index != bytes.len() {
800            return false;
801        }
802        0
803    };
804    let scale = i64::try_from(fraction_digits).unwrap_or(i64::MAX) - exponent;
805    scale <= 0
806        || usize::try_from(scale).is_ok_and(|required_zeroes| trailing_zeroes >= required_zeroes)
807}
808
809fn canonical_integer_lexeme(lexeme: &str) -> String {
810    debug_assert!(is_mathematical_integer(lexeme));
811    let bytes = lexeme.as_bytes();
812    let negative = bytes.first() == Some(&b'-');
813    let unsigned = if negative { &lexeme[1..] } else { lexeme };
814    let (coefficient, exponent) = match unsigned.find(['e', 'E']) {
815        Some(index) => (
816            &unsigned[..index],
817            unsigned[index + 1..].parse::<i64>().unwrap_or(0),
818        ),
819        None => (unsigned, 0),
820    };
821    let (whole, fraction) = coefficient.split_once('.').unwrap_or((coefficient, ""));
822    let mut digits = format!("{whole}{fraction}");
823    let leading = digits.bytes().take_while(|digit| *digit == b'0').count();
824    digits.drain(..leading);
825    if digits.is_empty() {
826        return "0".to_owned();
827    }
828    let scale = i64::try_from(fraction.len()).unwrap_or(i64::MAX) - exponent;
829    if scale > 0 {
830        let removable = usize::try_from(scale).unwrap_or(usize::MAX);
831        let retained = digits.len().saturating_sub(removable);
832        digits.truncate(retained);
833    } else {
834        let zeroes = usize::try_from(scale.unsigned_abs()).unwrap_or(usize::MAX);
835        digits.extend(std::iter::repeat_n('0', zeroes));
836    }
837    if negative {
838        format!("-{digits}")
839    } else {
840        digits
841    }
842}
843
844fn encoded_json_string_len(value: &str) -> usize {
845    value.chars().fold(2_usize, |length, character| {
846        let encoded = match character {
847            '"' | '\\' | '\u{0008}' | '\u{000c}' | '\n' | '\r' | '\t' => 2,
848            '\u{0000}'..='\u{001f}' => 6,
849            _ => character.len_utf8(),
850        };
851        length.saturating_add(encoded)
852    })
853}
854
855fn deserialize_request_id<'de, D>(deserializer: D) -> Result<Option<RequestId>, D::Error>
856where
857    D: Deserializer<'de>,
858{
859    RequestId::deserialize(deserializer).map(Some)
860}
861
862impl From<i64> for RequestId {
863    fn from(id: i64) -> Self {
864        RequestId::Number(id)
865    }
866}
867
868impl From<String> for RequestId {
869    fn from(id: String) -> Self {
870        RequestId::String(id)
871    }
872}
873
874impl From<&str> for RequestId {
875    fn from(id: &str) -> Self {
876        RequestId::String(id.to_owned())
877    }
878}
879
880impl std::fmt::Display for RequestId {
881    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
882        match self {
883            RequestId::Number(n) => write!(f, "{n}"),
884            RequestId::Integer(lexeme) => f.write_str(lexeme),
885            RequestId::String(s) => write!(f, "{s}"),
886        }
887    }
888}
889
890/// JSON-RPC 2.0 request.
891#[derive(Debug, Clone, Serialize)]
892#[serde(deny_unknown_fields)]
893pub struct JsonRpcRequest {
894    /// Protocol version (always "2.0").
895    #[serde(
896        serialize_with = "serialize_jsonrpc_version",
897        deserialize_with = "deserialize_jsonrpc_version"
898    )]
899    pub jsonrpc: Cow<'static, str>,
900    /// Method name.
901    pub method: String,
902    /// Request parameters.
903    #[serde(skip_serializing_if = "Option::is_none")]
904    pub params: Option<Value>,
905    /// Request ID (absent for notifications).
906    ///
907    /// An explicit JSON `null` is rejected instead of being conflated with an
908    /// absent member. Notifications omit `id` entirely.
909    #[serde(
910        default,
911        deserialize_with = "deserialize_request_id",
912        skip_serializing_if = "Option::is_none"
913    )]
914    pub id: Option<RequestId>,
915}
916
917#[derive(Deserialize)]
918#[serde(deny_unknown_fields)]
919struct JsonRpcRequestRawWire<'a> {
920    #[serde(deserialize_with = "deserialize_jsonrpc_version")]
921    jsonrpc: Cow<'static, str>,
922    method: String,
923    #[serde(borrow, default)]
924    params: Option<Cow<'a, RawValue>>,
925    #[serde(default, deserialize_with = "deserialize_request_id")]
926    id: Option<RequestId>,
927}
928
929impl<'de> Deserialize<'de> for JsonRpcRequest {
930    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
931    where
932        D: Deserializer<'de>,
933    {
934        let wire = JsonRpcRequestRawWire::deserialize(deserializer)?;
935        let params = wire
936            .params
937            .as_deref()
938            .map(RawValue::get)
939            .map(|source| {
940                crate::messages::validate_raw_final_completion_params(&wire.method, source)
941                    .map_err(D::Error::custom)?;
942                serde_json::from_str(source).map_err(D::Error::custom)
943            })
944            .transpose()?;
945
946        Ok(Self {
947            jsonrpc: wire.jsonrpc,
948            method: wire.method,
949            params,
950            id: wire.id,
951        })
952    }
953}
954
955impl JsonRpcRequest {
956    /// Creates a new request with the given method and parameters.
957    #[must_use]
958    pub fn new(method: impl Into<String>, params: Option<Value>, id: impl Into<RequestId>) -> Self {
959        Self {
960            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
961            method: method.into(),
962            params,
963            id: Some(id.into()),
964        }
965    }
966
967    /// Creates a notification (request without ID).
968    #[must_use]
969    pub fn notification(method: impl Into<String>, params: Option<Value>) -> Self {
970        Self {
971            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
972            method: method.into(),
973            params,
974            id: None,
975        }
976    }
977
978    /// Creates the MCP lifecycle `notifications/initialized` notification.
979    ///
980    /// Uses the spec-correct method name (`notifications/initialized`), avoiding
981    /// the bare `initialized` spelling that compliant servers do not route as the
982    /// lifecycle ack.
983    #[must_use]
984    pub fn initialized_notification() -> Self {
985        Self::notification(crate::methods::NOTIFICATIONS_INITIALIZED, None)
986    }
987
988    /// Returns true if this is a notification (no ID).
989    #[must_use]
990    pub fn is_notification(&self) -> bool {
991        self.id.is_none()
992    }
993
994    /// Verifies invariants that can otherwise be bypassed by constructing or
995    /// mutating this public protocol type directly.
996    ///
997    /// # Errors
998    ///
999    /// Returns an error for a non-standard protocol version or invalid ID.
1000    pub fn validate(&self) -> Result<(), &'static str> {
1001        if self.jsonrpc != JSONRPC_VERSION {
1002            return Err("jsonrpc must be exactly \"2.0\"");
1003        }
1004        if let Some(id) = &self.id {
1005            id.validate()?;
1006        }
1007        Ok(())
1008    }
1009}
1010
1011/// JSON-RPC 2.0 error object.
1012#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1013pub struct JsonRpcError {
1014    /// Error code retained without an implementation-width bound.
1015    pub code: JsonInteger,
1016    /// Error message.
1017    pub message: String,
1018    /// Additional error data.
1019    #[serde(skip_serializing_if = "Option::is_none")]
1020    pub data: Option<Value>,
1021}
1022
1023/// Immutable local endpoint role for raw JSON-RPC ingress disposition.
1024///
1025/// The role is chosen by local transport construction. It is deliberately a
1026/// closed value rather than a peer-provided header/body setting.
1027#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1028pub enum JsonRpcEndpointRole {
1029    /// This endpoint receives client-to-server JSON-RPC traffic.
1030    ServerIngress,
1031    /// This endpoint receives server-to-client JSON-RPC traffic.
1032    ClientIngress,
1033}
1034
1035/// The direction attached by the local transport to a decoded message.
1036#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1037pub enum JsonRpcMessageDirection {
1038    /// Client-to-server traffic.
1039    ClientToServer,
1040    /// Server-to-client traffic.
1041    ServerToClient,
1042}
1043
1044/// Transport ownership for a client-ingress raw protocol failure.
1045#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1046pub enum ClientIngressFailureScope {
1047    /// The malformed body belongs to one request/response exchange.
1048    OwningExchange,
1049    /// The malformed body arrived on a multiplexed/shared channel.
1050    SharedChannel,
1051}
1052
1053/// An error response that is deliberately uncorrelated and omits `id`.
1054///
1055/// It is distinct from [`JsonRpcResponse`], so safe code cannot accidentally
1056/// use an absent ID as an ordinary response correlation key.
1057#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1058#[serde(deny_unknown_fields)]
1059pub struct UncorrelatedJsonRpcErrorResponse {
1060    #[serde(
1061        serialize_with = "serialize_jsonrpc_version",
1062        deserialize_with = "deserialize_jsonrpc_version"
1063    )]
1064    jsonrpc: Cow<'static, str>,
1065    error: JsonRpcError,
1066}
1067
1068impl UncorrelatedJsonRpcErrorResponse {
1069    fn parse_or_invalid_request(message: impl Into<String>, code: i32) -> Self {
1070        Self {
1071            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
1072            error: JsonRpcError {
1073                code: code.into(),
1074                message: message.into(),
1075                data: None,
1076            },
1077        }
1078    }
1079
1080    /// Returns the error payload without exposing an ID-bearing response.
1081    #[must_use]
1082    pub fn error(&self) -> &JsonRpcError {
1083        &self.error
1084    }
1085}
1086
1087/// Role-aware disposition of a raw malformed JSON-RPC document.
1088#[derive(Debug, Clone, PartialEq)]
1089pub enum RawJsonRpcDisposition {
1090    /// Server ingress can emit an error correlated to the one readable ID.
1091    CorrelatedError(JsonRpcResponse),
1092    /// Server ingress can emit an explicitly uncorrelated parse/invalid error.
1093    UncorrelatedError(UncorrelatedJsonRpcErrorResponse),
1094    /// Client ingress emits no JSON-RPC response and fails only its owning exchange.
1095    ClientOwningFailure,
1096    /// Client ingress emits no JSON-RPC response and reports a shared-channel failure.
1097    ClientSharedChannelFailure,
1098    /// The direction is not an ingress path for this endpoint and emits nothing.
1099    NoAction,
1100}
1101
1102/// Convert a raw admission failure into an endpoint-safe disposition.
1103///
1104/// A valid request ID is echoed only at server ingress for client-to-server
1105/// traffic. Client ingress never obtains a response-emitting branch.
1106#[must_use]
1107pub fn dispose_raw_jsonrpc_failure(
1108    role: JsonRpcEndpointRole,
1109    direction: JsonRpcMessageDirection,
1110    readable_id: Option<RequestId>,
1111    failure_scope: ClientIngressFailureScope,
1112) -> RawJsonRpcDisposition {
1113    match (role, direction) {
1114        (JsonRpcEndpointRole::ServerIngress, JsonRpcMessageDirection::ClientToServer) => {
1115            if let Some(id) = readable_id {
1116                RawJsonRpcDisposition::CorrelatedError(JsonRpcResponse::error(
1117                    Some(id),
1118                    JsonRpcError {
1119                        code: (-32600).into(),
1120                        message: "Invalid Request".to_owned(),
1121                        data: None,
1122                    },
1123                ))
1124            } else {
1125                RawJsonRpcDisposition::UncorrelatedError(
1126                    UncorrelatedJsonRpcErrorResponse::parse_or_invalid_request(
1127                        "Parse error",
1128                        -32700,
1129                    ),
1130                )
1131            }
1132        }
1133        (JsonRpcEndpointRole::ClientIngress, JsonRpcMessageDirection::ServerToClient) => {
1134            match failure_scope {
1135                ClientIngressFailureScope::OwningExchange => {
1136                    RawJsonRpcDisposition::ClientOwningFailure
1137                }
1138                ClientIngressFailureScope::SharedChannel => {
1139                    RawJsonRpcDisposition::ClientSharedChannelFailure
1140                }
1141            }
1142        }
1143        _ => RawJsonRpcDisposition::NoAction,
1144    }
1145}
1146
1147impl From<fastmcp_core::McpError> for JsonRpcError {
1148    fn from(err: fastmcp_core::McpError) -> Self {
1149        Self {
1150            code: err.code.into(),
1151            message: err.message,
1152            data: err.data,
1153        }
1154    }
1155}
1156
1157fn deserialize_response_result<'de, D>(deserializer: D) -> Result<Option<Value>, D::Error>
1158where
1159    D: Deserializer<'de>,
1160{
1161    Value::deserialize(deserializer).map(Some)
1162}
1163
1164fn deserialize_response_error<'de, D>(deserializer: D) -> Result<Option<JsonRpcError>, D::Error>
1165where
1166    D: Deserializer<'de>,
1167{
1168    JsonRpcError::deserialize(deserializer).map(Some)
1169}
1170
1171fn deserialize_raw_response_result<'de, D>(
1172    deserializer: D,
1173) -> Result<Option<Box<RawValue>>, D::Error>
1174where
1175    D: Deserializer<'de>,
1176{
1177    Box::<RawValue>::deserialize(deserializer).map(Some)
1178}
1179
1180#[derive(Deserialize)]
1181#[serde(deny_unknown_fields)]
1182struct JsonRpcResponseRawWire {
1183    #[serde(deserialize_with = "deserialize_jsonrpc_version")]
1184    jsonrpc: Cow<'static, str>,
1185    #[serde(default, deserialize_with = "deserialize_raw_response_result")]
1186    result: Option<Box<RawValue>>,
1187    #[serde(default, deserialize_with = "deserialize_response_error")]
1188    error: Option<JsonRpcError>,
1189    #[serde(default, deserialize_with = "deserialize_request_id")]
1190    id: Option<RequestId>,
1191}
1192
1193#[derive(Serialize, Deserialize)]
1194#[serde(deny_unknown_fields)]
1195struct JsonRpcResponseWire {
1196    /// Protocol version (always "2.0").
1197    #[serde(
1198        serialize_with = "serialize_jsonrpc_version",
1199        deserialize_with = "deserialize_jsonrpc_version"
1200    )]
1201    jsonrpc: Cow<'static, str>,
1202    #[serde(
1203        default,
1204        deserialize_with = "deserialize_response_result",
1205        skip_serializing_if = "Option::is_none"
1206    )]
1207    result: Option<Value>,
1208    #[serde(
1209        default,
1210        deserialize_with = "deserialize_response_error",
1211        skip_serializing_if = "Option::is_none"
1212    )]
1213    error: Option<JsonRpcError>,
1214    #[serde(
1215        default,
1216        deserialize_with = "deserialize_request_id",
1217        skip_serializing_if = "Option::is_none"
1218    )]
1219    id: Option<RequestId>,
1220}
1221
1222/// JSON-RPC 2.0 response.
1223#[derive(Debug, Clone, PartialEq)]
1224pub struct JsonRpcResponse {
1225    /// Protocol version (always "2.0").
1226    pub jsonrpc: Cow<'static, str>,
1227    /// Result (present on success, including an explicit JSON `null`).
1228    pub result: Option<Value>,
1229    /// Error (present on failure).
1230    pub error: Option<JsonRpcError>,
1231    /// Request ID this is responding to.
1232    pub id: Option<RequestId>,
1233}
1234
1235impl Serialize for JsonRpcResponse {
1236    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1237    where
1238        S: Serializer,
1239    {
1240        self.validate().map_err(S::Error::custom)?;
1241
1242        JsonRpcResponseWire {
1243            jsonrpc: self.jsonrpc.clone(),
1244            result: self.result.clone(),
1245            error: self.error.clone(),
1246            id: self.id.clone(),
1247        }
1248        .serialize(serializer)
1249    }
1250}
1251
1252impl<'de> Deserialize<'de> for JsonRpcResponse {
1253    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1254    where
1255        D: Deserializer<'de>,
1256    {
1257        let wire = JsonRpcResponseWire::deserialize(deserializer)?;
1258        let response = Self {
1259            jsonrpc: wire.jsonrpc,
1260            result: wire.result,
1261            error: wire.error,
1262            id: wire.id,
1263        };
1264        response.validate().map_err(D::Error::custom)?;
1265        Ok(response)
1266    }
1267}
1268
1269impl JsonRpcResponse {
1270    /// Creates a success response.
1271    #[must_use]
1272    pub fn success(id: RequestId, result: Value) -> Self {
1273        Self {
1274            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
1275            result: Some(result),
1276            error: None,
1277            id: Some(id),
1278        }
1279    }
1280
1281    /// Creates an error response.
1282    #[must_use]
1283    pub fn error(id: Option<RequestId>, error: JsonRpcError) -> Self {
1284        Self {
1285            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
1286            result: None,
1287            error: Some(error),
1288            id,
1289        }
1290    }
1291
1292    /// Returns true if this is an error response.
1293    #[must_use]
1294    pub fn is_error(&self) -> bool {
1295        self.error.is_some()
1296    }
1297
1298    /// Verifies invariants that can otherwise be bypassed by constructing or
1299    /// mutating this public protocol type directly.
1300    ///
1301    /// # Errors
1302    ///
1303    /// Returns an error unless the protocol version is exact, exactly one
1304    /// outcome member is present, every ID is valid, and success is correlated
1305    /// to a request ID.
1306    pub fn validate(&self) -> Result<(), &'static str> {
1307        if self.jsonrpc != JSONRPC_VERSION {
1308            return Err("jsonrpc must be exactly \"2.0\"");
1309        }
1310        if self.result.is_some() == self.error.is_some() {
1311            return Err("JSON-RPC response must contain exactly one of result or error");
1312        }
1313        if self.result.is_some() && self.id.is_none() {
1314            return Err("JSON-RPC success response must contain an id");
1315        }
1316        if let Some(id) = &self.id {
1317            id.validate()?;
1318        }
1319        Ok(())
1320    }
1321}
1322
1323/// A JSON-RPC message (request, response, or notification).
1324#[derive(Debug, Clone, Serialize, Deserialize)]
1325#[serde(untagged)]
1326pub enum JsonRpcMessage {
1327    /// A request or notification.
1328    Request(JsonRpcRequest),
1329    /// A response.
1330    Response(JsonRpcResponse),
1331}
1332
1333impl JsonRpcMessage {
1334    /// Verifies the contained request or response invariants.
1335    ///
1336    /// Typed transports that do not serialize through [`serde_json`] should
1337    /// call this before accepting a message.
1338    ///
1339    /// # Errors
1340    ///
1341    /// Returns the first violated request or response invariant.
1342    pub fn validate(&self) -> Result<(), &'static str> {
1343        match self {
1344            Self::Request(request) => request.validate(),
1345            Self::Response(response) => response.validate(),
1346        }
1347    }
1348}
1349
1350#[cfg(test)]
1351mod tests {
1352    use super::*;
1353    use serde_json::json;
1354
1355    #[derive(Clone, Debug, Default, PartialEq, Eq)]
1356    struct AdmittedFrames {
1357        bytes: Vec<Vec<u8>>,
1358    }
1359
1360    fn admit_frame(
1361        state: &mut AdmittedFrames,
1362        frame: &[u8],
1363    ) -> Result<JsonRpcMessage, JsonRpcAdmissionError> {
1364        let message = decode_strict_jsonrpc_message(frame, 4 * 1024)?;
1365        state.bytes.push(frame.to_vec());
1366        Ok(message)
1367    }
1368
1369    #[test]
1370    fn request_and_response_envelopes_decode() {
1371        let request = br#"{"jsonrpc":"2.0","method":"tools/list","id":42}"#;
1372        let notification = br#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
1373        let success = br#"{"jsonrpc":"2.0","result":null,"id":"request-42"}"#;
1374        let error = br#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"missing"},"id":42}"#;
1375
1376        assert!(matches!(
1377            decode_strict_jsonrpc_message(request, 4 * 1024),
1378            Ok(JsonRpcMessage::Request(JsonRpcRequest {
1379                id: Some(RequestId::Number(42)),
1380                ..
1381            }))
1382        ));
1383        assert!(matches!(
1384            decode_strict_jsonrpc_message(notification, 4 * 1024),
1385            Ok(JsonRpcMessage::Request(JsonRpcRequest { id: None, .. }))
1386        ));
1387        assert!(matches!(
1388            decode_strict_jsonrpc_message(success, 4 * 1024),
1389            Ok(JsonRpcMessage::Response(JsonRpcResponse {
1390                result: Some(Value::Null),
1391                error: None,
1392                ..
1393            }))
1394        ));
1395        assert!(matches!(
1396            decode_strict_jsonrpc_message(error, 4 * 1024),
1397            Ok(JsonRpcMessage::Response(JsonRpcResponse {
1398                result: None,
1399                error: Some(_),
1400                ..
1401            }))
1402        ));
1403
1404        assert!(matches!(
1405            dispose_raw_jsonrpc_failure(
1406                JsonRpcEndpointRole::ServerIngress,
1407                JsonRpcMessageDirection::ClientToServer,
1408                Some(RequestId::String("known".to_owned())),
1409                ClientIngressFailureScope::OwningExchange,
1410            ),
1411            RawJsonRpcDisposition::CorrelatedError(JsonRpcResponse {
1412                id: Some(RequestId::String(_)),
1413                ..
1414            })
1415        ));
1416        assert!(matches!(
1417            dispose_raw_jsonrpc_failure(
1418                JsonRpcEndpointRole::ClientIngress,
1419                JsonRpcMessageDirection::ServerToClient,
1420                None,
1421                ClientIngressFailureScope::SharedChannel,
1422            ),
1423            RawJsonRpcDisposition::ClientSharedChannelFailure
1424        ));
1425    }
1426
1427    #[test]
1428    fn strict_response_admission_retains_exact_result_source() {
1429        let frame = br#"{"jsonrpc":"2.0","result":{"zeta":1.20e+4,"alpha":{"second":2,"first":1},"middle":null},"id":73}"#;
1430        let admission = decode_strict_jsonrpc_response(frame, 4 * 1024)
1431            .expect("strict response admission accepts the bounded frame");
1432        assert_eq!(admission.response().id, Some(RequestId::Number(73)));
1433        assert_eq!(
1434            admission.raw_result(),
1435            Some(r#"{"zeta":1.20e+4,"alpha":{"second":2,"first":1},"middle":null}"#),
1436            "the exact result substring retains top-level and nested member order plus number lexemes"
1437        );
1438
1439        let explicit_null =
1440            decode_strict_jsonrpc_response(br#"{"jsonrpc":"2.0","result":null,"id":74}"#, 4 * 1024)
1441                .expect("an explicit null success remains present");
1442        assert_eq!(explicit_null.raw_result(), Some("null"));
1443
1444        let error = decode_strict_jsonrpc_response(
1445            br#"{"jsonrpc":"2.0","error":{"code":-32601,"message":"missing"},"id":75}"#,
1446            4 * 1024,
1447        )
1448        .expect("an error response has no result source");
1449        assert!(error.raw_result().is_none());
1450    }
1451
1452    #[test]
1453    fn raw_admission_accepts_public_request_surface() {
1454        let frame = br#"{"jsonrpc":"2.0","method":"tools/list","id":"public-request"}"#;
1455        let mut state = AdmittedFrames::default();
1456        let admitted = admit_frame(&mut state, frame)
1457            .expect("the protocol-owned raw gate admits a strict public request envelope");
1458        assert!(matches!(admitted, JsonRpcMessage::Request(_)));
1459        assert_eq!(state.bytes, vec![frame.to_vec()]);
1460        assert!(matches!(
1461            dispose_raw_jsonrpc_failure(
1462                JsonRpcEndpointRole::ClientIngress,
1463                JsonRpcMessageDirection::ServerToClient,
1464                None,
1465                ClientIngressFailureScope::OwningExchange,
1466            ),
1467            RawJsonRpcDisposition::ClientOwningFailure
1468        ));
1469    }
1470
1471    #[test]
1472    fn duplicate_envelope_member_is_rejected_without_state_change() {
1473        let baseline = br#"{"jsonrpc":"2.0","method":"tools/list","id":42}"#;
1474        let planted = br#"{"jsonrpc":"2.0","method":"tools/list","id":42,"id":42}"#;
1475        let mut state = AdmittedFrames::default();
1476        admit_frame(&mut state, baseline).expect("the unmodified envelope is admitted");
1477        let state_before = state.clone();
1478
1479        assert!(
1480            matches!(
1481                admit_frame(&mut state, planted),
1482                Err(JsonRpcAdmissionError::Raw(
1483                    RawJsonAdmissionError::DuplicateObjectMember
1484                ))
1485            ),
1486            "changing only the second id member must reach production raw admission"
1487        );
1488        assert_eq!(
1489            state, state_before,
1490            "rejected raw JSON cannot mutate admitted state"
1491        );
1492    }
1493
1494    #[test]
1495    fn bom_is_rejected_without_state_change() {
1496        let baseline = br#"{"jsonrpc":"2.0","method":"tools/list","id":"public-request"}"#;
1497        let mut planted = baseline.to_vec();
1498        planted.splice(0..0, [0xef, 0xbb, 0xbf]);
1499        let mut state = AdmittedFrames::default();
1500        admit_frame(&mut state, baseline).expect("the baseline is admitted");
1501        let state_before = state.clone();
1502
1503        assert!(
1504            matches!(
1505                admit_frame(&mut state, &planted),
1506                Err(JsonRpcAdmissionError::Raw(
1507                    RawJsonAdmissionError::ByteOrderMark
1508                ))
1509            ),
1510            "inserting only a UTF-8 BOM must reach the typed raw-admission refusal"
1511        );
1512        assert_eq!(
1513            state, state_before,
1514            "rejected raw bytes leave admitted state unchanged"
1515        );
1516    }
1517
1518    #[test]
1519    fn request_id_correlation_key_normalizes_numeric_aliases() {
1520        let numeric = RequestId::Number(1);
1521        let string = RequestId::String("1".to_owned());
1522        assert_ne!(
1523            numeric, string,
1524            "string and numeric request IDs are disjoint"
1525        );
1526        assert_eq!(
1527            numeric.correlation_key().expect("valid numeric ID"),
1528            RequestId::Integer("1.0".to_owned())
1529                .correlation_key()
1530                .expect("valid mathematical integer ID"),
1531            "numeric aliases share one exact mathematical correlation key"
1532        );
1533        assert_eq!(
1534            numeric.correlation_key().expect("valid numeric ID"),
1535            RequestId::Integer("1e0".to_owned())
1536                .correlation_key()
1537                .expect("valid mathematical integer ID"),
1538            "exponent-form integer aliases share one exact mathematical correlation key"
1539        );
1540        assert_ne!(
1541            numeric.correlation_key().expect("valid numeric ID"),
1542            string.correlation_key().expect("valid string ID"),
1543            "a string ID never aliases its numeric spelling"
1544        );
1545        assert!(numeric.correlates_with(&RequestId::Integer("1.0".to_owned())));
1546        assert!(numeric.correlates_with(&RequestId::Integer("1e0".to_owned())));
1547        assert!(!numeric.correlates_with(&string));
1548        assert_eq!(
1549            JsonRpcResponse::success(numeric.clone(), Value::Null).id,
1550            Some(numeric),
1551            "a correlated success preserves its accepted request ID"
1552        );
1553        let large = "922337203685477580812345678901234567890";
1554        let raw = format!(r#"{{"jsonrpc":"2.0","method":"tools/list","id":{large}}}"#);
1555        let decoded = decode_strict_jsonrpc_message(raw.as_bytes(), 4 * 1024)
1556            .expect("an arbitrary-precision mathematical integer is admitted");
1557        let JsonRpcMessage::Request(request) = decoded else {
1558            panic!("the admitted envelope remains a request");
1559        };
1560        assert_eq!(request.id, Some(RequestId::Integer(large.to_owned())));
1561        let echoed = JsonRpcResponse::success(
1562            request
1563                .id
1564                .expect("admitted request keeps its original ID lexeme"),
1565            Value::Null,
1566        );
1567        assert!(
1568            serde_json::to_string(&echoed)
1569                .expect("the exact admitted ID can be echoed")
1570                .contains(large),
1571            "response serialization preserves the accepted arbitrary-precision ID lexeme"
1572        );
1573        assert!(matches!(
1574            dispose_raw_jsonrpc_failure(
1575                JsonRpcEndpointRole::ServerIngress,
1576                JsonRpcMessageDirection::ClientToServer,
1577                Some(RequestId::Number(9)),
1578                ClientIngressFailureScope::OwningExchange,
1579            ),
1580            RawJsonRpcDisposition::CorrelatedError(JsonRpcResponse {
1581                id: Some(RequestId::Number(9)),
1582                ..
1583            })
1584        ));
1585    }
1586
1587    #[test]
1588    fn request_id_fractional_lexeme_is_rejected_before_correlation() {
1589        let baseline = br#"{"jsonrpc":"2.0","method":"tools/list","id":1}"#;
1590        let planted = br#"{"jsonrpc":"2.0","method":"tools/list","id":1.5}"#;
1591        let mut state = AdmittedFrames::default();
1592        admit_frame(&mut state, baseline).expect("integer request ID is admitted");
1593        let state_before = state.clone();
1594
1595        assert!(
1596            matches!(
1597                admit_frame(&mut state, planted),
1598                Err(JsonRpcAdmissionError::InvalidEnvelope)
1599            ),
1600            "changing only the ID to a fractional number must be rejected"
1601        );
1602        assert_eq!(
1603            state, state_before,
1604            "a rejected fractional ID cannot claim a correlation slot"
1605        );
1606    }
1607
1608    #[test]
1609    fn duplicate_nested_member_is_rejected_without_state_change() {
1610        let baseline = br#"{"jsonrpc":"2.0","method":"tools/list","params":{"cursor":"a"}}"#;
1611        let planted =
1612            br#"{"jsonrpc":"2.0","method":"tools/list","params":{"cursor":"a","cursor":"b"}}"#;
1613        let mut state = AdmittedFrames::default();
1614        admit_frame(&mut state, baseline).expect("baseline nested object is admitted");
1615        let state_before = state.clone();
1616
1617        assert!(
1618            matches!(
1619                admit_frame(&mut state, planted),
1620                Err(JsonRpcAdmissionError::Raw(
1621                    RawJsonAdmissionError::DuplicateObjectMember
1622                ))
1623            ),
1624            "a one-member duplicate must fail before typed params decoding"
1625        );
1626        assert_eq!(
1627            state, state_before,
1628            "duplicate raw members cannot mutate admitted state"
1629        );
1630    }
1631
1632    #[test]
1633    fn top_level_batches_are_rejected_without_state_change() {
1634        let baseline = br#"{"jsonrpc":"2.0","method":"tools/list"}"#;
1635        let array_of_one = br#"[{"jsonrpc":"2.0","method":"tools/list"}]"#;
1636        let mixed_array = br#"[{"jsonrpc":"2.0","method":"tools/list"},{"jsonrpc":"2.0","method":"notifications/initialized"}]"#;
1637        let mut state = AdmittedFrames::default();
1638        admit_frame(&mut state, baseline).expect("one top-level request object is admitted");
1639        let state_before = state.clone();
1640
1641        for planted in [array_of_one.as_slice(), mixed_array.as_slice()] {
1642            assert!(
1643                matches!(
1644                    admit_frame(&mut state, planted),
1645                    Err(JsonRpcAdmissionError::Raw(
1646                        RawJsonAdmissionError::TopLevelBatch
1647                    ))
1648                ),
1649                "a top-level batch fails before envelope construction"
1650            );
1651            assert_eq!(
1652                state, state_before,
1653                "rejected batch traffic has no admitted state effect"
1654            );
1655        }
1656    }
1657
1658    // ========================================================================
1659    // RequestId Tests
1660    // ========================================================================
1661
1662    #[test]
1663    fn request_id_number_serialization() {
1664        let id = RequestId::Number(42);
1665        let value = serde_json::to_value(&id).expect("serialize");
1666        assert_eq!(value, 42);
1667    }
1668
1669    #[test]
1670    fn request_id_string_serialization() {
1671        let id = RequestId::String("req-1".to_string());
1672        let value = serde_json::to_value(&id).expect("serialize");
1673        assert_eq!(value, "req-1");
1674    }
1675
1676    #[test]
1677    fn request_id_number_deserialization() {
1678        let id: RequestId = serde_json::from_value(json!(99)).expect("deserialize");
1679        assert_eq!(id, RequestId::Number(99));
1680    }
1681
1682    #[test]
1683    fn request_id_string_deserialization() {
1684        let id: RequestId = serde_json::from_value(json!("abc")).expect("deserialize");
1685        assert_eq!(id, RequestId::String("abc".to_string()));
1686    }
1687
1688    #[test]
1689    fn request_id_string_enforces_encoded_byte_limit() {
1690        let exact = "a".repeat(MAX_JSONRPC_STRING_ID_ENCODED_BYTES - 2);
1691        let too_long = "a".repeat(MAX_JSONRPC_STRING_ID_ENCODED_BYTES - 1);
1692        let exact_json = format!("\"{exact}\"");
1693        let too_long_json = format!("\"{too_long}\"");
1694
1695        assert!(serde_json::from_str::<RequestId>(&exact_json).is_ok());
1696        assert!(serde_json::from_str::<RequestId>(&too_long_json).is_err());
1697        assert!(serde_json::to_string(&RequestId::String(exact)).is_ok());
1698        assert!(serde_json::to_string(&RequestId::String(too_long)).is_err());
1699
1700        let escaped_exact = format!("\"{}\"", "\\u0001".repeat(42));
1701        let escaped_too_long = format!("\"{}\"", "\\u0001".repeat(43));
1702        assert_eq!(escaped_exact.len(), 254);
1703        assert!(serde_json::from_str::<RequestId>(&escaped_exact).is_ok());
1704        assert!(serde_json::from_str::<RequestId>(&escaped_too_long).is_err());
1705    }
1706
1707    #[test]
1708    fn request_id_validation_catches_direct_construction_bypass() {
1709        let too_long = RequestId::String("a".repeat(MAX_JSONRPC_STRING_ID_ENCODED_BYTES));
1710
1711        assert_eq!(
1712            too_long.validate(),
1713            Err("JSON-RPC string id exceeds byte limit")
1714        );
1715    }
1716
1717    #[test]
1718    fn request_rejects_explicit_null_id_but_accepts_absent_id() {
1719        let error = serde_json::from_str::<JsonRpcRequest>(
1720            r#"{"jsonrpc":"2.0","method":"notifications/initialized","id":null}"#,
1721        )
1722        .expect_err("an explicit null id must not become a notification");
1723        assert!(
1724            error
1725                .to_string()
1726                .contains("JSON-RPC id must be a string or mathematical integer")
1727        );
1728
1729        let notification = serde_json::from_str::<JsonRpcRequest>(
1730            r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
1731        )
1732        .expect("an absent id denotes a notification");
1733        assert!(notification.is_notification());
1734    }
1735
1736    #[test]
1737    fn request_id_from_i64() {
1738        let id: RequestId = 7i64.into();
1739        assert_eq!(id, RequestId::Number(7));
1740    }
1741
1742    #[test]
1743    fn request_id_from_string() {
1744        let id: RequestId = "test-id".to_string().into();
1745        assert_eq!(id, RequestId::String("test-id".to_string()));
1746    }
1747
1748    #[test]
1749    fn request_id_from_str() {
1750        let id: RequestId = "test-id".into();
1751        assert_eq!(id, RequestId::String("test-id".to_string()));
1752    }
1753
1754    #[test]
1755    fn request_id_display() {
1756        assert_eq!(format!("{}", RequestId::Number(42)), "42");
1757        assert_eq!(
1758            format!("{}", RequestId::String("req-1".to_string())),
1759            "req-1"
1760        );
1761    }
1762
1763    #[test]
1764    fn request_id_equality() {
1765        assert_eq!(RequestId::Number(1), RequestId::Number(1));
1766        assert_ne!(RequestId::Number(1), RequestId::Number(2));
1767        assert_eq!(
1768            RequestId::String("a".to_string()),
1769            RequestId::String("a".to_string())
1770        );
1771        assert_ne!(RequestId::Number(1), RequestId::String("1".to_string()));
1772    }
1773
1774    // ========================================================================
1775    // JsonRpcRequest Tests
1776    // ========================================================================
1777
1778    #[test]
1779    fn jsonrpc_version_deserialize_borrows_static_for_request() {
1780        let req: JsonRpcRequest =
1781            serde_json::from_str(r#"{"jsonrpc":"2.0","method":"tools/list","id":1}"#)
1782                .expect("deserialize");
1783        assert!(matches!(req.jsonrpc, Cow::Borrowed(JSONRPC_VERSION)));
1784    }
1785
1786    #[test]
1787    fn request_rejects_nonstandard_missing_and_non_string_jsonrpc_versions() {
1788        for input in [
1789            r#"{"jsonrpc":"2.1","method":"tools/list","id":1}"#,
1790            r#"{"jsonrpc":"1.0","method":"tools/list","id":1}"#,
1791            r#"{"jsonrpc":null,"method":"tools/list","id":1}"#,
1792            r#"{"method":"tools/list","id":1}"#,
1793        ] {
1794            let error = serde_json::from_str::<JsonRpcRequest>(input).unwrap_err();
1795            assert!(error.is_data(), "unexpected error for {input}: {error}");
1796        }
1797    }
1798
1799    #[test]
1800    fn request_serialization() {
1801        let req = JsonRpcRequest::new("tools/list", None, 1i64);
1802        let json = serde_json::to_string(&req).unwrap();
1803        assert!(json.contains("\"jsonrpc\":\"2.0\""));
1804        assert!(json.contains("\"method\":\"tools/list\""));
1805        assert!(json.contains("\"id\":1"));
1806    }
1807
1808    #[test]
1809    fn request_with_params() {
1810        let params = json!({"name": "greet", "arguments": {"name": "World"}});
1811        let req = JsonRpcRequest::new("tools/call", Some(params.clone()), 2i64);
1812        let value = serde_json::to_value(&req).expect("serialize");
1813        assert_eq!(value["jsonrpc"], "2.0");
1814        assert_eq!(value["method"], "tools/call");
1815        assert_eq!(value["params"]["name"], "greet");
1816        assert_eq!(value["id"], 2);
1817    }
1818
1819    #[test]
1820    fn strict_request_raw_params_sidecar_preserves_exact_source_and_rejects_duplicates() {
1821        let raw_params = r#"{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}},"name":"weather","inputResponses":{"second":{"roots":[]},"first":{"roots":[]}},"requestState":"retry-1"}"#;
1822        let frame =
1823            format!(r#"{{"jsonrpc":"2.0","method":"tools/call","params":{raw_params},"id":7}}"#);
1824        let (request, sidecar) =
1825            JsonRpcRequest::decode_strict_with_raw_params(frame.as_bytes(), frame.len())
1826                .expect("strict request admission retains the exact parameter source");
1827        assert_eq!(sidecar.as_deref(), Some(raw_params));
1828        assert_eq!(
1829            request.params,
1830            Some(serde_json::from_str(raw_params).expect("raw params materialize")),
1831            "the sidecar belongs to the same admitted typed request"
1832        );
1833
1834        let duplicate = r#"{"jsonrpc":"2.0","method":"tools/call","params":{"inputResponses":{"roots":{"roots":[]},"roots":{"roots":[]}}},"id":7}"#;
1835        assert!(matches!(
1836            JsonRpcRequest::decode_strict_with_raw_params(duplicate.as_bytes(), duplicate.len()),
1837            Err(JsonRpcAdmissionError::Raw(
1838                RawJsonAdmissionError::DuplicateObjectMember
1839            ))
1840        ));
1841    }
1842
1843    #[test]
1844    fn request_without_params_omits_field() {
1845        let req = JsonRpcRequest::new("tools/list", None, 1i64);
1846        let value = serde_json::to_value(&req).expect("serialize");
1847        assert!(value.get("params").is_none());
1848    }
1849
1850    #[test]
1851    fn notification_has_no_id() {
1852        let notif = JsonRpcRequest::notification("notifications/progress", None);
1853        assert!(notif.is_notification());
1854        assert!(notif.id.is_none());
1855        let value = serde_json::to_value(&notif).expect("serialize");
1856        assert!(value.get("id").is_none());
1857    }
1858
1859    #[test]
1860    fn notification_with_params() {
1861        let params = json!({"uri": "file://changed.txt"});
1862        let notif = JsonRpcRequest::notification("notifications/resources/updated", Some(params));
1863        assert!(notif.is_notification());
1864        let value = serde_json::to_value(&notif).expect("serialize");
1865        assert_eq!(value["params"]["uri"], "file://changed.txt");
1866    }
1867
1868    #[test]
1869    fn request_is_not_notification() {
1870        let req = JsonRpcRequest::new("tools/list", None, 1i64);
1871        assert!(!req.is_notification());
1872    }
1873
1874    #[test]
1875    fn request_with_string_id() {
1876        let req = JsonRpcRequest::new("tools/list", None, "req-abc");
1877        let value = serde_json::to_value(&req).expect("serialize");
1878        assert_eq!(value["id"], "req-abc");
1879    }
1880
1881    #[test]
1882    fn request_round_trip() {
1883        let original = JsonRpcRequest::new(
1884            "tools/call",
1885            Some(json!({"name": "add", "arguments": {"a": 1, "b": 2}})),
1886            42i64,
1887        );
1888        let json_str = serde_json::to_string(&original).expect("serialize");
1889        let deserialized: JsonRpcRequest = serde_json::from_str(&json_str).expect("deserialize");
1890        assert_eq!(deserialized.method, "tools/call");
1891        assert_eq!(deserialized.id, Some(RequestId::Number(42)));
1892        assert!(deserialized.params.is_some());
1893    }
1894
1895    #[test]
1896    fn request_rejects_unknown_top_level_envelope_members() {
1897        let error = serde_json::from_str::<JsonRpcRequest>(
1898            r#"{"jsonrpc":"2.0","method":"tools/list","id":1,"extension":true}"#,
1899        )
1900        .expect_err("request envelopes are closed");
1901
1902        assert!(error.to_string().contains("unknown field"));
1903    }
1904
1905    // ========================================================================
1906    // JsonRpcError Tests
1907    // ========================================================================
1908
1909    #[test]
1910    fn jsonrpc_error_from_mcp_error_preserves_code_message_and_data() {
1911        let err = fastmcp_core::McpError::with_data(
1912            fastmcp_core::McpErrorCode::InvalidParams,
1913            "bad params",
1914            json!({"field":"name"}),
1915        );
1916        let rpc_err: JsonRpcError = err.into();
1917        assert_eq!(rpc_err.code.as_i32(), Some(-32602));
1918        assert_eq!(rpc_err.message, "bad params");
1919        assert_eq!(rpc_err.data, Some(json!({"field":"name"})));
1920    }
1921
1922    #[test]
1923    fn jsonrpc_error_serialization() {
1924        let error = JsonRpcError {
1925            code: (-32600).into(),
1926            message: "Invalid Request".to_string(),
1927            data: None,
1928        };
1929        let value = serde_json::to_value(&error).expect("serialize");
1930        assert_eq!(value["code"], -32600);
1931        assert_eq!(value["message"], "Invalid Request");
1932        assert!(value.get("data").is_none());
1933    }
1934
1935    #[test]
1936    fn jsonrpc_error_with_data() {
1937        let error = JsonRpcError {
1938            code: (-32602).into(),
1939            message: "Invalid params".to_string(),
1940            data: Some(json!({"field": "name", "reason": "required"})),
1941        };
1942        let value = serde_json::to_value(&error).expect("serialize");
1943        assert_eq!(value["code"], -32602);
1944        assert_eq!(value["data"]["field"], "name");
1945    }
1946
1947    #[test]
1948    fn jsonrpc_error_preserves_arbitrary_width_integer_code() {
1949        let source = r#"{"code":-340282366920938463463374607431768211457,"message":"unbounded"}"#;
1950        let error: JsonRpcError = serde_json::from_str(source).expect("decode arbitrary code");
1951
1952        assert_eq!(
1953            error.code.as_str(),
1954            "-340282366920938463463374607431768211457"
1955        );
1956        assert_eq!(
1957            serde_json::to_string(&error).expect("re-encode arbitrary code"),
1958            source
1959        );
1960    }
1961
1962    #[test]
1963    fn jsonrpc_error_rejects_nearby_fractional_code() {
1964        let source =
1965            r#"{"code":-340282366920938463463374607431768211457.5,"message":"not integer"}"#;
1966
1967        assert!(serde_json::from_str::<JsonRpcError>(source).is_err());
1968    }
1969
1970    #[test]
1971    fn jsonrpc_error_standard_codes() {
1972        // Parse error
1973        let err = JsonRpcError {
1974            code: (-32700).into(),
1975            message: "Parse error".to_string(),
1976            data: None,
1977        };
1978        assert_eq!(serde_json::to_value(&err).unwrap()["code"], -32700);
1979
1980        // Method not found
1981        let err = JsonRpcError {
1982            code: (-32601).into(),
1983            message: "Method not found".to_string(),
1984            data: None,
1985        };
1986        assert_eq!(serde_json::to_value(&err).unwrap()["code"], -32601);
1987
1988        // Internal error
1989        let err = JsonRpcError {
1990            code: (-32603).into(),
1991            message: "Internal error".to_string(),
1992            data: None,
1993        };
1994        assert_eq!(serde_json::to_value(&err).unwrap()["code"], -32603);
1995    }
1996
1997    // ========================================================================
1998    // JsonRpcResponse Tests
1999    // ========================================================================
2000
2001    #[test]
2002    fn jsonrpc_version_deserialize_borrows_static_for_response() {
2003        let resp: JsonRpcResponse =
2004            serde_json::from_str(r#"{"jsonrpc":"2.0","result":{"tools":[]},"id":1}"#)
2005                .expect("deserialize");
2006        assert!(matches!(resp.jsonrpc, Cow::Borrowed(JSONRPC_VERSION)));
2007    }
2008
2009    #[test]
2010    fn response_rejects_nonstandard_missing_and_non_string_jsonrpc_versions() {
2011        for input in [
2012            r#"{"jsonrpc":"2.1","result":{"tools":[]},"id":1}"#,
2013            r#"{"jsonrpc":"1.0","result":{"tools":[]},"id":1}"#,
2014            r#"{"jsonrpc":null,"result":{"tools":[]},"id":1}"#,
2015            r#"{"result":{"tools":[]},"id":1}"#,
2016        ] {
2017            let error = serde_json::from_str::<JsonRpcResponse>(input).unwrap_err();
2018            assert!(error.is_data(), "unexpected error for {input}: {error}");
2019        }
2020    }
2021
2022    #[test]
2023    fn serialization_rejects_mutated_nonstandard_jsonrpc_version() {
2024        let mut request = JsonRpcRequest::new("tools/list", None, 1_i64);
2025        request.jsonrpc = Cow::Borrowed("2.1");
2026        assert!(serde_json::to_string(&request).is_err());
2027
2028        let mut response = JsonRpcResponse::success(RequestId::Number(1), Value::Null);
2029        response.jsonrpc = Cow::Borrowed("1.0");
2030        assert!(serde_json::to_string(&response).is_err());
2031    }
2032
2033    #[test]
2034    fn response_success() {
2035        let resp = JsonRpcResponse::success(RequestId::Number(1), json!({"result": "ok"}));
2036        let value = serde_json::to_value(&resp).expect("serialize");
2037        assert_eq!(value["jsonrpc"], "2.0");
2038        assert_eq!(value["result"]["result"], "ok");
2039        assert_eq!(value["id"], 1);
2040        assert!(value.get("error").is_none());
2041        assert!(!resp.is_error());
2042    }
2043
2044    #[test]
2045    fn response_error() {
2046        let error = JsonRpcError {
2047            code: (-32601).into(),
2048            message: "Method not found".to_string(),
2049            data: None,
2050        };
2051        let resp = JsonRpcResponse::error(Some(RequestId::Number(1)), error);
2052        let value = serde_json::to_value(&resp).expect("serialize");
2053        assert_eq!(value["jsonrpc"], "2.0");
2054        assert!(value.get("result").is_none());
2055        assert_eq!(value["error"]["code"], -32601);
2056        assert_eq!(value["error"]["message"], "Method not found");
2057        assert_eq!(value["id"], 1);
2058        assert!(resp.is_error());
2059    }
2060
2061    #[test]
2062    fn uncorrelated_response_error_omits_id() {
2063        let error = JsonRpcError {
2064            code: (-32700).into(),
2065            message: "Parse error".to_string(),
2066            data: None,
2067        };
2068        let resp = JsonRpcResponse::error(None, error);
2069        let value = serde_json::to_value(&resp).expect("serialize");
2070        assert!(value.get("id").is_none());
2071    }
2072
2073    #[test]
2074    fn response_rejects_explicit_null_id_but_accepts_absent_id() {
2075        let explicit_null = serde_json::from_str::<JsonRpcResponse>(
2076            r#"{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"},"id":null}"#,
2077        );
2078        assert!(explicit_null.is_err());
2079
2080        let absent = serde_json::from_str::<JsonRpcResponse>(
2081            r#"{"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"}}"#,
2082        )
2083        .expect("an uncorrelated MCP error omits id");
2084        assert!(absent.id.is_none());
2085    }
2086
2087    #[test]
2088    fn response_round_trip() {
2089        let original =
2090            JsonRpcResponse::success(RequestId::String("abc".to_string()), json!({"tools": []}));
2091        let json_str = serde_json::to_string(&original).expect("serialize");
2092        let deserialized: JsonRpcResponse = serde_json::from_str(&json_str).expect("deserialize");
2093        assert!(!deserialized.is_error());
2094        assert!(deserialized.result.is_some());
2095        assert_eq!(deserialized.id, Some(RequestId::String("abc".to_string())));
2096    }
2097
2098    #[test]
2099    fn response_null_result_round_trip_preserves_member_presence() {
2100        let raw = r#"{"jsonrpc":"2.0","result":null,"id":1}"#;
2101        let response: JsonRpcResponse = serde_json::from_str(raw).expect("deserialize response");
2102
2103        assert_eq!(response.result, Some(Value::Null));
2104        assert!(response.error.is_none());
2105
2106        let encoded = serde_json::to_value(response).expect("serialize response");
2107        assert_eq!(encoded.get("result"), Some(&Value::Null));
2108        assert!(encoded.get("error").is_none());
2109    }
2110
2111    #[test]
2112    fn response_rejects_both_or_neither_outcome_members() {
2113        for raw in [
2114            r#"{"jsonrpc":"2.0","result":null,"error":{"code":-32603,"message":"failure"},"id":1}"#,
2115            r#"{"jsonrpc":"2.0","id":1}"#,
2116        ] {
2117            let error = serde_json::from_str::<JsonRpcResponse>(raw)
2118                .expect_err("invalid response envelope must be rejected");
2119            assert!(error.to_string().contains("exactly one"));
2120        }
2121
2122        let both = JsonRpcResponse {
2123            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
2124            result: Some(Value::Null),
2125            error: Some(JsonRpcError {
2126                code: (-32_603).into(),
2127                message: "failure".to_string(),
2128                data: None,
2129            }),
2130            id: Some(RequestId::Number(1)),
2131        };
2132        assert!(serde_json::to_value(both).is_err());
2133
2134        let neither = JsonRpcResponse {
2135            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
2136            result: None,
2137            error: None,
2138            id: Some(RequestId::Number(1)),
2139        };
2140        assert!(serde_json::to_value(neither).is_err());
2141    }
2142
2143    #[test]
2144    fn response_rejects_unknown_top_level_envelope_members() {
2145        let error = serde_json::from_str::<JsonRpcResponse>(
2146            r#"{"jsonrpc":"2.0","result":null,"id":1,"extension":true}"#,
2147        )
2148        .expect_err("response envelopes are closed");
2149
2150        assert!(error.to_string().contains("unknown field"));
2151    }
2152
2153    #[test]
2154    fn response_validation_rejects_uncorrelated_success() {
2155        let response = JsonRpcResponse {
2156            jsonrpc: Cow::Borrowed(JSONRPC_VERSION),
2157            result: Some(Value::Null),
2158            error: None,
2159            id: None,
2160        };
2161
2162        assert_eq!(
2163            response.validate(),
2164            Err("JSON-RPC success response must contain an id")
2165        );
2166        assert!(serde_json::to_value(response).is_err());
2167        assert!(
2168            serde_json::from_str::<JsonRpcResponse>(r#"{"jsonrpc":"2.0","result":null}"#).is_err()
2169        );
2170    }
2171
2172    // ========================================================================
2173    // JsonRpcMessage Tests
2174    // ========================================================================
2175
2176    #[test]
2177    fn message_request_variant() {
2178        let req = JsonRpcRequest::new("tools/list", None, 1i64);
2179        let msg = JsonRpcMessage::Request(req);
2180        let value = serde_json::to_value(&msg).expect("serialize");
2181        assert_eq!(value["method"], "tools/list");
2182    }
2183
2184    #[test]
2185    fn message_response_variant() {
2186        let resp = JsonRpcResponse::success(RequestId::Number(1), json!("ok"));
2187        let msg = JsonRpcMessage::Response(resp);
2188        let value = serde_json::to_value(&msg).expect("serialize");
2189        assert_eq!(value["result"], "ok");
2190    }
2191
2192    #[test]
2193    fn message_deserialize_as_request() {
2194        let json_str = r#"{"jsonrpc":"2.0","method":"tools/list","id":1}"#;
2195        let msg: JsonRpcMessage = serde_json::from_str(json_str).expect("deserialize");
2196        let (method, id) = match msg {
2197            JsonRpcMessage::Request(req) => (req.method, req.id),
2198            JsonRpcMessage::Response(_) => (String::new(), None),
2199        };
2200        assert_eq!(method, "tools/list");
2201        assert_eq!(id, Some(RequestId::Number(1)));
2202    }
2203
2204    #[test]
2205    fn message_deserialize_as_response() {
2206        let json_str = r#"{"jsonrpc":"2.0","result":{"tools":[]},"id":1}"#;
2207        let msg: JsonRpcMessage = serde_json::from_str(json_str).expect("deserialize");
2208        let (is_error, id) = match msg {
2209            JsonRpcMessage::Response(resp) => (resp.is_error(), resp.id),
2210            JsonRpcMessage::Request(_) => (true, None),
2211        };
2212        assert!(!is_error);
2213        assert_eq!(id, Some(RequestId::Number(1)));
2214    }
2215
2216    #[test]
2217    fn message_rejects_mixed_request_and_response_envelopes() {
2218        for raw in [
2219            r#"{"jsonrpc":"2.0","method":"tools/list","result":null,"id":1}"#,
2220            r#"{"jsonrpc":"2.0","params":{},"error":{"code":-32603,"message":"failure"},"id":1}"#,
2221        ] {
2222            assert!(
2223                serde_json::from_str::<JsonRpcMessage>(raw).is_err(),
2224                "mixed envelope was accepted: {raw}"
2225            );
2226        }
2227    }
2228
2229    #[test]
2230    fn message_validation_catches_public_field_mutation() {
2231        let mut request = JsonRpcRequest::new("tools/list", None, 1_i64);
2232        request.jsonrpc = Cow::Borrowed("2.1");
2233        let message = JsonRpcMessage::Request(request);
2234
2235        assert_eq!(message.validate(), Err("jsonrpc must be exactly \"2.0\""));
2236    }
2237
2238    // ========================================================================
2239    // JSONRPC_VERSION constant test
2240    // ========================================================================
2241
2242    #[test]
2243    fn jsonrpc_version_constant() {
2244        assert_eq!(JSONRPC_VERSION, "2.0");
2245    }
2246}