agcodex_core/subagents/
mcp_tools.rs

1//! MCP tool definitions and integration for AGCodex subagents
2//!
3//! This module provides MCP (Model Context Protocol) tool definitions for each agent,
4//! enabling them to be invoked via MCP clients and to call other MCP tools.
5
6use serde::Deserialize;
7use serde::Serialize;
8use serde_json::Value as JsonValue;
9use serde_json::json;
10use std::collections::BTreeMap;
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use tracing::debug;
15use tracing::error;
16use tracing::info;
17
18use crate::models::FunctionCallOutputPayload;
19use crate::models::ResponseInputItem;
20use crate::openai_tools::JsonSchema;
21use crate::openai_tools::ResponsesApiTool;
22use crate::subagents::AgentContext;
23use crate::subagents::AgentInvocation;
24use crate::subagents::AgentOrchestrator;
25use crate::subagents::SharedContext;
26use crate::subagents::SubagentError;
27use crate::subagents::SubagentResult;
28
29/// MCP tool descriptor for an agent
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct McpAgentTool {
32    /// Tool name (e.g., "agent_code_reviewer")
33    pub name: String,
34    /// Human-readable description
35    pub description: String,
36    /// JSON schema for parameters
37    pub parameters: JsonSchema,
38    /// Agent name this tool maps to
39    pub agent_name: String,
40    /// Whether this tool requires confirmation
41    pub requires_confirmation: bool,
42    /// Maximum execution time in seconds
43    pub timeout_seconds: Option<u64>,
44}
45
46/// MCP tool provider for subagents
47#[derive(Debug, Clone)]
48pub struct McpAgentToolProvider {
49    /// Registry of available agent tools
50    tools: Arc<RwLock<HashMap<String, McpAgentTool>>>,
51    /// Agent orchestrator for execution
52    orchestrator: Arc<AgentOrchestrator>,
53}
54
55impl McpAgentToolProvider {
56    /// Create a new MCP agent tool provider
57    pub fn new(orchestrator: Arc<AgentOrchestrator>) -> Self {
58        Self {
59            tools: Arc::new(RwLock::new(HashMap::new())),
60            orchestrator,
61        }
62    }
63
64    /// Register standard agent tools
65    pub async fn register_standard_tools(&self) -> SubagentResult<()> {
66        let mut tools = self.tools.write().await;
67
68        // Code Reviewer Agent Tool
69        tools.insert(
70            "agent_code_reviewer".to_string(),
71            McpAgentTool {
72                name: "agent_code_reviewer".to_string(),
73                description: "Review code for quality, security, and maintainability".to_string(),
74                parameters: JsonSchema::Object {
75                    properties: BTreeMap::from([
76                        (
77                            "files".to_string(),
78                            JsonSchema::Array {
79                                items: Box::new(JsonSchema::String { description: None }),
80                                description: Some("Files to review".to_string()),
81                            },
82                        ),
83                        (
84                            "focus".to_string(),
85                            JsonSchema::String {
86                                description: Some(
87                                    "Review focus area: security, performance, quality, or all"
88                                        .to_string(),
89                                ),
90                            },
91                        ),
92                    ]),
93                    required: Some(vec!["files".to_string()]),
94                    additional_properties: Some(false),
95                },
96                agent_name: "code-reviewer".to_string(),
97                requires_confirmation: false,
98                timeout_seconds: Some(120),
99            },
100        );
101
102        // Refactorer Agent Tool
103        tools.insert(
104            "agent_refactorer".to_string(),
105            McpAgentTool {
106                name: "agent_refactorer".to_string(),
107                description: "Refactor code for better structure and maintainability".to_string(),
108                parameters: JsonSchema::Object {
109                    properties: BTreeMap::from([
110                        (
111                            "target".to_string(),
112                            JsonSchema::String {
113                                description: Some("File or directory to refactor".to_string()),
114                            },
115                        ),
116                        (
117                            "pattern".to_string(),
118                            JsonSchema::String {
119                                description: Some("Refactoring pattern to apply".to_string()),
120                            },
121                        ),
122                        (
123                            "dry_run".to_string(),
124                            JsonSchema::Boolean {
125                                description: Some("Preview changes without applying".to_string()),
126                            },
127                        ),
128                    ]),
129                    required: Some(vec!["target".to_string(), "pattern".to_string()]),
130                    additional_properties: Some(false),
131                },
132                agent_name: "refactorer".to_string(),
133                requires_confirmation: true,
134                timeout_seconds: Some(180),
135            },
136        );
137
138        // Debugger Agent Tool
139        tools.insert(
140            "agent_debugger".to_string(),
141            McpAgentTool {
142                name: "agent_debugger".to_string(),
143                description: "Debug code issues and find root causes".to_string(),
144                parameters: JsonSchema::Object {
145                    properties: BTreeMap::from([
146                        (
147                            "error".to_string(),
148                            JsonSchema::String {
149                                description: Some("Error message or stack trace".to_string()),
150                            },
151                        ),
152                        (
153                            "context".to_string(),
154                            JsonSchema::String {
155                                description: Some("Additional context about the issue".to_string()),
156                            },
157                        ),
158                        (
159                            "files".to_string(),
160                            JsonSchema::Array {
161                                items: Box::new(JsonSchema::String { description: None }),
162                                description: Some("Related files to analyze".to_string()),
163                            },
164                        ),
165                    ]),
166                    required: Some(vec!["error".to_string()]),
167                    additional_properties: Some(false),
168                },
169                agent_name: "debugger".to_string(),
170                requires_confirmation: false,
171                timeout_seconds: Some(150),
172            },
173        );
174
175        // Test Writer Agent Tool
176        tools.insert(
177            "agent_test_writer".to_string(),
178            McpAgentTool {
179                name: "agent_test_writer".to_string(),
180                description: "Generate comprehensive test suites".to_string(),
181                parameters: JsonSchema::Object {
182                    properties: BTreeMap::from([
183                        (
184                            "target".to_string(),
185                            JsonSchema::String {
186                                description: Some("File or module to test".to_string()),
187                            },
188                        ),
189                        (
190                            "test_type".to_string(),
191                            JsonSchema::String {
192                                description: Some(
193                                    "Type of tests to generate: unit, integration, e2e, or all"
194                                        .to_string(),
195                                ),
196                            },
197                        ),
198                        (
199                            "framework".to_string(),
200                            JsonSchema::String {
201                                description: Some("Testing framework to use".to_string()),
202                            },
203                        ),
204                    ]),
205                    required: Some(vec!["target".to_string()]),
206                    additional_properties: Some(false),
207                },
208                agent_name: "test-writer".to_string(),
209                requires_confirmation: true,
210                timeout_seconds: Some(120),
211            },
212        );
213
214        // Performance Agent Tool
215        tools.insert(
216            "agent_performance".to_string(),
217            McpAgentTool {
218                name: "agent_performance".to_string(),
219                description: "Analyze and optimize performance bottlenecks".to_string(),
220                parameters: JsonSchema::Object {
221                    properties: BTreeMap::from([
222                        (
223                            "target".to_string(),
224                            JsonSchema::String {
225                                description: Some("Code to analyze for performance".to_string()),
226                            },
227                        ),
228                        (
229                            "profile_data".to_string(),
230                            JsonSchema::String {
231                                description: Some("Optional profiling data".to_string()),
232                            },
233                        ),
234                        (
235                            "optimization_level".to_string(),
236                            JsonSchema::String {
237                                description: Some("How aggressive to be with optimizations: basic, aggressive, or extreme".to_string()),
238                            },
239                        ),
240                    ]),
241                    required: Some(vec!["target".to_string()]),
242                    additional_properties: Some(false),
243                },
244                agent_name: "performance".to_string(),
245                requires_confirmation: true,
246                timeout_seconds: Some(240),
247            },
248        );
249
250        // Security Agent Tool
251        tools.insert(
252            "agent_security".to_string(),
253            McpAgentTool {
254                name: "agent_security".to_string(),
255                description: "Scan for security vulnerabilities and suggest fixes".to_string(),
256                parameters: JsonSchema::Object {
257                    properties: BTreeMap::from([
258                        (
259                            "target".to_string(),
260                            JsonSchema::String {
261                                description: Some("Code to scan for vulnerabilities".to_string()),
262                            },
263                        ),
264                        (
265                            "scan_type".to_string(),
266                            JsonSchema::String {
267                                description: Some(
268                                    "Type of security scan: owasp, cve, secrets, or all"
269                                        .to_string(),
270                                ),
271                            },
272                        ),
273                        (
274                            "fix_suggestions".to_string(),
275                            JsonSchema::Boolean {
276                                description: Some("Generate fix suggestions".to_string()),
277                            },
278                        ),
279                    ]),
280                    required: Some(vec!["target".to_string()]),
281                    additional_properties: Some(false),
282                },
283                agent_name: "security".to_string(),
284                requires_confirmation: false,
285                timeout_seconds: Some(180),
286            },
287        );
288
289        // Documentation Agent Tool
290        tools.insert(
291            "agent_docs".to_string(),
292            McpAgentTool {
293                name: "agent_docs".to_string(),
294                description: "Generate comprehensive documentation".to_string(),
295                parameters: JsonSchema::Object {
296                    properties: BTreeMap::from([
297                        (
298                            "target".to_string(),
299                            JsonSchema::String {
300                                description: Some("Code to document".to_string()),
301                            },
302                        ),
303                        (
304                            "doc_type".to_string(),
305                            JsonSchema::String {
306                                description: Some(
307                                    "Type of documentation: api, tutorial, reference, or all"
308                                        .to_string(),
309                                ),
310                            },
311                        ),
312                        (
313                            "format".to_string(),
314                            JsonSchema::String {
315                                description: Some(
316                                    "Output format: markdown, html, or rst".to_string(),
317                                ),
318                            },
319                        ),
320                    ]),
321                    required: Some(vec!["target".to_string()]),
322                    additional_properties: Some(false),
323                },
324                agent_name: "docs".to_string(),
325                requires_confirmation: false,
326                timeout_seconds: Some(90),
327            },
328        );
329
330        // Agent Chain Tool (for sequential execution)
331        tools.insert(
332            "agent_chain".to_string(),
333            McpAgentTool {
334                name: "agent_chain".to_string(),
335                description: "Execute multiple agents in sequence".to_string(),
336                parameters: JsonSchema::Object {
337                    properties: BTreeMap::from([
338                        (
339                            "agents".to_string(),
340                            JsonSchema::Array {
341                                items: Box::new(JsonSchema::String { description: None }),
342                                description: Some("Agent names to execute in order".to_string()),
343                            },
344                        ),
345                        (
346                            "context".to_string(),
347                            JsonSchema::Object {
348                                properties: BTreeMap::new(),
349                                required: None,
350                                additional_properties: Some(true),
351                            },
352                        ),
353                        (
354                            "stop_on_error".to_string(),
355                            JsonSchema::Boolean {
356                                description: Some("Stop chain if an agent fails".to_string()),
357                            },
358                        ),
359                    ]),
360                    required: Some(vec!["agents".to_string()]),
361                    additional_properties: Some(false),
362                },
363                agent_name: "_chain".to_string(), // Special internal agent
364                requires_confirmation: true,
365                timeout_seconds: Some(600),
366            },
367        );
368
369        // Agent Parallel Tool (for parallel execution)
370        tools.insert(
371            "agent_parallel".to_string(),
372            McpAgentTool {
373                name: "agent_parallel".to_string(),
374                description: "Execute multiple agents in parallel".to_string(),
375                parameters: JsonSchema::Object {
376                    properties: BTreeMap::from([
377                        (
378                            "agents".to_string(),
379                            JsonSchema::Array {
380                                items: Box::new(JsonSchema::String { description: None }),
381                                description: Some("Agent names to execute in parallel".to_string()),
382                            },
383                        ),
384                        (
385                            "context".to_string(),
386                            JsonSchema::Object {
387                                properties: BTreeMap::new(),
388                                required: None,
389                                additional_properties: Some(true),
390                            },
391                        ),
392                        (
393                            "max_concurrency".to_string(),
394                            JsonSchema::Number {
395                                description: Some("Maximum agents to run concurrently".to_string()),
396                            },
397                        ),
398                    ]),
399                    required: Some(vec!["agents".to_string()]),
400                    additional_properties: Some(false),
401                },
402                agent_name: "_parallel".to_string(), // Special internal agent
403                requires_confirmation: true,
404                timeout_seconds: Some(600),
405            },
406        );
407
408        Ok(())
409    }
410
411    /// Get all available MCP tools for agents
412    pub async fn get_tools(&self) -> Vec<ResponsesApiTool> {
413        let tools = self.tools.read().await;
414        tools
415            .values()
416            .map(|tool| ResponsesApiTool {
417                name: tool.name.clone(),
418                description: tool.description.clone(),
419                strict: false,
420                parameters: tool.parameters.clone(),
421            })
422            .collect()
423    }
424
425    /// Discover tools available from connected MCP servers
426    pub async fn discover_mcp_tools(&self, _server_name: &str) -> SubagentResult<Vec<String>> {
427        // This would query the MCP server for available tools
428        // For now, return a placeholder list
429        Ok(vec![
430            "read_file".to_string(),
431            "write_file".to_string(),
432            "run_command".to_string(),
433            "search_code".to_string(),
434        ])
435    }
436
437    /// Execute an MCP tool call for an agent
438    pub async fn execute_tool(
439        &self,
440        tool_name: &str,
441        arguments: JsonValue,
442        context: &AgentContext,
443    ) -> SubagentResult<JsonValue> {
444        let tools = self.tools.read().await;
445
446        let tool = tools
447            .get(tool_name)
448            .ok_or_else(|| SubagentError::AgentNotFound {
449                name: tool_name.to_string(),
450            })?;
451
452        info!(
453            "Executing MCP tool '{}' for agent '{}'",
454            tool_name, tool.agent_name
455        );
456
457        // Special handling for chain and parallel tools
458        if tool.agent_name == "_chain" {
459            return self.execute_chain(arguments, context).await;
460        } else if tool.agent_name == "_parallel" {
461            return self.execute_parallel(arguments, context).await;
462        }
463
464        // Execute the agent
465        let invocation = AgentInvocation {
466            agent_name: tool.agent_name.clone(),
467            parameters: self.json_to_params(arguments)?,
468            raw_parameters: String::new(),
469            position: 0,
470            mode_override: None,
471            intelligence_override: None,
472        };
473
474        // Create a SharedContext for the orchestrator
475        let shared_context = SharedContext::new();
476        let result = self
477            .orchestrator
478            .execute_single(invocation, &shared_context)
479            .await?;
480
481        // Convert result to JSON
482        Ok(json!({
483            "success": true,
484            "agent": tool.agent_name,
485            "output": result.output,
486            "modified_files": result.modified_files,
487            "duration_ms": result.duration().map(|d| d.as_millis()),
488        }))
489    }
490
491    /// Execute agents in sequence
492    async fn execute_chain(
493        &self,
494        arguments: JsonValue,
495        context: &AgentContext,
496    ) -> SubagentResult<JsonValue> {
497        let agents = arguments["agents"]
498            .as_array()
499            .ok_or_else(|| SubagentError::InvalidConfig("agents must be an array".to_string()))?;
500
501        let stop_on_error = arguments["stop_on_error"].as_bool().unwrap_or(true);
502
503        let mut results = Vec::new();
504
505        for agent_name in agents {
506            let agent_name = agent_name.as_str().ok_or_else(|| {
507                SubagentError::InvalidConfig("agent name must be a string".to_string())
508            })?;
509
510            let invocation = AgentInvocation {
511                agent_name: agent_name.to_string(),
512                parameters: HashMap::new(),
513                raw_parameters: String::new(),
514                position: 0,
515                mode_override: None,
516                intelligence_override: None,
517            };
518
519            // Create a SharedContext for the orchestrator
520            let shared_context = SharedContext::new();
521            match self
522                .orchestrator
523                .execute_single(invocation, &shared_context)
524                .await
525            {
526                Ok(result) => {
527                    // Update context with results for next agent
528                    context
529                        .send_message(crate::subagents::context::AgentMessage {
530                            id: uuid::Uuid::new_v4(),
531                            from: agent_name.to_string(),
532                            to: crate::subagents::context::MessageTarget::Broadcast,
533                            message_type: crate::subagents::context::MessageType::Result,
534                            priority: crate::subagents::context::MessagePriority::Normal,
535                            payload: serde_json::json!({
536                                "output": result.output.clone().unwrap_or_default()
537                            }),
538                            timestamp: chrono::Utc::now(),
539                        })
540                        .await
541                        .ok();
542                    results.push(json!({
543                        "agent": agent_name,
544                        "success": true,
545                        "output": result.output,
546                    }));
547                }
548                Err(e) => {
549                    results.push(json!({
550                        "agent": agent_name,
551                        "success": false,
552                        "error": e.to_string(),
553                    }));
554                    if stop_on_error {
555                        break;
556                    }
557                }
558            }
559        }
560
561        Ok(json!({
562            "chain_results": results,
563            "completed": results.len() == agents.len(),
564        }))
565    }
566
567    /// Execute agents in parallel
568    async fn execute_parallel(
569        &self,
570        arguments: JsonValue,
571        _context: &AgentContext,
572    ) -> SubagentResult<JsonValue> {
573        let agents = arguments["agents"]
574            .as_array()
575            .ok_or_else(|| SubagentError::InvalidConfig("agents must be an array".to_string()))?;
576
577        let max_concurrency = arguments["max_concurrency"]
578            .as_u64()
579            .unwrap_or(4)
580            .min(agents.len() as u64) as usize;
581
582        let mut handles = Vec::new();
583        let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrency));
584
585        for agent_name in agents {
586            let agent_name = agent_name
587                .as_str()
588                .ok_or_else(|| {
589                    SubagentError::InvalidConfig("agent name must be a string".to_string())
590                })?
591                .to_string();
592
593            let orchestrator = self.orchestrator.clone();
594            let permit = semaphore.clone().acquire_owned().await.unwrap();
595
596            let handle = tokio::spawn(async move {
597                let invocation = AgentInvocation {
598                    agent_name: agent_name.clone(),
599                    parameters: HashMap::new(),
600                    raw_parameters: String::new(),
601                    position: 0,
602                    mode_override: None,
603                    intelligence_override: None,
604                };
605
606                // Create a SharedContext for the orchestrator
607                let shared_context = SharedContext::new();
608                let result = orchestrator
609                    .execute_single(invocation, &shared_context)
610                    .await;
611                drop(permit); // Release semaphore
612
613                match result {
614                    Ok(execution) => json!({
615                        "agent": agent_name,
616                        "success": true,
617                        "output": execution.output,
618                        "duration_ms": execution.duration().map(|d| d.as_millis()),
619                    }),
620                    Err(e) => json!({
621                        "agent": agent_name,
622                        "success": false,
623                        "error": e.to_string(),
624                    }),
625                }
626            });
627
628            handles.push(handle);
629        }
630
631        let mut results = Vec::new();
632        for handle in handles {
633            match handle.await {
634                Ok(result) => results.push(result),
635                Err(e) => {
636                    error!("Failed to join agent task: {}", e);
637                    results.push(json!({
638                        "error": "Task join error",
639                    }));
640                }
641            }
642        }
643
644        Ok(json!({
645            "parallel_results": results,
646            "total_agents": agents.len(),
647        }))
648    }
649
650    /// Convert JSON arguments to parameter map
651    fn json_to_params(&self, json: JsonValue) -> SubagentResult<HashMap<String, String>> {
652        let obj = json.as_object().ok_or_else(|| {
653            SubagentError::InvalidConfig("arguments must be an object".to_string())
654        })?;
655
656        let mut params = HashMap::new();
657        for (key, value) in obj {
658            let string_value = match value {
659                JsonValue::String(s) => s.clone(),
660                JsonValue::Number(n) => n.to_string(),
661                JsonValue::Bool(b) => b.to_string(),
662                JsonValue::Array(_) | JsonValue::Object(_) => serde_json::to_string(value)
663                    .map_err(|e| SubagentError::InvalidConfig(e.to_string()))?,
664                JsonValue::Null => String::new(),
665            };
666            params.insert(key.clone(), string_value);
667        }
668
669        Ok(params)
670    }
671
672    /// Stream results back via MCP protocol
673    pub async fn stream_results(
674        &self,
675        call_id: String,
676        agent_name: String,
677        output: String,
678    ) -> ResponseInputItem {
679        ResponseInputItem::FunctionCallOutput {
680            call_id,
681            output: FunctionCallOutputPayload {
682                content: json!({
683                    "agent": agent_name,
684                    "output": output,
685                    "timestamp": chrono::Utc::now().to_rfc3339(),
686                })
687                .to_string(),
688                success: Some(true),
689            },
690        }
691    }
692}
693
694/// MCP tool call handler for agents
695pub struct McpAgentHandler {
696    provider: Arc<McpAgentToolProvider>,
697}
698
699impl McpAgentHandler {
700    pub const fn new(provider: Arc<McpAgentToolProvider>) -> Self {
701        Self { provider }
702    }
703
704    /// Handle an incoming MCP tool call for an agent
705    pub async fn handle_tool_call(
706        &self,
707        tool_name: &str,
708        arguments: JsonValue,
709        context: AgentContext,
710    ) -> SubagentResult<JsonValue> {
711        debug!("Handling MCP tool call: {}", tool_name);
712        self.provider
713            .execute_tool(tool_name, arguments, &context)
714            .await
715    }
716
717    /// Register this handler with an MCP server
718    pub async fn register_with_server(&self, server_name: &str) -> SubagentResult<()> {
719        info!("Registering agent tools with MCP server: {}", server_name);
720
721        // Get all tools
722        let tools = self.provider.get_tools().await;
723
724        // Here we would send tool definitions to the MCP server
725        // This is a placeholder for the actual MCP protocol implementation
726        for tool in tools {
727            debug!("Registered tool: {} with server {}", tool.name, server_name);
728        }
729
730        Ok(())
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737    use crate::subagents::OrchestratorConfig;
738
739    #[tokio::test]
740    async fn test_mcp_tool_registration() {
741        let registry = Arc::new(crate::subagents::registry::SubagentRegistry::new().unwrap());
742        let config = OrchestratorConfig::default();
743        let orchestrator = Arc::new(AgentOrchestrator::new(
744            registry,
745            config,
746            crate::modes::OperatingMode::Build,
747        ));
748        let provider = McpAgentToolProvider::new(orchestrator);
749
750        provider.register_standard_tools().await.unwrap();
751        let tools = provider.get_tools().await;
752
753        assert!(!tools.is_empty());
754        assert!(tools.iter().any(|t| t.name == "agent_code_reviewer"));
755        assert!(tools.iter().any(|t| t.name == "agent_chain"));
756    }
757
758    #[tokio::test]
759    async fn test_json_to_params_conversion() {
760        let registry = Arc::new(crate::subagents::registry::SubagentRegistry::new().unwrap());
761        let config = OrchestratorConfig::default();
762        let orchestrator = Arc::new(AgentOrchestrator::new(
763            registry,
764            config,
765            crate::modes::OperatingMode::Build,
766        ));
767        let provider = McpAgentToolProvider::new(orchestrator);
768
769        let json = json!({
770            "file": "main.rs",
771            "line": 42,
772            "enabled": true,
773            "tags": ["rust", "async"],
774        });
775
776        let params = provider.json_to_params(json).unwrap();
777        assert_eq!(params.get("file").unwrap(), "main.rs");
778        assert_eq!(params.get("line").unwrap(), "42");
779        assert_eq!(params.get("enabled").unwrap(), "true");
780        assert!(params.get("tags").unwrap().contains("rust"));
781    }
782}