1use std::collections::BTreeSet;
6
7use sim_kernel::{Error, Expr, NumberLiteral, Result, Symbol};
8
9use crate::envelope::{
10 McpEnvelope, McpError, McpErrorEnvelope, McpNotification, McpRequest, McpResponse,
11 is_jsonrpc_id,
12};
13
14const MCP_VERSION: &str = "2.0";
15
16pub fn envelope_to_expr(envelope: &McpEnvelope) -> Expr {
30 match envelope {
31 McpEnvelope::Request(request) => Expr::Map(vec![
32 field("mcp", Expr::String(MCP_VERSION.to_owned())),
33 field("id", request.id.clone()),
34 field("method", Expr::String(request.method.clone())),
35 field("params", request.params.clone()),
36 ]),
37 McpEnvelope::Notification(notification) => Expr::Map(vec![
38 field("mcp", Expr::String(MCP_VERSION.to_owned())),
39 field("method", Expr::String(notification.method.clone())),
40 field("params", notification.params.clone()),
41 ]),
42 McpEnvelope::Response(response) => Expr::Map(vec![
43 field("mcp", Expr::String(MCP_VERSION.to_owned())),
44 field("id", response.id.clone()),
45 field("result", response.result.clone()),
46 ]),
47 McpEnvelope::Error(error) => Expr::Map(vec![
48 field("mcp", Expr::String(MCP_VERSION.to_owned())),
49 field("id", error.id.clone()),
50 field(
51 "error",
52 Expr::Map(vec![
53 field("code", error_code_expr(error.error.code)),
54 field("message", Expr::String(error.error.message.clone())),
55 field("data", error.error.data.clone()),
56 ]),
57 ),
58 ]),
59 }
60}
61
62pub fn expr_to_envelope(expr: &Expr) -> Result<McpEnvelope> {
77 let fields = map_fields(expr, "MCP envelope")?;
78 reject_unknown(
79 fields,
80 &["mcp", "id", "method", "params", "result", "error"],
81 )?;
82 require_version(fields)?;
83
84 let has_id = optional_field(fields, "id").is_some();
85 let has_method = optional_field(fields, "method").is_some();
86 let has_result = optional_field(fields, "result").is_some();
87 let has_error = optional_field(fields, "error").is_some();
88
89 match (has_method, has_id, has_result, has_error) {
90 (true, true, false, false) => request_from_fields(fields),
91 (true, false, false, false) => notification_from_fields(fields),
92 (false, true, true, false) => response_from_fields(fields),
93 (false, true, false, true) => error_from_fields(fields),
94 _ => Err(Error::Eval(
95 "invalid MCP JSON-RPC envelope field combination".to_owned(),
96 )),
97 }
98}
99
100fn request_from_fields(fields: &[(Expr, Expr)]) -> Result<McpEnvelope> {
101 reject_unknown(fields, &["mcp", "id", "method", "params"])?;
102 let id = required_id(fields)?;
103 let method = required_string(fields, "method")?;
104 let params = required_field(fields, "params")?.clone();
105 Ok(McpEnvelope::Request(McpRequest { id, method, params }))
106}
107
108fn notification_from_fields(fields: &[(Expr, Expr)]) -> Result<McpEnvelope> {
109 reject_unknown(fields, &["mcp", "method", "params"])?;
110 let method = required_string(fields, "method")?;
111 let params = required_field(fields, "params")?.clone();
112 Ok(McpEnvelope::Notification(McpNotification {
113 method,
114 params,
115 }))
116}
117
118fn response_from_fields(fields: &[(Expr, Expr)]) -> Result<McpEnvelope> {
119 reject_unknown(fields, &["mcp", "id", "result"])?;
120 let id = required_id(fields)?;
121 let result = required_field(fields, "result")?.clone();
122 Ok(McpEnvelope::Response(McpResponse { id, result }))
123}
124
125fn error_from_fields(fields: &[(Expr, Expr)]) -> Result<McpEnvelope> {
126 reject_unknown(fields, &["mcp", "id", "error"])?;
127 let id = required_id(fields)?;
128 let error = error_object(required_field(fields, "error")?)?;
129 Ok(McpEnvelope::Error(McpErrorEnvelope { id, error }))
130}
131
132fn error_object(expr: &Expr) -> Result<McpError> {
133 let fields = map_fields(expr, "MCP error object")?;
134 reject_unknown(fields, &["code", "message", "data"])?;
135 Ok(McpError {
136 code: required_i64(fields, "code")?,
137 message: required_string(fields, "message")?,
138 data: required_field(fields, "data")?.clone(),
139 })
140}
141
142fn require_version(fields: &[(Expr, Expr)]) -> Result<()> {
143 match required_field(fields, "mcp")? {
144 Expr::String(version) if version == MCP_VERSION => Ok(()),
145 _ => Err(Error::Eval(
146 "MCP envelope must declare :mcp \"2.0\"".to_owned(),
147 )),
148 }
149}
150
151fn required_id(fields: &[(Expr, Expr)]) -> Result<Expr> {
152 let id = required_field(fields, "id")?.clone();
153 if is_jsonrpc_id(&id) {
154 Ok(id)
155 } else {
156 Err(Error::TypeMismatch {
157 expected: "JSON-RPC id string, number, or nil",
158 found: "invalid id",
159 })
160 }
161}
162
163fn required_i64(fields: &[(Expr, Expr)], name: &str) -> Result<i64> {
164 match required_field(fields, name)? {
165 Expr::Number(number) => number
166 .canonical
167 .parse::<i64>()
168 .map_err(|_| Error::TypeMismatch {
169 expected: "integer error code",
170 found: "non-integer number",
171 }),
172 _ => Err(Error::TypeMismatch {
173 expected: "integer error code",
174 found: "non-number",
175 }),
176 }
177}
178
179fn required_string(fields: &[(Expr, Expr)], name: &str) -> Result<String> {
180 match required_field(fields, name)? {
181 Expr::String(value) => Ok(value.clone()),
182 _ => Err(Error::TypeMismatch {
183 expected: "string",
184 found: "non-string",
185 }),
186 }
187}
188
189fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
190 optional_field(fields, name)
191 .ok_or_else(|| Error::Eval(format!("MCP envelope is missing {name}")))
192}
193
194fn optional_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
195 fields
196 .iter()
197 .find_map(|(key, value)| (field_name(key).ok()?.as_str() == name).then_some(value))
198}
199
200use sim_value::access::map_entries as map_fields;
201
202fn reject_unknown(fields: &[(Expr, Expr)], allowed: &[&str]) -> Result<()> {
203 let mut seen = BTreeSet::new();
204 for (key, _) in fields {
205 let name = field_name(key)?;
206 if !seen.insert(name.clone()) {
207 return Err(Error::Eval(format!("duplicate MCP envelope field {name}")));
208 }
209 if !allowed.contains(&name.as_str()) {
210 return Err(Error::Eval(format!("unknown MCP envelope field {name}")));
211 }
212 }
213 Ok(())
214}
215
216fn field_name(expr: &Expr) -> Result<String> {
217 match expr {
218 Expr::Symbol(symbol) if symbol.namespace.is_none() => Ok(symbol.name.to_string()),
219 Expr::String(value) => Ok(value.clone()),
220 _ => Err(Error::TypeMismatch {
221 expected: "MCP envelope field symbol",
222 found: "invalid field key",
223 }),
224 }
225}
226
227fn field(name: &str, value: Expr) -> (Expr, Expr) {
228 sim_value::build::entry(name, value)
229}
230
231pub(crate) fn error_code_expr(code: i64) -> Expr {
232 Expr::Number(NumberLiteral {
233 domain: Symbol::qualified("numbers", "i64"),
234 canonical: code.to_string(),
235 })
236}