soma_mcp_client/upstream/pool/
tools.rs1use crate::upstream::{CapScope, ToolDescriptor, UpstreamError};
2
3use super::PoolEntry;
4
5pub(super) fn ensure_tool_exposed(entry: &PoolEntry, tool: &str) -> Result<(), UpstreamError> {
6 if !matches_filter(entry.config.expose_tools.as_deref(), tool) {
7 return Err(UpstreamError::NotExposed {
8 upstream: entry.snapshot.name.clone(),
9 item: tool.to_owned(),
10 });
11 }
12 if entry
13 .snapshot
14 .tools
15 .iter()
16 .any(|candidate| candidate.name == tool)
17 {
18 return Ok(());
19 }
20 Err(UpstreamError::NotExposed {
21 upstream: entry.snapshot.name.clone(),
22 item: tool.to_owned(),
23 })
24}
25
26pub(super) fn matches_filter(filters: Option<&[String]>, candidate: &str) -> bool {
27 filters.is_none_or(|filters| {
28 filters.iter().any(|filter| {
29 filter == candidate
30 || filter == "*"
31 || filter
32 .strip_suffix('*')
33 .is_some_and(|prefix| candidate.starts_with(prefix))
34 })
35 })
36}
37
38impl super::UpstreamPool {
39 pub fn exposed_tools(&self, upstream: &str) -> Result<Vec<ToolDescriptor>, UpstreamError> {
40 self.with_entry(upstream, |entry| {
41 let tools: Vec<ToolDescriptor> = entry
42 .snapshot
43 .tools
44 .iter()
45 .filter(|tool| matches_filter(entry.config.expose_tools.as_deref(), &tool.name))
46 .cloned()
47 .collect();
48 let bytes = serde_json::to_vec(&tools).map_or(usize::MAX, |bytes| bytes.len());
49 self.response_caps().enforce(CapScope::ToolsList, bytes)?;
50 Ok(tools)
51 })
52 }
53
54 pub fn exposed_tool_count(&self) -> usize {
55 self.entries
56 .read()
57 .expect("upstream pool lock poisoned")
58 .values()
59 .map(|entry| {
60 entry
61 .snapshot
62 .tools
63 .iter()
64 .filter(|tool| matches_filter(entry.config.expose_tools.as_deref(), &tool.name))
65 .count()
66 })
67 .sum()
68 }
69}
70
71#[cfg(test)]
72#[path = "tools_tests.rs"]
73mod tests;