Skip to main content

sim_codec_mcp/
envelope.rs

1//! The MCP envelope model: the `McpEnvelope` enum and its request,
2//! notification, response, and error payload structs, with their stable class
3//! symbols.
4
5use sim_citizen::CitizenField;
6use sim_citizen_derive::Citizen;
7use sim_kernel::Expr;
8use sim_kernel::{Result, Symbol};
9
10/// One MCP JSON-RPC 2.0 envelope: the typed payload carried by a single frame.
11///
12/// The four variants are the complete envelope shapes the `codec:mcp` codec
13/// round-trips; any other JSON structure is rejected.
14#[derive(Clone, Debug, PartialEq)]
15pub enum McpEnvelope {
16    /// A method call with an `id` expecting a response.
17    Request(McpRequest),
18    /// A method call without an `id`, expecting no response.
19    Notification(McpNotification),
20    /// A successful result for a prior request `id`.
21    Response(McpResponse),
22    /// A failure for a prior request `id`.
23    Error(McpErrorEnvelope),
24}
25
26/// A JSON-RPC request: an addressed method call expecting a response.
27#[derive(Clone, Debug, PartialEq, Citizen)]
28#[citizen(symbol = "mcp/Request", version = 1)]
29pub struct McpRequest {
30    /// The correlation id (string, number, or nil) the response must echo.
31    pub id: Expr,
32    /// The method name being invoked.
33    pub method: String,
34    /// The method parameters.
35    pub params: Expr,
36}
37
38/// A JSON-RPC notification: a method call with no `id` and no response.
39#[derive(Clone, Debug, PartialEq, Citizen)]
40#[citizen(symbol = "mcp/Notification", version = 1)]
41pub struct McpNotification {
42    /// The method name being notified.
43    pub method: String,
44    /// The method parameters.
45    pub params: Expr,
46}
47
48/// A JSON-RPC successful response for a prior request.
49#[derive(Clone, Debug, PartialEq, Citizen)]
50#[citizen(symbol = "mcp/Response", version = 1)]
51pub struct McpResponse {
52    /// The request id this response answers.
53    pub id: Expr,
54    /// The successful result payload.
55    pub result: Expr,
56}
57
58/// A JSON-RPC error response for a prior request.
59#[derive(Clone, Debug, PartialEq, Citizen)]
60#[citizen(symbol = "mcp/ErrorEnvelope", version = 1)]
61pub struct McpErrorEnvelope {
62    /// The request id this error answers.
63    pub id: Expr,
64    /// The error detail.
65    pub error: McpError,
66}
67
68/// The error object inside an [`McpErrorEnvelope`].
69#[derive(Clone, Debug, PartialEq, Citizen)]
70#[citizen(symbol = "mcp/Error", version = 1)]
71pub struct McpError {
72    /// The numeric error code (see the constants in this crate, e.g.
73    /// [`INTERNAL_ERROR`](crate::INTERNAL_ERROR)).
74    pub code: i64,
75    /// A human-readable error message.
76    pub message: String,
77    /// Optional structured error data.
78    pub data: Expr,
79}
80
81impl Default for McpRequest {
82    fn default() -> Self {
83        Self {
84            id: Expr::String("fixture".to_owned()),
85            method: "tools/list".to_owned(),
86            params: Expr::Map(Vec::new()),
87        }
88    }
89}
90
91impl Default for McpNotification {
92    fn default() -> Self {
93        Self {
94            method: "notifications/initialized".to_owned(),
95            params: Expr::Map(Vec::new()),
96        }
97    }
98}
99
100impl Default for McpResponse {
101    fn default() -> Self {
102        Self {
103            id: Expr::String("fixture".to_owned()),
104            result: Expr::Map(Vec::new()),
105        }
106    }
107}
108
109impl Default for McpErrorEnvelope {
110    fn default() -> Self {
111        Self {
112            id: Expr::String("fixture".to_owned()),
113            error: McpError::default(),
114        }
115    }
116}
117
118impl Default for McpError {
119    fn default() -> Self {
120        Self {
121            code: -32603,
122            message: "fixture error".to_owned(),
123            data: Expr::Nil,
124        }
125    }
126}
127
128impl CitizenField for McpError {
129    fn encode_field(&self) -> Expr {
130        Expr::List(vec![
131            self.code.encode_field(),
132            self.message.encode_field(),
133            self.data.encode_field(),
134        ])
135    }
136
137    fn decode_field_expr(expr: &Expr, field: &'static str) -> Result<Self> {
138        let Expr::List(items) = expr else {
139            return Err(sim_citizen::field_error(field, "expected MCP error list"));
140        };
141        let [code, message, data] = items.as_slice() else {
142            return Err(sim_citizen::field_error(
143                field,
144                format!("expected 3 MCP error field(s), found {}", items.len()),
145            ));
146        };
147        Ok(Self {
148            code: i64::decode_field_expr(code, field)?,
149            message: String::decode_field_expr(message, field)?,
150            data: Expr::decode_field_expr(data, field)?,
151        })
152    }
153}
154
155/// The stable class symbol `mcp/Request` for [`McpRequest`].
156pub fn mcp_request_class_symbol() -> Symbol {
157    Symbol::qualified("mcp", "Request")
158}
159
160/// The stable class symbol `mcp/Notification` for [`McpNotification`].
161pub fn mcp_notification_class_symbol() -> Symbol {
162    Symbol::qualified("mcp", "Notification")
163}
164
165/// The stable class symbol `mcp/Response` for [`McpResponse`].
166pub fn mcp_response_class_symbol() -> Symbol {
167    Symbol::qualified("mcp", "Response")
168}
169
170/// The stable class symbol `mcp/ErrorEnvelope` for [`McpErrorEnvelope`].
171pub fn mcp_error_envelope_class_symbol() -> Symbol {
172    Symbol::qualified("mcp", "ErrorEnvelope")
173}
174
175/// The stable class symbol `mcp/Error` for [`McpError`].
176pub fn mcp_error_class_symbol() -> Symbol {
177    Symbol::qualified("mcp", "Error")
178}
179
180pub(crate) fn is_jsonrpc_id(expr: &Expr) -> bool {
181    matches!(expr, Expr::String(_) | Expr::Number(_) | Expr::Nil)
182}