sim-lib-mcp 0.1.0

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

use crate::content::field;

const NOT_FOUND_PREFIX: &str = "mcp.not-found|";

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum McpResourceUriKind {
    Sim,
    Skill,
    Unsupported,
}

pub(crate) fn resource_uri_kind(uri: &str) -> McpResourceUriKind {
    if uri.starts_with("sim://") {
        McpResourceUriKind::Sim
    } else if uri.starts_with("skill://") {
        McpResourceUriKind::Skill
    } else {
        McpResourceUriKind::Unsupported
    }
}

pub(crate) fn not_found_error(kind: &str, id: impl AsRef<str>) -> Error {
    Error::Eval(format!("{NOT_FOUND_PREFIX}{kind}|{}", id.as_ref()))
}

pub(crate) fn not_found_error_data(error: &Error) -> Option<Expr> {
    let Error::Eval(message) = error else {
        return None;
    };
    let rest = message.strip_prefix(NOT_FOUND_PREFIX)?;
    let (kind, id) = rest.split_once('|')?;
    Some(Expr::Map(vec![
        field("code", Expr::String("not-found".to_owned())),
        field("kind", Expr::String(kind.to_owned())),
        field("id", Expr::String(id.to_owned())),
        field("message", Expr::String(format!("{kind} not found: {id}"))),
    ]))
}

pub(crate) 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) fn required_string_field(fields: &[(Expr, Expr)], name: &str) -> Result<String, Error> {
    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 MCP field",
            found: "missing field",
        }),
    }
}