use crate::tool::metadata::ToolExposure;
use crate::tool::{ToolDefinition, ToolKind};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolRegistry {
pub tools: HashMap<String, RegisteredTool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RegisteredTool {
pub definition: ToolDefinition,
pub exposure: ToolExposure,
#[serde(default)]
pub phases: Vec<String>,
}
pub const MCP_TOOL_DEFER_THRESHOLD: usize = 100;
impl ToolRegistry {
pub fn new() -> Self {
Self {
tools: HashMap::new(),
}
}
pub fn register(&mut self, definition: ToolDefinition) {
let exposure = definition.metadata.exposure;
self.tools.insert(
definition.name.clone(),
RegisteredTool {
definition,
exposure,
phases: Vec::new(), },
);
}
pub fn register_with(
&mut self,
definition: ToolDefinition,
exposure: ToolExposure,
phases: Vec<String>,
) {
self.tools.insert(
definition.name.clone(),
RegisteredTool {
definition,
exposure,
phases,
},
);
}
pub fn visible_definitions(&self) -> Vec<ToolDefinition> {
let mut defs: Vec<ToolDefinition> = self
.tools
.values()
.filter(|t| matches!(t.exposure, ToolExposure::Direct | ToolExposure::ModelOnly))
.map(|t| t.definition.clone())
.collect();
defs.sort_by(|a, b| a.name.cmp(&b.name));
defs
}
pub fn visible_tool_names(&self) -> Vec<String> {
self.visible_definitions()
.into_iter()
.map(|d| d.name)
.collect()
}
pub fn for_phase(&self, phase: &str) -> Vec<ToolDefinition> {
self.tools
.values()
.filter(|t| t.exposure == ToolExposure::Direct)
.filter(|t| t.phases.is_empty() || t.phases.iter().any(|p| p == phase))
.map(|t| t.definition.clone())
.collect()
}
pub fn names(&self) -> Vec<String> {
let mut names: Vec<String> = self.tools.keys().cloned().collect();
names.sort();
names
}
pub fn unregister_prefix(&mut self, prefix: &str) {
self.tools.retain(|name, _| !name.starts_with(prefix));
}
pub fn retain_tools<F>(&mut self, mut pred: F)
where
F: FnMut(&str) -> bool,
{
self.tools.retain(|name, _| pred(name));
}
pub fn clear(&mut self) {
self.tools.clear();
}
pub fn get(&self, name: &str) -> Option<&RegisteredTool> {
self.tools.get(name)
}
pub fn deferred_definitions(&self) -> Vec<ToolDefinition> {
self.tools
.values()
.filter(|t| t.exposure == ToolExposure::Deferred)
.map(|t| t.definition.clone())
.collect()
}
pub fn search(&self, query: &str, max_results: usize) -> Vec<ToolDefinition> {
let query = query.to_lowercase();
let query_terms: Vec<&str> = query.split_whitespace().collect();
if query_terms.is_empty() || max_results == 0 {
return Vec::new();
}
let mut scored: Vec<(i32, &ToolDefinition)> = self
.tools
.values()
.filter(|t| {
matches!(
t.exposure,
ToolExposure::Direct | ToolExposure::Deferred | ToolExposure::ModelOnly
)
})
.map(|t| {
let def = &t.definition;
let score = compute_search_score(def, &query_terms);
(score, def)
})
.filter(|(score, _)| *score > 0)
.collect();
scored.sort_by(|(left_score, left), (right_score, right)| {
right_score
.cmp(left_score)
.then_with(|| left.name.cmp(&right.name))
});
scored
.into_iter()
.take(max_results)
.map(|(_, def)| def.clone())
.collect()
}
}
impl Default for ToolRegistry {
fn default() -> Self {
Self::new()
}
}
fn compute_search_score(def: &ToolDefinition, terms: &[&str]) -> i32 {
let mut score = 0i32;
for term in terms {
if def.name.to_lowercase() == *term {
score += 100;
continue;
}
if def.name.to_lowercase().contains(term) {
score += 50;
}
if def.description.to_lowercase().contains(term) {
score += 20;
}
if def.metadata.namespace.to_lowercase().contains(term) {
score += 18;
}
for tag in &def.metadata.tags {
if tag.to_lowercase().contains(term) {
score += 15;
}
}
for cap in &def.metadata.capabilities {
if cap.to_lowercase().contains(term) {
score += 10;
}
}
for example in &def.metadata.examples {
if example.to_string().to_lowercase().contains(term) {
score += 6;
}
}
let kind_str = match def.kind {
ToolKind::Read => "read",
ToolKind::Write => "write",
ToolKind::Command => "command",
ToolKind::Custom => "custom",
};
if kind_str.contains(term) {
score += 5;
}
}
score
}
#[derive(Debug, Clone)]
pub struct ToolSet {
pub phase: String,
pub definitions: Vec<ToolDefinition>,
}
impl ToolSet {
pub fn for_phase(registry: &ToolRegistry, phase: &str) -> Self {
Self {
phase: phase.to_string(),
definitions: registry.for_phase(phase),
}
}
pub fn names(&self) -> Vec<String> {
self.definitions.iter().map(|d| d.name.clone()).collect()
}
}
pub mod phases {
pub const PLANNING: &str = "planning";
pub const READING: &str = "reading";
pub const EDITING: &str = "editing";
pub const VERIFYING: &str = "verifying";
pub const REVIEWING: &str = "reviewing";
pub const RECOVERY: &str = "recovery";
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tool::ToolMetadata;
use serde_json::json;
fn make_def(name: &str, kind: ToolKind, tags: &[&str], caps: &[&str]) -> ToolDefinition {
ToolDefinition {
name: name.to_string(),
description: format!("Tool that does {}", name),
kind,
input_schema: json!({"type": "object"}),
metadata: ToolMetadata {
tags: tags.iter().map(|s| s.to_string()).collect(),
capabilities: caps.iter().map(|s| s.to_string()).collect(),
..ToolMetadata::default()
},
}
}
#[test]
fn registry_empty_by_default() {
let reg = ToolRegistry::new();
assert!(reg.visible_definitions().is_empty());
}
#[test]
fn visible_definitions_are_sorted_by_name() {
let mut reg = ToolRegistry::new();
for name in ["write_file", "bash", "read_file", "apply_patch"] {
reg.register(make_def(name, ToolKind::Read, &[], &[]));
}
let names: Vec<String> = reg
.visible_definitions()
.into_iter()
.map(|d| d.name)
.collect();
assert_eq!(
names,
vec!["apply_patch", "bash", "read_file", "write_file"]
);
}
#[test]
fn registry_register_and_retrieve() {
let mut reg = ToolRegistry::new();
let def = make_def(
"read_file",
ToolKind::Read,
&["file", "read"],
&["repo.read"],
);
reg.register(def.clone());
assert_eq!(reg.visible_definitions().len(), 1);
assert!(reg.get("read_file").is_some());
}
#[test]
fn registry_deferred_tools_not_visible() {
let mut reg = ToolRegistry::new();
reg.register(make_def("read_file", ToolKind::Read, &["tool"], &[]));
let mut def = make_def("secret_tool", ToolKind::Custom, &["power"], &[]);
def.metadata.exposure = ToolExposure::Deferred;
reg.register(def);
let visible: Vec<String> = reg
.visible_definitions()
.into_iter()
.map(|d| d.name)
.collect();
assert_eq!(visible, vec!["read_file".to_string()]);
assert_eq!(reg.deferred_definitions().len(), 1);
let found = reg.search("secret", 5);
assert_eq!(found.len(), 1);
assert_eq!(found[0].name, "secret_tool");
}
#[test]
fn registry_never_auto_promotes_deferred_tools() {
let mut reg = ToolRegistry::new();
for name in ["bash", "edit", "read_file"] {
reg.register(make_def(name, ToolKind::Read, &["core"], &[]));
}
for name in ["code", "package_manager", "browser"] {
let mut def = make_def(name, ToolKind::Custom, &["power"], &[]);
def.metadata.exposure = ToolExposure::Deferred;
reg.register(def);
}
let visible = reg.visible_tool_names();
assert_eq!(visible.len(), 3);
assert!(!visible.iter().any(|n| n == "code"));
assert!(!visible.iter().any(|n| n == "package_manager"));
assert!(!visible.iter().any(|n| n == "browser"));
assert_eq!(MCP_TOOL_DEFER_THRESHOLD, 100);
}
#[test]
fn registry_hidden_tools_not_searchable() {
let mut reg = ToolRegistry::new();
let mut def = make_def("internal_tool", ToolKind::Custom, &["internal"], &[]);
def.metadata.exposure = ToolExposure::Hidden;
reg.register(def);
assert!(reg.search("internal", 10).is_empty());
}
#[test]
fn registry_search_ranking() {
let mut reg = ToolRegistry::new();
reg.register(make_def(
"read_file",
ToolKind::Read,
&["file", "read"],
&["repo.read"],
));
reg.register(make_def(
"write_file",
ToolKind::Write,
&["file", "write"],
&["repo.write"],
));
reg.register(make_def(
"bash",
ToolKind::Command,
&["shell"],
&["shell.exec"],
));
let results = reg.search("file", 10);
assert!(results.len() >= 2);
let names: Vec<&str> = results.iter().map(|d| d.name.as_str()).collect();
assert!(names.contains(&"read_file"));
assert!(names.contains(&"write_file"));
}
#[test]
fn registry_phase_filtering() {
let mut reg = ToolRegistry::new();
let read_def = make_def("read_file", ToolKind::Read, &[], &[]);
let write_def = make_def("write_file", ToolKind::Write, &[], &[]);
reg.register_with(read_def, ToolExposure::Direct, vec!["reading".to_string()]);
reg.register_with(write_def, ToolExposure::Direct, vec!["editing".to_string()]);
let reading_set = reg.for_phase("reading");
assert_eq!(reading_set.len(), 1);
assert_eq!(reading_set[0].name, "read_file");
let editing_set = reg.for_phase("editing");
assert_eq!(editing_set.len(), 1);
assert_eq!(editing_set[0].name, "write_file");
}
#[test]
fn registry_phase_empty_means_all_phases() {
let mut reg = ToolRegistry::new();
let def = make_def("bash", ToolKind::Command, &[], &[]);
reg.register_with(def, ToolExposure::Direct, vec![]);
assert_eq!(reg.for_phase("planning").len(), 1);
assert_eq!(reg.for_phase("reading").len(), 1);
assert_eq!(reg.for_phase("editing").len(), 1);
}
#[test]
fn toolset_for_phase_creates_correct_set() {
let mut reg = ToolRegistry::new();
reg.register(make_def("read", ToolKind::Read, &[], &[]));
reg.register(make_def("write", ToolKind::Write, &[], &[]));
let ts = ToolSet::for_phase(®, "planning");
assert_eq!(ts.phase, "planning");
assert_eq!(ts.definitions.len(), 2);
}
#[test]
fn search_respects_max_results() {
let mut reg = ToolRegistry::new();
for i in 0..10 {
reg.register(make_def(
&format!("tool_{}", i),
ToolKind::Custom,
&["test"],
&[],
));
}
let results = reg.search("test", 3);
assert_eq!(results.len(), 3);
}
#[test]
fn search_excludes_zero_score_tools() {
let mut reg = ToolRegistry::new();
reg.register(make_def(
"read_file",
ToolKind::Read,
&["file"],
&["repo.read"],
));
assert!(reg.search("nonexistent-capability", 10).is_empty());
assert!(reg.search("", 10).is_empty());
}
}