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