sim-lib-mcp 0.1.0-rc.1

Library-only MCP surface projection for SIM.
Documentation
use sim_kernel::{Cx, Error, Expr, Result, Value};

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct McpCallParams {
    pub name: String,
    pub arguments: Vec<Expr>,
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) struct McpToolResult {
    pub content: Vec<Expr>,
    pub is_error: bool,
}

impl McpCallParams {
    pub fn from_expr(expr: &Expr) -> Result<Self> {
        let fields = map_fields(expr, "MCP tools/call params")?;
        Ok(Self {
            name: required_string(fields, "name")?,
            arguments: optional_arguments(fields)?,
        })
    }
}

impl McpToolResult {
    pub fn success(cx: &mut Cx, value: Value) -> Result<Self> {
        Ok(Self {
            content: vec![value_to_content(cx, value)?],
            is_error: false,
        })
    }

    pub fn error(message: impl Into<String>) -> Self {
        Self {
            content: vec![text_content(message.into())],
            is_error: true,
        }
    }

    pub fn to_expr(&self) -> Expr {
        Expr::Map(vec![
            field("content", Expr::List(self.content.clone())),
            field("isError", Expr::Bool(self.is_error)),
        ])
    }
}

pub(crate) fn arguments_to_values(cx: &mut Cx, arguments: &[Expr]) -> Result<Vec<Value>> {
    arguments
        .iter()
        .cloned()
        .map(|expr| expr_to_value(cx, expr))
        .collect()
}

fn expr_to_value(cx: &mut Cx, expr: Expr) -> Result<Value> {
    match expr {
        Expr::Nil => cx.factory().nil(),
        Expr::Bool(value) => cx.factory().bool(value),
        Expr::Number(number) => cx.factory().number_literal(number.domain, number.canonical),
        Expr::String(value) => cx.factory().string(value),
        Expr::Symbol(value) => cx.factory().symbol(value),
        other => cx.factory().expr(other),
    }
}

pub(crate) fn value_to_content(cx: &mut Cx, value: Value) -> Result<Expr> {
    let expr = value.object().as_expr(cx)?;
    Ok(match &expr {
        Expr::String(text) => text_content(text.clone()),
        Expr::Symbol(symbol) => text_content(symbol.to_string()),
        _ if is_json_safe(&expr) => json_content(expr),
        other => text_content(format!("{other:?}")),
    })
}

fn is_json_safe(expr: &Expr) -> bool {
    match expr {
        Expr::Nil | Expr::Bool(_) | Expr::Number(_) | Expr::String(_) => true,
        Expr::List(items) | Expr::Vector(items) | Expr::Set(items) => {
            items.iter().all(is_json_safe)
        }
        Expr::Map(fields) => fields
            .iter()
            .all(|(key, value)| json_key_safe(key) && is_json_safe(value)),
        _ => false,
    }
}

fn json_key_safe(expr: &Expr) -> bool {
    matches!(expr, Expr::String(_))
}

pub(crate) fn json_content(expr: Expr) -> Expr {
    Expr::Map(vec![
        field("type", Expr::String("json".to_owned())),
        field("json", expr),
    ])
}

pub(crate) fn text_content(text: String) -> Expr {
    Expr::Map(vec![
        field("type", Expr::String("text".to_owned())),
        field("text", Expr::String(text)),
    ])
}

fn optional_arguments(fields: &[(Expr, Expr)]) -> Result<Vec<Expr>> {
    match optional_field(fields, "arguments") {
        Some(Expr::List(items)) => Ok(items.clone()),
        Some(Expr::Nil) | None => Ok(Vec::new()),
        Some(Expr::Map(fields)) => Ok(vec![Expr::Map(fields.clone())]),
        Some(_) => Err(Error::TypeMismatch {
            expected: "argument list, map, or nil",
            found: "invalid arguments",
        }),
    }
}

use sim_value::access::map_entries as map_fields;

fn required_string(fields: &[(Expr, Expr)], name: &str) -> Result<String> {
    match optional_field(fields, name) {
        Some(Expr::String(value)) => Ok(value.clone()),
        Some(_) => Err(Error::TypeMismatch {
            expected: "string",
            found: "non-string",
        }),
        None => Err(Error::TypeMismatch {
            expected: "required tools/call field",
            found: "missing field",
        }),
    }
}

fn optional_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
    fields.iter().find_map(|(key, value)| {
        let key = match key {
            Expr::Symbol(symbol) if symbol.namespace.is_none() => symbol.name.as_ref(),
            Expr::String(text) => text.as_str(),
            _ => return None,
        };
        (key == name).then_some(value)
    })
}

pub(crate) use sim_value::build::entry as field;