use crate::capability::{classify, CapabilityClass};
use crate::mcp::McpRegistry;
use serde_json::Value;
use std::path::Path;
pub async fn list_mcp_tools_for_agent(registry: &McpRegistry) -> Vec<Value> {
let mut tools = Vec::new();
let servers = match registry.list_all_servers().await {
Ok(s) => s,
Err(_) => return tools,
};
for server in servers {
if !server.connected {
continue;
}
for schema in registry.tool_schemas_for_server(&server.name).await {
let class = classify(&schema.server, &schema.name);
if class != CapabilityClass::Read {
continue;
}
let mut properties = serde_json::Map::new();
if let Some(props) = schema.input_schema.get("properties") {
if let Some(obj) = props.as_object() {
for (k, v) in obj {
properties.insert(k.clone(), v.clone());
}
}
}
let required: Vec<String> = schema
.input_schema
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default();
tools.push(serde_json::json!({
"type": "function",
"function": {
"name": mcp_tool_name(&schema.server, &schema.name),
"description": schema.description,
"parameters": {
"type": "object",
"properties": properties,
"required": required,
}
}
}));
}
}
tools
}
pub async fn execute_mcp_tool(
registry: &McpRegistry,
server: &str,
tool: &str,
args: &Value,
workspace_id: &str,
_workspace_root: &Path,
) -> anyhow::Result<String> {
let class = classify(server, tool);
if class != CapabilityClass::Read {
return Err(anyhow::anyhow!(
"MCP tool '{}.{}' is not Read-class (class: {:?}) — requires human approval",
server,
tool,
class
));
}
let args = crate::capability::stamp_workspace_scope(server, tool, args.clone(), workspace_id);
let outcome = registry
.call_tool(server, tool, args)
.await
.ok_or_else(|| {
anyhow::anyhow!("MCP tool '{}.{}' not found or not connected", server, tool)
})?;
if outcome.success {
Ok(serde_json::to_string_pretty(&outcome.result).unwrap_or_default())
} else {
Err(anyhow::anyhow!(
"MCP tool '{}.{}' failed: {}",
server,
tool,
outcome.error.unwrap_or_else(|| "unknown error".into())
))
}
}
const NAME_SEP: &str = "::";
pub fn mcp_tool_name(server: &str, tool: &str) -> String {
format!("mcp_{}{}{}", server, NAME_SEP, tool)
}
pub fn parse_mcp_tool_name(name: &str) -> Option<(String, String)> {
let rest = name.strip_prefix("mcp_")?;
let (server, tool) = rest.split_once(NAME_SEP)?;
if server.is_empty() || tool.is_empty() {
return None;
}
Some((server.to_string(), tool.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mcp_tool_name_roundtrips_simple_names() {
let name = mcp_tool_name("rag", "search");
assert_eq!(name, "mcp_rag::search");
let (server, tool) = parse_mcp_tool_name(&name).unwrap();
assert_eq!(server, "rag");
assert_eq!(tool, "search");
}
#[test]
fn mcp_tool_name_roundtrips_underscored_names() {
let name = mcp_tool_name("sequential_thinking", "think_step");
let (server, tool) = parse_mcp_tool_name(&name).unwrap();
assert_eq!(server, "sequential_thinking");
assert_eq!(tool, "think_step");
}
#[test]
fn mcp_tool_name_roundtrips_memory_tools() {
let name = mcp_tool_name("memory", "memory_search");
let (server, tool) = parse_mcp_tool_name(&name).unwrap();
assert_eq!(server, "memory");
assert_eq!(tool, "memory_search");
}
#[test]
fn parse_mcp_tool_name_rejects_non_mcp() {
assert!(parse_mcp_tool_name("write_file").is_none());
}
#[test]
fn parse_mcp_tool_name_rejects_empty() {
assert!(parse_mcp_tool_name("mcp_").is_none());
assert!(parse_mcp_tool_name("mcp_rag").is_none());
assert!(parse_mcp_tool_name("mcp_rag::").is_none());
assert!(parse_mcp_tool_name("mcp_::search").is_none());
}
#[test]
fn parse_mcp_tool_name_rejects_underscore_only_form() {
assert!(parse_mcp_tool_name("mcp_rag_search").is_none());
}
}