use crate::server::NotificationSink;
use crate::types::{ToolCallResult, ToolDefinition};
use std::collections::BTreeMap;
use std::sync::mpsc;
pub type DispatchFn = fn(
&serde_json::Value,
&mpsc::Receiver<()>,
Option<&NotificationSink>,
Option<serde_json::Value>,
) -> ToolCallResult;
#[derive(Debug)]
pub struct McpToolEntry {
pub name: &'static str,
pub definition_fn: fn() -> ToolDefinition,
pub dispatch_fn: DispatchFn,
}
inventory::collect!(McpToolEntry);
#[derive(Debug)]
pub struct ToolIndex {
definitions: Vec<ToolDefinition>,
dispatch: BTreeMap<&'static str, DispatchFn>,
}
impl ToolIndex {
#[must_use]
pub fn from_inventory() -> Self {
let mut by_name: BTreeMap<&'static str, &'static McpToolEntry> = BTreeMap::new();
for entry in inventory::iter::<McpToolEntry> {
if let Some(prior) = by_name.insert(entry.name, entry) {
by_name.insert(prior.name, prior);
panic!(
"FALSIFY-INVENTORY-002: duplicate MCP tool name {:?} registered twice in the \
inventory. Two `register_mcp_tool!` invocations advertise the same name; \
pick one. Existing: {:p}, duplicate: {:p}.",
entry.name, prior, entry,
);
}
}
let mut definitions: Vec<ToolDefinition> =
by_name.values().map(|e| (e.definition_fn)()).collect();
definitions.sort_by(|a, b| a.name.cmp(&b.name));
let dispatch: BTreeMap<&'static str, DispatchFn> = by_name
.iter()
.map(|(name, entry)| (*name, entry.dispatch_fn))
.collect();
Self {
definitions,
dispatch,
}
}
#[must_use]
pub fn definitions(&self) -> &[ToolDefinition] {
&self.definitions
}
#[must_use]
pub fn dispatch_for(&self, name: &str) -> Option<&DispatchFn> {
self.dispatch.get(name)
}
#[must_use]
pub fn names(&self) -> Vec<&'static str> {
self.dispatch.keys().copied().collect()
}
}
#[macro_export]
macro_rules! register_mcp_tool {
(name: $name:expr, definition: $def:path, dispatch: $dispatch:path $(,)?) => {
::inventory::submit! {
$crate::tools::registry::McpToolEntry {
name: $name,
definition_fn: $def,
dispatch_fn: $dispatch,
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn live_inventory_yields_phase_one_tool_set() {
let index = ToolIndex::from_inventory();
let names = index.names();
let expected = [
"apr.bench",
"apr.finetune",
"apr.qa",
"apr.run",
"apr.serve",
"apr.tensors",
"apr.trace",
"apr.validate",
"apr.version",
];
assert_eq!(names, expected);
assert_eq!(index.definitions().len(), 9);
}
#[test]
fn dispatch_for_known_tool_returns_some() {
let index = ToolIndex::from_inventory();
assert!(index.dispatch_for("apr.version").is_some());
assert!(index.dispatch_for("apr.qa").is_some());
}
#[test]
fn dispatch_for_unknown_tool_returns_none() {
let index = ToolIndex::from_inventory();
assert!(index.dispatch_for("apr.never").is_none());
}
}