1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct McpAgentTool {
32 pub name: String,
34 pub description: String,
36 pub parameters: JsonSchema,
38 pub agent_name: String,
40 pub requires_confirmation: bool,
42 pub timeout_seconds: Option<u64>,
44}
45
46#[derive(Debug, Clone)]
48pub struct McpAgentToolProvider {
49 tools: Arc<RwLock<HashMap<String, McpAgentTool>>>,
51 orchestrator: Arc<AgentOrchestrator>,
53}
54
55impl McpAgentToolProvider {
56 pub fn new(orchestrator: Arc<AgentOrchestrator>) -> Self {
58 Self {
59 tools: Arc::new(RwLock::new(HashMap::new())),
60 orchestrator,
61 }
62 }
63
64 pub async fn register_standard_tools(&self) -> SubagentResult<()> {
66 let mut tools = self.tools.write().await;
67
68 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 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 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 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 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 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 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 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(), requires_confirmation: true,
365 timeout_seconds: Some(600),
366 },
367 );
368
369 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(), requires_confirmation: true,
404 timeout_seconds: Some(600),
405 },
406 );
407
408 Ok(())
409 }
410
411 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 pub async fn discover_mcp_tools(&self, _server_name: &str) -> SubagentResult<Vec<String>> {
427 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 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 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 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 let shared_context = SharedContext::new();
476 let result = self
477 .orchestrator
478 .execute_single(invocation, &shared_context)
479 .await?;
480
481 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 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 let shared_context = SharedContext::new();
521 match self
522 .orchestrator
523 .execute_single(invocation, &shared_context)
524 .await
525 {
526 Ok(result) => {
527 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 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 let shared_context = SharedContext::new();
608 let result = orchestrator
609 .execute_single(invocation, &shared_context)
610 .await;
611 drop(permit); 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 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 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
694pub 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 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 pub async fn register_with_server(&self, server_name: &str) -> SubagentResult<()> {
719 info!("Registering agent tools with MCP server: {}", server_name);
720
721 let tools = self.provider.get_tools().await;
723
724 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}