use crate::capabilities::Capability;
use crate::capability_types::CapabilityStatus;
use crate::mcp_server::{McpToolDefinition, mcp_tool_name};
use crate::tools::Tool;
use everruns_contracts::CapabilityId;
use everruns_contracts::tool_types::{
BuiltinTool, DeferrablePolicy, ToolDefinition, ToolHints, ToolPolicy,
};
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 narrate(
&self,
_tool_def: Option<&everruns_contracts::tool_types::ToolDefinition>,
tool_call: &everruns_contracts::tool_types::ToolCall,
phase: crate::tool_narration::ToolNarrationPhase,
locale: Option<&str>,
_ctx: crate::tool_narration::ToolNarrationContext<'_>,
) -> Option<String> {
if !tool_call.name.ends_with("__search") {
return None;
}
Some(crate::tool_narration::narrate_provider_search(
&tool_call.arguments,
phase,
locale,
))
}
fn tools(&self) -> Vec<Box<dyn Tool>> {
vec![]
}
fn tool_definitions(&self) -> Vec<ToolDefinition> {
if !crate::mcp_server::is_valid_mcp_server_name(&self.server_name) {
return vec![];
}
self.tools
.iter()
.map(|t| self.mcp_tool_to_definition(t))
.collect()
}
}
pub trait McpCapabilityIdExt: Sized {
fn is_mcp(&self) -> bool;
fn mcp(server_id: Uuid) -> Self;
fn mcp_server_id(&self) -> Option<Uuid>;
}
impl McpCapabilityIdExt for CapabilityId {
fn is_mcp(&self) -> bool {
is_mcp_capability(self.as_str())
}
fn mcp(server_id: Uuid) -> Self {
Self::new(mcp_capability_id(server_id))
}
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 capability_id_helpers_preserve_wire_identity_and_reject_invalid_namespaces() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let wire = "mcp:550e8400-e29b-41d4-a716-446655440000";
assert_eq!(mcp_capability_id(id), wire);
assert!(is_mcp_capability(wire));
assert_eq!(parse_mcp_capability_id(wire), Some(id));
let capability_id = CapabilityId::mcp(id);
assert_eq!(capability_id.as_str(), wire);
assert!(capability_id.is_mcp());
assert_eq!(capability_id.mcp_server_id(), Some(id));
for invalid in ["current_time", "mcp_something", "mcp:invalid", "mcp:"] {
assert_eq!(parse_mcp_capability_id(invalid), None);
assert_eq!(CapabilityId::new(invalid).mcp_server_id(), None);
}
assert!(!is_mcp_capability("current_time"));
assert!(!is_mcp_capability("mcp_something"));
assert!(!CapabilityId::new("current_time").is_mcp());
assert!(is_mcp_capability("mcp:invalid"));
}
#[test]
fn capability_definitions_preserve_schema_attribution_and_annotation_overrides() {
let id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
let schema = json!({"type":"object","properties":{"query":{"type":"string","minLength":3}},"required":["query"]});
for (
annotations,
expected_readonly,
expected_destructive,
expected_idempotent,
expected_open_world,
) in [
(None, None, None, None, true),
(
Some(crate::McpToolAnnotations {
read_only_hint: Some(true),
destructive_hint: Some(false),
idempotent_hint: Some(true),
open_world_hint: Some(false),
}),
Some(true),
Some(false),
Some(true),
false,
),
] {
let capability = McpCapability::new(
id,
"microsoft-learn".into(),
Some("Microsoft Learn MCP".into()),
vec![McpToolDefinition {
name: "search".into(),
description: Some("Search documentation".into()),
input_schema: schema.clone(),
annotations,
}],
);
let defs = capability.tool_definitions();
assert_eq!(defs.len(), 1);
let ToolDefinition::Builtin(builtin) = &defs[0] else {
panic!("expected builtin")
};
assert_eq!(builtin.name, "mcp_microsoft_learn__search");
assert_eq!(builtin.description, "Search documentation");
assert_eq!(builtin.parameters, schema);
assert_eq!(builtin.hints.readonly, expected_readonly);
assert_eq!(builtin.hints.destructive, expected_destructive);
assert_eq!(builtin.hints.idempotent, expected_idempotent);
assert_eq!(builtin.hints.open_world, Some(expected_open_world));
assert_eq!(
defs[0].capability_attribution(),
Some((
"mcp:550e8400-e29b-41d4-a716-446655440000",
Some("microsoft-learn")
))
);
}
}
#[test]
fn ambiguous_server_names_do_not_publish_misrouted_tool_definitions() {
for name in ["docs_", "docs-", "docs__private", "docs..private", "_", ""] {
let capability = McpCapability::new(
Uuid::nil(),
name.into(),
None,
vec![McpToolDefinition {
name: "search".into(),
description: None,
input_schema: json!({"type":"object"}),
annotations: None,
}],
);
assert!(
capability.tool_definitions().is_empty(),
"ambiguous server {name:?} published tools"
);
}
let capability = McpCapability::new(
Uuid::nil(),
"docs_api".into(),
None,
vec![McpToolDefinition {
name: "read__file".into(),
description: None,
input_schema: json!({"type":"object"}),
annotations: None,
}],
);
let definitions = capability.tool_definitions();
assert_eq!(definitions.len(), 1);
assert_eq!(
crate::parse_mcp_tool_name(definitions[0].name()),
Some(("docs_api".into(), "read__file".into()))
);
}
}