use std::collections::BTreeMap;
use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, ShapeRef, Symbol};
use crate::{McpNativeCard, McpProfile, native_surface_rows};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum McpSurfaceSource {
NativeCard,
SkillCard,
}
impl McpSurfaceSource {
pub fn as_symbol(&self) -> Symbol {
Symbol::new(match self {
Self::NativeCard => "native-card",
Self::SkillCard => "skill-card",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum McpSurfaceRole {
Tool,
Resource,
Prompt,
Model,
}
impl McpSurfaceRole {
pub fn as_symbol(&self) -> Symbol {
Symbol::new(match self {
Self::Tool => "tool",
Self::Resource => "resource",
Self::Prompt => "prompt",
Self::Model => "model",
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum McpStreamPolicy {
#[default]
None,
Progress,
DataStream,
}
impl McpStreamPolicy {
pub fn as_symbol(&self) -> Symbol {
Symbol::new(match self {
Self::None => "none",
Self::Progress => "progress",
Self::DataStream => "data-stream",
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum McpAnnotationVisibility {
Public,
Private,
}
#[derive(Clone, Debug, PartialEq)]
pub struct McpAnnotation {
pub key: Symbol,
pub value: Expr,
pub visibility: McpAnnotationVisibility,
}
impl McpAnnotation {
pub fn public(key: impl Into<Symbol>, value: Expr) -> Self {
Self {
key: key.into(),
value,
visibility: McpAnnotationVisibility::Public,
}
}
pub fn private(key: impl Into<Symbol>, value: Expr) -> Self {
Self {
key: key.into(),
value,
visibility: McpAnnotationVisibility::Private,
}
}
}
#[derive(Clone)]
pub struct McpSurfaceCard {
pub id: String,
pub source: McpSurfaceSource,
pub role: McpSurfaceRole,
pub name: String,
pub symbol: Option<Symbol>,
pub uri: Option<String>,
pub description: String,
pub input_shape: Option<ShapeRef>,
pub output_shape: Option<ShapeRef>,
pub annotations: Vec<(Symbol, Expr)>,
pub capabilities: Vec<CapabilityName>,
pub stream_policy: McpStreamPolicy,
}
impl McpSurfaceCard {
pub fn to_expr(&self, cx: &mut Cx) -> Result<Expr> {
Ok(Expr::Map(vec![
field(
"kind",
Expr::Symbol(Symbol::qualified("mcp", "surface-card")),
),
field("id", Expr::String(self.id.clone())),
field("source", Expr::Symbol(self.source.as_symbol())),
field("role", Expr::Symbol(self.role.as_symbol())),
field("name", Expr::String(self.name.clone())),
field(
"symbol",
self.symbol.clone().map(Expr::Symbol).unwrap_or(Expr::Nil),
),
field(
"uri",
self.uri
.as_ref()
.map(|uri| Expr::String(uri.clone()))
.unwrap_or(Expr::Nil),
),
field("description", Expr::String(self.description.clone())),
field("input-shape", shape_expr(cx, &self.input_shape)?),
field("output-shape", shape_expr(cx, &self.output_shape)?),
field(
"annotations",
Expr::List(
self.annotations
.iter()
.map(|(key, value)| {
Expr::Map(vec![
field("key", Expr::Symbol(key.clone())),
field("value", value.clone()),
])
})
.collect(),
),
),
field(
"capabilities",
Expr::List(
self.capabilities
.iter()
.map(|capability| Expr::String(capability.as_str().to_owned()))
.collect(),
),
),
field(
"stream-policy",
Expr::Symbol(self.stream_policy.as_symbol()),
),
]))
}
}
pub fn project_native_surface(
native_cards: &[McpNativeCard],
profile: &McpProfile,
) -> Result<Vec<McpSurfaceCard>> {
project_surface_rows(native_surface_rows(native_cards)?, profile)
}
pub fn project_mcp_surface(
cx: &mut Cx,
native_cards: &[McpNativeCard],
profile: &McpProfile,
) -> Result<Vec<McpSurfaceCard>> {
#[cfg(not(feature = "skill"))]
let _ = cx;
let rows = native_surface_rows(native_cards)?;
#[cfg(feature = "skill")]
let rows = {
let mut rows = rows;
rows.extend(crate::skill::skill_surface_rows(cx)?);
rows
};
project_surface_rows(rows, profile)
}
pub fn project_surface_rows(
rows: Vec<McpSurfaceCard>,
profile: &McpProfile,
) -> Result<Vec<McpSurfaceCard>> {
let mut by_name = BTreeMap::new();
for row in rows.into_iter().filter(|row| profile.allows(row)) {
if let Some(existing) = by_name.insert(row.name.clone(), row) {
return Err(Error::Eval(format!(
"MCP surface name collision for {}",
existing.name
)));
}
}
Ok(by_name.into_values().collect())
}
pub fn stable_mcp_name(symbol: &Symbol) -> Result<String> {
stable_mcp_name_text(&symbol.to_string())
}
pub(crate) fn stable_mcp_name_text(text: &str) -> Result<String> {
if !text.is_ascii() {
return Err(Error::Eval(format!("MCP name {text} is not ASCII")));
}
let mut output = String::new();
let mut last_was_separator = false;
for ch in text.chars() {
let mapped = match ch {
'A'..='Z' | 'a'..='z' | '0'..='9' | '_' | '-' => ch,
'/' | '.' => '.',
_ => '-',
};
if mapped == '.' || mapped == '-' {
if last_was_separator {
continue;
}
last_was_separator = true;
} else {
last_was_separator = false;
}
output.push(mapped);
}
let output = output.trim_matches(['.', '-']).to_owned();
if output.is_empty() {
return Err(Error::Eval("MCP name cannot be empty".to_owned()));
}
Ok(output)
}
pub(crate) fn public_annotations(annotations: &[McpAnnotation]) -> Vec<(Symbol, Expr)> {
annotations
.iter()
.filter(|annotation| annotation.visibility == McpAnnotationVisibility::Public)
.map(|annotation| (annotation.key.clone(), annotation.value.clone()))
.collect()
}
fn shape_expr(cx: &mut Cx, shape: &Option<ShapeRef>) -> Result<Expr> {
match shape {
Some(shape) => shape.object().as_expr(cx),
None => Ok(Expr::Nil),
}
}
use sim_value::build::entry as field;