Skip to main content

crabtalk_runtime/mcp/
tool.rs

1//! Tool dispatch and schema registration for the MCP tool.
2
3use crate::{RuntimeHook, bridge::RuntimeBridge};
4use schemars::JsonSchema;
5use serde::Deserialize;
6use wcore::agent::ToolDescription;
7
8#[derive(Deserialize, JsonSchema)]
9pub struct Mcp {
10    /// Tool name to call. If no exact match, returns fuzzy matches.
11    /// Leave empty to list all available MCP tools.
12    pub name: String,
13    /// JSON-encoded arguments string (only used when calling a tool).
14    #[serde(default)]
15    pub args: Option<String>,
16}
17
18impl ToolDescription for Mcp {
19    const DESCRIPTION: &'static str =
20        "Call an MCP tool by name, or list available tools if no exact match.";
21}
22
23impl<B: RuntimeBridge> RuntimeHook<B> {
24    pub async fn dispatch_mcp(&self, args: &str, agent: &str) -> String {
25        let input: Mcp = match serde_json::from_str(args) {
26            Ok(v) => v,
27            Err(e) => return format!("invalid arguments: {e}"),
28        };
29
30        let bridge = self.mcp.bridge().await;
31
32        // Resolve allowed tools from agent's MCP scope.
33        let allowed_tools: Option<Vec<String>> = if let Some(scope) = self.scopes.get(agent)
34            && !scope.mcps.is_empty()
35        {
36            let servers = bridge.list_servers().await;
37            Some(
38                servers
39                    .into_iter()
40                    .filter(|(name, _)| scope.mcps.iter().any(|m| m == name.as_str()))
41                    .flat_map(|(_, tools)| tools)
42                    .collect(),
43            )
44        } else {
45            None
46        };
47
48        // Try exact call first.
49        if !input.name.is_empty() {
50            // Enforce scope.
51            if let Some(ref allowed) = allowed_tools
52                && !allowed.iter().any(|t| t.as_str() == input.name)
53            {
54                return format!("tool not available: {}", input.name);
55            }
56
57            let tools = bridge.tools().await;
58            if tools.iter().any(|t| t.name == input.name) {
59                let tool_args = input.args.unwrap_or_default();
60                return bridge.call(&input.name, &tool_args).await;
61            }
62        }
63
64        // No exact match — fuzzy search / list all.
65        let query = input.name.to_lowercase();
66        let tools = bridge.tools().await;
67        let matches: Vec<String> = tools
68            .iter()
69            .filter(|t| {
70                if let Some(ref allowed) = allowed_tools
71                    && !allowed.iter().any(|a| a.as_str() == t.name.as_str())
72                {
73                    return false;
74                }
75                query.is_empty()
76                    || t.name.to_lowercase().contains(&query)
77                    || t.description.to_lowercase().contains(&query)
78            })
79            .map(|t| format!("{}: {}", t.name, t.description))
80            .collect();
81
82        if matches.is_empty() {
83            "no tools found".to_owned()
84        } else {
85            matches.join("\n")
86        }
87    }
88}