Skip to main content

soma_mcp_client/upstream/pool/
tools.rs

1use 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;