everruns_core/capabilities/
mcp.rs1use crate::capability_types::{CapabilityId, CapabilityStatus};
16use crate::mcp_server::{McpToolDefinition, mcp_tool_name};
17use crate::tool_types::{BuiltinTool, DeferrablePolicy, ToolDefinition, ToolHints, ToolPolicy};
18use crate::tools::Tool;
19
20use super::Capability;
21use uuid::Uuid;
22
23pub const MCP_CAPABILITY_PREFIX: &str = "mcp:";
25
26pub fn mcp_capability_id(server_id: Uuid) -> String {
28 format!("{}{}", MCP_CAPABILITY_PREFIX, server_id)
29}
30
31pub fn is_mcp_capability(capability_id: &str) -> bool {
33 capability_id.starts_with(MCP_CAPABILITY_PREFIX)
34}
35
36pub fn parse_mcp_capability_id(capability_id: &str) -> Option<Uuid> {
38 if !capability_id.starts_with(MCP_CAPABILITY_PREFIX) {
39 return None;
40 }
41 let uuid_str = &capability_id[MCP_CAPABILITY_PREFIX.len()..];
42 Uuid::parse_str(uuid_str).ok()
43}
44
45#[derive(Debug, Clone)]
50pub struct McpCapability {
51 pub server_id: Uuid,
53 pub server_name: String,
55 pub description: Option<String>,
57 pub tools: Vec<McpToolDefinition>,
59}
60
61impl McpCapability {
62 pub fn new(
64 server_id: Uuid,
65 server_name: String,
66 description: Option<String>,
67 tools: Vec<McpToolDefinition>,
68 ) -> Self {
69 Self {
70 server_id,
71 server_name,
72 description,
73 tools,
74 }
75 }
76
77 pub fn capability_id(&self) -> String {
79 mcp_capability_id(self.server_id)
80 }
81
82 fn mcp_tool_to_definition(&self, mcp_tool: &McpToolDefinition) -> ToolDefinition {
85 let prefixed_name = mcp_tool_name(&self.server_name, &mcp_tool.name);
86
87 let hints = match &mcp_tool.annotations {
89 Some(ann) => ToolHints {
90 readonly: ann.read_only_hint,
91 destructive: ann.destructive_hint,
92 idempotent: ann.idempotent_hint,
93 open_world: Some(ann.open_world_hint.unwrap_or(true)),
95 ..ToolHints::default()
97 },
98 None => {
99 ToolHints::default().with_open_world(true)
101 }
102 };
103
104 ToolDefinition::Builtin(BuiltinTool {
105 name: prefixed_name,
106 display_name: None,
107 description: mcp_tool
108 .description
109 .clone()
110 .unwrap_or_else(|| format!("Tool from MCP server: {}", self.server_name)),
111 parameters: mcp_tool.input_schema.clone(),
112 policy: ToolPolicy::Auto,
113 category: self.category().map(|s| s.to_string()),
114 deferrable: DeferrablePolicy::default(),
115 hints,
116 full_parameters: None,
117 })
118 .with_capability_attribution(self.capability_id(), Some(self.server_name.clone()))
119 }
120}
121
122impl Capability for McpCapability {
123 fn id(&self) -> &str {
124 Box::leak(self.capability_id().into_boxed_str())
127 }
128
129 fn name(&self) -> &str {
130 Box::leak(self.server_name.clone().into_boxed_str())
131 }
132
133 fn description(&self) -> &str {
134 let desc = self
135 .description
136 .clone()
137 .unwrap_or_else(|| format!("MCP Server providing {} tool(s)", self.tools.len()));
138 Box::leak(desc.into_boxed_str())
139 }
140
141 fn status(&self) -> CapabilityStatus {
142 CapabilityStatus::Available
143 }
144
145 fn icon(&self) -> Option<&str> {
146 Some("mcp") }
148
149 fn category(&self) -> Option<&str> {
150 Some("MCP Servers")
151 }
152
153 fn system_prompt_addition(&self) -> Option<&str> {
154 None }
156
157 fn narrate(
158 &self,
159 _tool_def: Option<&crate::tool_types::ToolDefinition>,
160 tool_call: &crate::tool_types::ToolCall,
161 phase: crate::tool_narration::ToolNarrationPhase,
162 locale: Option<&str>,
163 _ctx: crate::tool_narration::ToolNarrationContext<'_>,
164 ) -> Option<String> {
165 if !tool_call.name.ends_with("__search") {
167 return None;
168 }
169 Some(crate::tool_narration::narrate_provider_search(
170 &tool_call.arguments,
171 phase,
172 locale,
173 ))
174 }
175
176 fn tools(&self) -> Vec<Box<dyn Tool>> {
177 vec![]
180 }
181
182 fn tool_definitions(&self) -> Vec<ToolDefinition> {
183 self.tools
184 .iter()
185 .map(|t| self.mcp_tool_to_definition(t))
186 .collect()
187 }
188}
189
190impl CapabilityId {
192 pub fn is_mcp(&self) -> bool {
194 is_mcp_capability(self.as_str())
195 }
196
197 pub fn mcp(server_id: Uuid) -> Self {
199 Self::new(mcp_capability_id(server_id))
200 }
201
202 pub fn mcp_server_id(&self) -> Option<Uuid> {
204 parse_mcp_capability_id(self.as_str())
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211 use serde_json::json;
212
213 #[test]
214 fn test_mcp_capability_id() {
215 let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
216 let cap_id = mcp_capability_id(server_id);
217 assert_eq!(cap_id, "mcp:550e8400-e29b-41d4-a716-446655440000");
218 }
219
220 #[test]
221 fn test_is_mcp_capability() {
222 assert!(is_mcp_capability(
223 "mcp:550e8400-e29b-41d4-a716-446655440000"
224 ));
225 assert!(!is_mcp_capability("current_time"));
226 assert!(!is_mcp_capability("mcp_something")); }
228
229 #[test]
230 fn test_parse_mcp_capability_id() {
231 let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
232 let cap_id = mcp_capability_id(server_id);
233 let parsed = parse_mcp_capability_id(&cap_id);
234 assert_eq!(parsed, Some(server_id));
235
236 assert_eq!(parse_mcp_capability_id("current_time"), None);
237 assert_eq!(parse_mcp_capability_id("mcp:invalid"), None);
238 }
239
240 #[test]
241 fn test_mcp_capability_tool_definitions() {
242 let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
243 let tools = vec![McpToolDefinition {
244 name: "search".to_string(),
245 description: Some("Search documentation".to_string()),
246 input_schema: json!({
247 "type": "object",
248 "properties": {
249 "query": { "type": "string" }
250 }
251 }),
252 annotations: None,
253 }];
254
255 let capability = McpCapability::new(
256 server_id,
257 "microsoft-learn".to_string(),
258 Some("Microsoft Learn MCP".to_string()),
259 tools,
260 );
261
262 let defs = capability.tool_definitions();
263 assert_eq!(defs.len(), 1);
264
265 let ToolDefinition::Builtin(builtin) = &defs[0] else {
266 panic!("expected Builtin variant");
267 };
268 assert_eq!(builtin.name, "mcp_microsoft_learn__search");
269 assert_eq!(builtin.description, "Search documentation");
270 assert_eq!(
271 defs[0].capability_attribution(),
272 Some((
273 "mcp:550e8400-e29b-41d4-a716-446655440000",
274 Some("microsoft-learn")
275 ))
276 );
277 }
278
279 #[test]
280 fn test_capability_id_mcp_methods() {
281 let server_id = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
282 let cap_id = CapabilityId::mcp(server_id);
283
284 assert!(cap_id.is_mcp());
285 assert_eq!(cap_id.mcp_server_id(), Some(server_id));
286
287 let regular_cap = CapabilityId::new("current_time");
288 assert!(!regular_cap.is_mcp());
289 assert_eq!(regular_cap.mcp_server_id(), None);
290 }
291}