1use indexmap::IndexMap;
15
16use super::{Exposure, RegisteredTool};
17
18#[derive(Clone, Default)]
20pub struct ToolRegistry {
21 tools: IndexMap<String, RegisteredTool>,
22 active: Vec<String>,
23 pending: Vec<String>,
24 allowed: Option<Vec<String>>,
26 excluded: Vec<String>,
28}
29
30impl ToolRegistry {
31 pub fn new(tools: Vec<RegisteredTool>, active: Vec<String>) -> ToolRegistry {
33 let mut registry = ToolRegistry {
34 tools: tools
35 .into_iter()
36 .map(|tool| (tool.name().to_owned(), tool))
37 .collect(),
38 active: Vec::new(),
39 pending: Vec::new(),
40 allowed: None,
41 excluded: Vec::new(),
42 };
43 registry.apply(active);
44 registry
45 }
46
47 pub fn restrict(&mut self, allowed: Option<Vec<String>>, excluded: Vec<String>) {
50 self.allowed = allowed;
51 self.excluded = excluded;
52 let removed: Vec<String> = self
53 .tools
54 .keys()
55 .filter(|name| !self.is_allowed(name))
56 .cloned()
57 .collect();
58 for name in removed {
59 self.tools.shift_remove(&name);
60 }
61 self.apply(self.active.clone());
62 }
63
64 pub fn is_allowed(&self, name: &str) -> bool {
66 self.allowed
67 .as_ref()
68 .is_none_or(|allowed| allowed.iter().any(|allowed| allowed == name))
69 && !self.excluded.iter().any(|excluded| excluded == name)
70 }
71
72 fn activated_on_registration(tool: &RegisteredTool) -> bool {
73 tool.exposure.declarable() && tool.default_active
74 }
75
76 fn apply(&mut self, names: Vec<String>) {
78 let mut active = Vec::new();
79 for name in names {
80 let usable = self
81 .tools
82 .get(&name)
83 .is_some_and(|tool| tool.exposure != Exposure::Hidden);
84 if usable && !active.contains(&name) {
85 active.push(name);
86 }
87 }
88 self.pending.retain(|name| !active.contains(name));
89 self.active = active;
90 }
91
92 pub fn register(&mut self, tool: RegisteredTool) {
94 let name = tool.name().to_owned();
95 if !self.is_allowed(&name) {
96 return;
97 }
98 let was_activated = self
99 .tools
100 .get(&name)
101 .is_some_and(Self::activated_on_registration);
102 let named = self.allowed.is_some() && tool.exposure.declarable();
105 let activate = !was_activated && (named || Self::activated_on_registration(&tool));
106 self.tools.insert(name.clone(), tool);
107 let mut next = self.active.clone();
108 if activate {
109 next.push(name);
110 }
111 next.extend(
112 self.pending
113 .iter()
114 .filter(|pending| self.tools.contains_key(*pending))
115 .cloned(),
116 );
117 self.apply(next);
118 }
119
120 pub fn set_active(&mut self, names: Vec<String>) {
123 let previous = self.active.clone();
124 self.apply(names);
125 if previous.iter().any(|name| !self.active.contains(name)) {
126 self.pending.clear();
127 }
128 }
129
130 pub fn restore(&mut self, names: Vec<String>) {
133 self.pending = names
134 .iter()
135 .filter(|name| !self.tools.contains_key(*name))
136 .cloned()
137 .collect();
138 self.apply(names);
139 }
140
141 pub fn active(&self) -> Vec<String> {
143 self.active.clone()
144 }
145
146 pub fn declared(&self) -> Vec<RegisteredTool> {
148 self.active
149 .iter()
150 .filter_map(|name| self.tools.get(name).cloned())
151 .collect()
152 }
153
154 pub fn all(&self) -> Vec<RegisteredTool> {
156 self.tools.values().cloned().collect()
157 }
158
159 pub fn get(&self, name: &str) -> Option<&RegisteredTool> {
161 self.tools.get(name)
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use std::sync::Arc;
168
169 use futures_util::future::BoxFuture;
170 use serde_json::Value;
171 use tokio_util::sync::CancellationToken;
172 use yapi_agent::{Tool, UpdateSink};
173 use yapi_types::event::ToolResult;
174 use yapi_types::message::ToolDeclaration;
175
176 use super::*;
177
178 struct Named(ToolDeclaration);
179
180 impl Tool for Named {
181 fn declaration(&self) -> &ToolDeclaration {
182 &self.0
183 }
184
185 fn execute(
186 &self,
187 _call_id: String,
188 _args: Value,
189 _cancel: CancellationToken,
190 _updates: UpdateSink,
191 ) -> BoxFuture<'_, Result<ToolResult, String>> {
192 Box::pin(async { Err("unused".into()) })
193 }
194 }
195
196 fn tool(name: &str, exposure: Exposure) -> RegisteredTool {
197 RegisteredTool {
198 exposure,
199 ..RegisteredTool::direct(
200 Arc::new(Named(ToolDeclaration {
201 name: name.into(),
202 description: String::new(),
203 parameters: serde_json::json!({}),
204 constrained_sampling: None,
205 })),
206 None,
207 Vec::new(),
208 )
209 }
210 }
211
212 #[test]
213 fn activates_like_pi() {
214 let mut registry = ToolRegistry::new(
215 vec![
216 tool("read", Exposure::Direct),
217 tool("bash", Exposure::Direct),
218 ],
219 vec!["read".into(), "nope".into()],
220 );
221 assert_eq!(registry.active(), ["read"]);
222 registry.register(tool("mcp__a", Exposure::Direct));
223 registry.register(tool("mcp__b", Exposure::Deferred));
224 assert_eq!(registry.active(), ["read", "mcp__a"]);
225 registry.set_active(vec!["read".into()]);
227 registry.register(tool("mcp__a", Exposure::Direct));
228 assert_eq!(registry.active(), ["read"]);
229 registry.set_active(vec!["read".into(), "mcp__a".into(), "mcp__b".into()]);
232 registry.register(tool("mcp__a", Exposure::Hidden));
233 assert_eq!(registry.active(), ["read", "mcp__b"]);
234 registry.register(tool("mcp__a", Exposure::Direct));
235 assert_eq!(registry.active(), ["read", "mcp__b", "mcp__a"]);
236 }
237
238 #[test]
239 fn tool_flags_restrict_registrations() {
240 let mut registry = ToolRegistry::new(
241 vec![
242 tool("read", Exposure::Direct),
243 tool("bash", Exposure::Direct),
244 ],
245 vec!["read".into(), "bash".into()],
246 );
247 registry.restrict(Some(vec!["read".into(), "mcp__named".into()]), Vec::new());
250 assert_eq!(registry.active(), ["read"]);
251 registry.register(tool("codemode", Exposure::Direct));
252 registry.register(tool("mcp__other", Exposure::Direct));
253 registry.register(tool("mcp__named", Exposure::Deferred));
254 assert!(registry.get("codemode").is_none());
255 assert!(registry.get("mcp__other").is_none());
256 registry.set_active(vec!["read".into(), "codemode".into()]);
257 assert_eq!(registry.active(), ["read"]);
258 registry.restrict(Some(Vec::new()), Vec::new());
260 registry.register(tool("mcp__a", Exposure::Direct));
261 assert!(registry.active().is_empty());
262 registry.restrict(None, vec!["bash".into()]);
263 registry.register(tool("bash", Exposure::Direct));
264 registry.register(tool("mcp__a", Exposure::Direct));
265 assert_eq!(registry.active(), ["mcp__a"]);
266 }
267
268 #[test]
269 fn restored_names_await_registration() {
270 let mut registry = ToolRegistry::new(vec![tool("read", Exposure::Direct)], Vec::new());
271 registry.restore(vec!["read".into(), "mcp__late".into()]);
272 assert_eq!(registry.active(), ["read"]);
273 registry.register(tool("mcp__late", Exposure::Deferred));
274 assert_eq!(registry.active(), ["read", "mcp__late"]);
275 }
276}