use crate::capability_types::{CapabilityId, CapabilityStatus};
use crate::mcp_server::{McpToolDefinition, mcp_tool_name};
use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolDefinition, ToolHints, ToolPolicy};
use crate::tools::Tool;
use super::Capability;
use uuid::Uuid;
pub const MCP_CAPABILITY_PREFIX: &str = "mcp:";
pub fn mcp_capability_id(server_id: Uuid) -> String {
format!("{}{}", MCP_CAPABILITY_PREFIX, server_id)
}
pub fn is_mcp_capability(capability_id: &str) -> bool {
capability_id.starts_with(MCP_CAPABILITY_PREFIX)
}
pub fn parse_mcp_capability_id(capability_id: &str) -> Option<Uuid> {
if !capability_id.starts_with(MCP_CAPABILITY_PREFIX) {
return None;
}
let uuid_str = &capability_id[MCP_CAPABILITY_PREFIX.len()..];
Uuid::parse_str(uuid_str).ok()
}
#[derive(Debug, Clone)]
pub struct McpCapability {
pub server_id: Uuid,
pub server_name: String,
pub description: Option<String>,
pub tools: Vec<McpToolDefinition>,
}
impl McpCapability {
pub fn new(
server_id: Uuid,
server_name: String,
description: Option<String>,
tools: Vec<McpToolDefinition>,
) -> Self {
Self {
server_id,
server_name,
description,
tools,
}
}
pub fn capability_id(&self) -> String {
mcp_capability_id(self.server_id)
}
fn mcp_tool_to_definition(&self, mcp_tool: &McpToolDefinition) -> ToolDefinition {
let prefixed_name = mcp_tool_name(&self.server_name, &mcp_tool.name);
let hints = match &mcp_tool.annotations {
Some(ann) => ToolHints {
readonly: ann.read_only_hint,
destructive: ann.destructive_hint,
idempotent: ann.idempotent_hint,
open_world: Some(ann.open_world_hint.unwrap_or(true)),
..ToolHints::default()
},
None => {
ToolHints::default().with_open_world(true)
}
};
ToolDefinition::Builtin(BuiltinTool {
name: prefixed_name,
display_name: None,
description: mcp_tool
.description
.clone()
.unwrap_or_else(|| format!("Tool from MCP server: {}", self.server_name)),
parameters: mcp_tool.input_schema.clone(),
policy: ToolPolicy::Auto,
category: self.category().map(|s| s.to_string()),
deferrable: DeferrablePolicy::default(),
hints,
full_parameters: None,
})
.with_capability_attribution(self.capability_id(), Some(self.server_name.clone()))
}
}
impl Capability for McpCapability {
fn id(&self) -> &str {
Box::leak(self.capability_id().into_boxed_str())
}
fn name(&self) -> &str {
Box::leak(self.server_name.clone().into_boxed_str())
}
fn description(&self) -> &str {
let desc = self
.description
.clone()
.unwrap_or_else(|| format!("MCP Server providing {} tool(s)", self.tools.len()));
Box::leak(desc.into_boxed_str())
}
fn status(&self) -> CapabilityStatus {
CapabilityStatus::Available
}
fn icon(&self) -> Option<&str> {
Some("mcp") }
fn category(&self) -> Option<&str> {
Some("MCP Servers")
}
fn system_prompt_addition(&self) -> Option<&str> {
None }
fn tools(&self) -> Vec<Box<dyn Tool>> {
vec![]
}
fn tool_definitions(&self) -> Vec<ToolDefinition> {
self.tools
.iter()
.map(|t| self.mcp_tool_to_definition(t))
.collect()
}
}
impl CapabilityId {
pub fn is_mcp(&self) -> bool {
is_mcp_capability(self.as_str())
}
pub fn mcp(server_id: Uuid) -> Self {
Self::new(mcp_capability_id(server_id))
}
pub fn mcp_server_id(&self) -> Option<Uuid> {
parse_mcp_capability_id(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_mcp_capability_id() {
let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let cap_id = mcp_capability_id(server_id);
assert_eq!(cap_id, "mcp:550e8400-e29b-41d4-a716-446655440000");
}
#[test]
fn test_is_mcp_capability() {
assert!(is_mcp_capability(
"mcp:550e8400-e29b-41d4-a716-446655440000"
));
assert!(!is_mcp_capability("current_time"));
assert!(!is_mcp_capability("mcp_something")); }
#[test]
fn test_parse_mcp_capability_id() {
let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let cap_id = mcp_capability_id(server_id);
let parsed = parse_mcp_capability_id(&cap_id);
assert_eq!(parsed, Some(server_id));
assert_eq!(parse_mcp_capability_id("current_time"), None);
assert_eq!(parse_mcp_capability_id("mcp:invalid"), None);
}
#[test]
fn test_mcp_capability_tool_definitions() {
let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let tools = vec![McpToolDefinition {
name: "search".to_string(),
description: Some("Search documentation".to_string()),
input_schema: json!({
"type": "object",
"properties": {
"query": { "type": "string" }
}
}),
annotations: None,
}];
let capability = McpCapability::new(
server_id,
"microsoft-learn".to_string(),
Some("Microsoft Learn MCP".to_string()),
tools,
);
let defs = capability.tool_definitions();
assert_eq!(defs.len(), 1);
let ToolDefinition::Builtin(builtin) = &defs[0] else {
panic!("expected Builtin variant");
};
assert_eq!(builtin.name, "mcp_microsoft_learn__search");
assert_eq!(builtin.description, "Search documentation");
assert_eq!(
defs[0].capability_attribution(),
Some((
"mcp:550e8400-e29b-41d4-a716-446655440000",
Some("microsoft-learn")
))
);
}
#[test]
fn test_capability_id_mcp_methods() {
let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let cap_id = CapabilityId::mcp(server_id);
assert!(cap_id.is_mcp());
assert_eq!(cap_id.mcp_server_id(), Some(server_id));
let regular_cap = CapabilityId::new("current_time");
assert!(!regular_cap.is_mcp());
assert_eq!(regular_cap.mcp_server_id(), None);
}
}