1use std::collections::BTreeMap;
2
3use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, ShapeRef, Symbol};
4
5use crate::{McpNativeCard, McpProfile, native_surface_rows};
6
7#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum McpSurfaceSource {
10 NativeCard,
12 SkillCard,
14}
15
16impl McpSurfaceSource {
17 pub fn as_symbol(&self) -> Symbol {
19 Symbol::new(match self {
20 Self::NativeCard => "native-card",
21 Self::SkillCard => "skill-card",
22 })
23 }
24}
25
26#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
28pub enum McpSurfaceRole {
29 Tool,
31 Resource,
33 Prompt,
35 Model,
37}
38
39impl McpSurfaceRole {
40 pub fn as_symbol(&self) -> Symbol {
42 Symbol::new(match self {
43 Self::Tool => "tool",
44 Self::Resource => "resource",
45 Self::Prompt => "prompt",
46 Self::Model => "model",
47 })
48 }
49}
50
51#[derive(Clone, Debug, Default, PartialEq, Eq)]
53pub enum McpStreamPolicy {
54 #[default]
56 None,
57 Progress,
59 DataStream,
61}
62
63impl McpStreamPolicy {
64 pub fn as_symbol(&self) -> Symbol {
66 Symbol::new(match self {
67 Self::None => "none",
68 Self::Progress => "progress",
69 Self::DataStream => "data-stream",
70 })
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
76pub enum McpAnnotationVisibility {
77 Public,
79 Private,
81}
82
83#[derive(Clone, Debug, PartialEq)]
85pub struct McpAnnotation {
86 pub key: Symbol,
88 pub value: Expr,
90 pub visibility: McpAnnotationVisibility,
92}
93
94impl McpAnnotation {
95 pub fn public(key: impl Into<Symbol>, value: Expr) -> Self {
97 Self {
98 key: key.into(),
99 value,
100 visibility: McpAnnotationVisibility::Public,
101 }
102 }
103
104 pub fn private(key: impl Into<Symbol>, value: Expr) -> Self {
106 Self {
107 key: key.into(),
108 value,
109 visibility: McpAnnotationVisibility::Private,
110 }
111 }
112}
113
114#[derive(Clone)]
116pub struct McpSurfaceCard {
117 pub id: String,
119 pub source: McpSurfaceSource,
121 pub role: McpSurfaceRole,
123 pub name: String,
125 pub symbol: Option<Symbol>,
127 pub uri: Option<String>,
129 pub description: String,
131 pub input_shape: Option<ShapeRef>,
133 pub output_shape: Option<ShapeRef>,
135 pub annotations: Vec<(Symbol, Expr)>,
137 pub capabilities: Vec<CapabilityName>,
139 pub stream_policy: McpStreamPolicy,
141}
142
143impl McpSurfaceCard {
144 pub fn to_expr(&self, cx: &mut Cx) -> Result<Expr> {
146 Ok(Expr::Map(vec![
147 field(
148 "kind",
149 Expr::Symbol(Symbol::qualified("mcp", "surface-card")),
150 ),
151 field("id", Expr::String(self.id.clone())),
152 field("source", Expr::Symbol(self.source.as_symbol())),
153 field("role", Expr::Symbol(self.role.as_symbol())),
154 field("name", Expr::String(self.name.clone())),
155 field(
156 "symbol",
157 self.symbol.clone().map(Expr::Symbol).unwrap_or(Expr::Nil),
158 ),
159 field(
160 "uri",
161 self.uri
162 .as_ref()
163 .map(|uri| Expr::String(uri.clone()))
164 .unwrap_or(Expr::Nil),
165 ),
166 field("description", Expr::String(self.description.clone())),
167 field("input-shape", shape_expr(cx, &self.input_shape)?),
168 field("output-shape", shape_expr(cx, &self.output_shape)?),
169 field(
170 "annotations",
171 Expr::List(
172 self.annotations
173 .iter()
174 .map(|(key, value)| {
175 Expr::Map(vec![
176 field("key", Expr::Symbol(key.clone())),
177 field("value", value.clone()),
178 ])
179 })
180 .collect(),
181 ),
182 ),
183 field(
184 "capabilities",
185 Expr::List(
186 self.capabilities
187 .iter()
188 .map(|capability| Expr::String(capability.as_str().to_owned()))
189 .collect(),
190 ),
191 ),
192 field(
193 "stream-policy",
194 Expr::Symbol(self.stream_policy.as_symbol()),
195 ),
196 ]))
197 }
198}
199
200pub fn project_native_surface(
202 native_cards: &[McpNativeCard],
203 profile: &McpProfile,
204) -> Result<Vec<McpSurfaceCard>> {
205 project_surface_rows(native_surface_rows(native_cards)?, profile)
206}
207
208pub fn project_mcp_surface(
211 cx: &mut Cx,
212 native_cards: &[McpNativeCard],
213 profile: &McpProfile,
214) -> Result<Vec<McpSurfaceCard>> {
215 #[cfg(not(feature = "skill"))]
216 let _ = cx;
217
218 let rows = native_surface_rows(native_cards)?;
219 #[cfg(feature = "skill")]
220 let rows = {
221 let mut rows = rows;
222 rows.extend(crate::skill::skill_surface_rows(cx)?);
223 rows
224 };
225 project_surface_rows(rows, profile)
226}
227
228pub fn project_surface_rows(
231 rows: Vec<McpSurfaceCard>,
232 profile: &McpProfile,
233) -> Result<Vec<McpSurfaceCard>> {
234 let mut by_name = BTreeMap::new();
235 for row in rows.into_iter().filter(|row| profile.allows(row)) {
236 if let Some(existing) = by_name.insert(row.name.clone(), row) {
237 return Err(Error::Eval(format!(
238 "MCP surface name collision for {}",
239 existing.name
240 )));
241 }
242 }
243 Ok(by_name.into_values().collect())
244}
245
246pub fn stable_mcp_name(symbol: &Symbol) -> Result<String> {
248 stable_mcp_name_text(&symbol.to_string())
249}
250
251pub(crate) fn stable_mcp_name_text(text: &str) -> Result<String> {
252 if !text.is_ascii() {
253 return Err(Error::Eval(format!("MCP name {text} is not ASCII")));
254 }
255 let mut output = String::new();
256 let mut last_was_separator = false;
257 for ch in text.chars() {
258 let mapped = match ch {
259 'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' => ch,
260 '/' | '.' => '.',
261 _ => '-',
262 };
263 if mapped == '.' || mapped == '-' {
264 if last_was_separator {
265 continue;
266 }
267 last_was_separator = true;
268 } else {
269 last_was_separator = false;
270 }
271 output.push(mapped);
272 }
273 let output = output.trim_matches(['.', '-']).to_owned();
274 if output.is_empty() {
275 return Err(Error::Eval("MCP name cannot be empty".to_owned()));
276 }
277 Ok(output)
278}
279
280pub(crate) fn public_annotations(annotations: &[McpAnnotation]) -> Vec<(Symbol, Expr)> {
281 annotations
282 .iter()
283 .filter(|annotation| annotation.visibility == McpAnnotationVisibility::Public)
284 .map(|annotation| (annotation.key.clone(), annotation.value.clone()))
285 .collect()
286}
287
288fn shape_expr(cx: &mut Cx, shape: &Option<ShapeRef>) -> Result<Expr> {
289 match shape {
290 Some(shape) => shape.object().as_expr(cx),
291 None => Ok(Expr::Nil),
292 }
293}
294
295use sim_value::build::entry as field;