use serde_json::Value as JsonValue;
use crate::EmbeddedDatabase;
use super::tools::ToolOutcome;
pub type McpToolHandler = fn(db: Option<&EmbeddedDatabase>, args: JsonValue) -> ToolOutcome;
pub type McpToolSchema = fn() -> JsonValue;
pub struct McpExtensionTool {
pub name: &'static str,
pub description: &'static str,
pub schema: McpToolSchema,
pub handler: McpToolHandler,
}
inventory::collect!(McpExtensionTool);
pub fn registered() -> impl Iterator<Item = &'static McpExtensionTool> {
inventory::iter::<McpExtensionTool>.into_iter()
}
pub fn try_call(
db: Option<&EmbeddedDatabase>,
name: &str,
args: JsonValue,
) -> Option<ToolOutcome> {
for entry in registered() {
if entry.name == name {
return Some((entry.handler)(db, args));
}
}
None
}
#[macro_export]
macro_rules! mcp_tool {
(
name: $name:literal,
description: $desc:literal,
schema: $schema:expr,
handler: $handler:expr $(,)?
) => {
inventory::submit! {
$crate::mcp::auto_register::McpExtensionTool {
name: $name,
description: $desc,
schema: $schema,
handler: $handler,
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn dummy(_: Option<&EmbeddedDatabase>, _: JsonValue) -> ToolOutcome {
ToolOutcome::ok(json!({ "ok": true }))
}
inventory::submit! {
McpExtensionTool {
name: "helios_test_dummy",
description: "test fixture",
schema: || json!({ "type": "object", "properties": {} }),
handler: dummy,
}
}
#[test]
fn inventory_includes_dummy() {
let names: Vec<_> = registered().map(|t| t.name).collect();
assert!(names.contains(&"helios_test_dummy"), "have: {names:?}");
}
#[test]
fn try_call_dispatches() {
let r = try_call(None, "helios_test_dummy", json!({})).expect("matched");
assert!(!r.is_error);
}
#[test]
fn try_call_misses_unknown() {
assert!(try_call(None, "definitely_not_a_tool", json!({})).is_none());
}
}