use std::sync::Arc;
use sim_citizen_derive::Citizen;
use sim_kernel::{Cx, Error, Expr, Result, Symbol, Value};
use sim_shape::{AnyShape, parse_shape_expr, shape_value};
use crate::{SkillCard, SkillPolicy, SkillRole, skill_specific_call_capability};
#[derive(Clone, Debug, PartialEq, Citizen)]
#[citizen(symbol = "skill/McpToolDescriptor", version = 1)]
pub struct McpToolDescriptor {
pub name: String,
pub title: Option<String>,
pub description: String,
pub input_schema: Expr,
pub output_schema: Option<Expr>,
}
#[derive(Clone, Debug, PartialEq, Citizen)]
#[citizen(symbol = "skill/McpCallParams", version = 1)]
pub struct McpCallParams {
pub name: String,
pub arguments: Vec<Expr>,
}
#[derive(Clone, Debug, PartialEq, Citizen)]
#[citizen(symbol = "skill/McpToolResult", version = 1)]
pub struct McpToolResult {
pub result: Expr,
pub is_error: bool,
}
impl Default for McpToolDescriptor {
fn default() -> Self {
Self {
name: "citizen-tool".to_owned(),
title: Some("Citizen Tool".to_owned()),
description: "Citizen fixture tool descriptor".to_owned(),
input_schema: Expr::Symbol(Symbol::new("Any")),
output_schema: Some(Expr::Symbol(Symbol::new("Any"))),
}
}
}
impl Default for McpCallParams {
fn default() -> Self {
Self {
name: "citizen-tool".to_owned(),
arguments: vec![Expr::String("fixture".to_owned())],
}
}
}
impl Default for McpToolResult {
fn default() -> Self {
Self {
result: Expr::String("ok".to_owned()),
is_error: false,
}
}
}
impl McpToolDescriptor {
pub fn from_skill_card(cx: &mut Cx, card: &SkillCard) -> Result<Self> {
Ok(Self {
name: card.id.clone(),
title: Some(card.title.clone()),
description: card.description.clone(),
input_schema: parseable_shape_expr(cx, &card.input_shape)?,
output_schema: Some(parseable_shape_expr(cx, &card.output_shape)?),
})
}
pub fn to_skill_card(&self, transport_id: impl Into<String>) -> Result<SkillCard> {
let transport_id = transport_id.into();
let input_shape = parse_shape_expr(&self.input_schema)?;
let output_shape = match &self.output_schema {
Some(expr) => parse_shape_expr(expr)?,
None => Arc::new(AnyShape),
};
Ok(SkillCard {
id: self.name.clone(),
symbol: Symbol::qualified("skill", self.name.clone()),
aliases: Vec::new(),
origin: Symbol::new("mcp-tool"),
title: self.title.clone().unwrap_or_else(|| self.name.clone()),
description: self.description.clone(),
input_shape: shape_value(
Symbol::qualified(format!("skill/{}", self.name), "args"),
input_shape,
),
output_shape: shape_value(
Symbol::qualified(format!("skill/{}", self.name), "result"),
output_shape,
),
roles: vec![SkillRole::Tool],
capabilities: vec![skill_specific_call_capability(&self.name)],
policy: SkillPolicy::default(),
transport_id,
transport_kind: "mcp".to_owned(),
operation: self.name.clone(),
})
}
pub fn to_expr(&self) -> Expr {
let mut fields = vec![
field("kind", Expr::Symbol(Symbol::qualified("mcp", "tool"))),
field("name", Expr::String(self.name.clone())),
field("description", Expr::String(self.description.clone())),
field("inputSchema", self.input_schema.clone()),
];
fields.push(field(
"title",
self.title
.as_ref()
.map(|title| Expr::String(title.clone()))
.unwrap_or(Expr::Nil),
));
fields.push(field(
"outputSchema",
self.output_schema.clone().unwrap_or(Expr::Nil),
));
Expr::Map(fields)
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
let fields = map_fields(expr, "MCP tool descriptor")?;
Ok(Self {
name: required_string(fields, "name")?,
title: optional_string(fields, "title")?,
description: required_string(fields, "description")?,
input_schema: required_field_any(fields, &["inputSchema", "input-schema"])?.clone(),
output_schema: optional_field_any(fields, &["outputSchema", "output-schema"]).cloned(),
})
}
}
impl McpCallParams {
pub fn to_expr(&self) -> Expr {
Expr::Map(vec![
field("kind", Expr::Symbol(Symbol::qualified("mcp", "tools-call"))),
field("name", Expr::String(self.name.clone())),
field("arguments", Expr::List(self.arguments.clone())),
])
}
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: match required_field(fields, "arguments")? {
Expr::List(items) => items.clone(),
_ => {
return Err(Error::TypeMismatch {
expected: "argument list",
found: "non-list",
});
}
},
})
}
pub fn from_value(cx: &mut Cx, value: &Value) -> Result<Self> {
Self::from_expr(&value.object().as_expr(cx)?)
}
}
impl McpToolResult {
pub fn success(cx: &mut Cx, value: Value) -> Result<Self> {
Ok(Self {
result: value.object().as_expr(cx)?,
is_error: false,
})
}
pub fn to_expr(&self) -> Expr {
Expr::Map(vec![
field(
"kind",
Expr::Symbol(Symbol::qualified("mcp", "tool-result")),
),
field("result", self.result.clone()),
field("isError", Expr::Bool(self.is_error)),
])
}
pub fn from_expr(expr: &Expr) -> Result<Self> {
let fields = map_fields(expr, "MCP tool result")?;
Ok(Self {
result: required_field(fields, "result")?.clone(),
is_error: match required_field_any(fields, &["isError", "is-error"])? {
Expr::Bool(value) => *value,
_ => {
return Err(Error::TypeMismatch {
expected: "bool",
found: "non-bool",
});
}
},
})
}
pub fn to_value(&self, cx: &mut Cx) -> Result<Value> {
cx.factory().expr(self.to_expr())
}
}
use sim_value::access::map_entries as map_fields;
fn required_field<'a>(fields: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
required_field_any(fields, &[name])
}
fn required_field_any<'a>(fields: &'a [(Expr, Expr)], names: &[&str]) -> Result<&'a Expr> {
optional_field_any(fields, names)
.ok_or_else(|| Error::Eval(format!("MCP tool descriptor is missing {}", names[0])))
}
fn optional_field_any<'a>(fields: &'a [(Expr, Expr)], names: &[&str]) -> Option<&'a Expr> {
fields.iter().find_map(|(key, value)| {
let name = field_name(key)?;
names.contains(&name.as_str()).then_some(value)
})
}
fn required_string(fields: &[(Expr, Expr)], name: &str) -> Result<String> {
match required_field(fields, name)? {
Expr::String(value) => Ok(value.clone()),
_ => Err(Error::TypeMismatch {
expected: "string",
found: "non-string",
}),
}
}
fn optional_string(fields: &[(Expr, Expr)], name: &str) -> Result<Option<String>> {
match optional_field_any(fields, &[name]) {
Some(Expr::String(value)) => Ok(Some(value.clone())),
Some(Expr::Nil) | None => Ok(None),
Some(_) => Err(Error::TypeMismatch {
expected: "string or nil",
found: "invalid optional string",
}),
}
}
fn field_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Symbol(symbol) if symbol.namespace.is_none() => Some(symbol.name.to_string()),
Expr::String(value) => Some(value.clone()),
_ => None,
}
}
use sim_value::build::entry as field;
fn parseable_shape_expr(cx: &mut Cx, shape: &Value) -> Result<Expr> {
let expr = shape.object().as_expr(cx)?;
if parse_shape_expr(&expr).is_ok() {
Ok(expr)
} else {
Ok(Expr::Symbol(Symbol::new("Any")))
}
}
pub fn mcp_tool_descriptor_class_symbol() -> Symbol {
Symbol::qualified("skill", "McpToolDescriptor")
}
pub fn mcp_call_params_class_symbol() -> Symbol {
Symbol::qualified("skill", "McpCallParams")
}
pub fn mcp_tool_result_class_symbol() -> Symbol {
Symbol::qualified("skill", "McpToolResult")
}