hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (93) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +118 -170
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +449 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +117 -99
  14. hanzo_mcp/tools/__init__.py +121 -33
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/config_tool.py +396 -0
  23. hanzo_mcp/tools/common/context.py +26 -292
  24. hanzo_mcp/tools/common/permissions.py +12 -12
  25. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  26. hanzo_mcp/tools/common/validation.py +1 -63
  27. hanzo_mcp/tools/filesystem/__init__.py +97 -57
  28. hanzo_mcp/tools/filesystem/base.py +32 -24
  29. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  30. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  31. hanzo_mcp/tools/filesystem/edit.py +279 -0
  32. hanzo_mcp/tools/filesystem/grep.py +458 -0
  33. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  34. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  35. hanzo_mcp/tools/filesystem/read.py +255 -0
  36. hanzo_mcp/tools/filesystem/unified_search.py +689 -0
  37. hanzo_mcp/tools/filesystem/write.py +156 -0
  38. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  39. hanzo_mcp/tools/jupyter/base.py +66 -57
  40. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  41. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  42. hanzo_mcp/tools/shell/__init__.py +29 -20
  43. hanzo_mcp/tools/shell/base.py +87 -45
  44. hanzo_mcp/tools/shell/bash_session.py +731 -0
  45. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  46. hanzo_mcp/tools/shell/command_executor.py +435 -384
  47. hanzo_mcp/tools/shell/run_command.py +284 -131
  48. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  49. hanzo_mcp/tools/shell/session_manager.py +196 -0
  50. hanzo_mcp/tools/shell/session_storage.py +325 -0
  51. hanzo_mcp/tools/todo/__init__.py +66 -0
  52. hanzo_mcp/tools/todo/base.py +319 -0
  53. hanzo_mcp/tools/todo/todo_read.py +148 -0
  54. hanzo_mcp/tools/todo/todo_write.py +378 -0
  55. hanzo_mcp/tools/vector/__init__.py +99 -0
  56. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  57. hanzo_mcp/tools/vector/git_ingester.py +482 -0
  58. hanzo_mcp/tools/vector/infinity_store.py +731 -0
  59. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  60. hanzo_mcp/tools/vector/project_manager.py +361 -0
  61. hanzo_mcp/tools/vector/vector_index.py +116 -0
  62. hanzo_mcp/tools/vector/vector_search.py +225 -0
  63. hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
  64. hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
  65. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
  66. hanzo_mcp/tools/agent/base_provider.py +0 -73
  67. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  68. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  69. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  70. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  71. hanzo_mcp/tools/common/error_handling.py +0 -86
  72. hanzo_mcp/tools/common/logging_config.py +0 -115
  73. hanzo_mcp/tools/common/session.py +0 -91
  74. hanzo_mcp/tools/common/think_tool.py +0 -123
  75. hanzo_mcp/tools/common/version_tool.py +0 -120
  76. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  77. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  78. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  79. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  80. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  81. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  82. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  83. hanzo_mcp/tools/project/__init__.py +0 -64
  84. hanzo_mcp/tools/project/analysis.py +0 -886
  85. hanzo_mcp/tools/project/base.py +0 -66
  86. hanzo_mcp/tools/project/project_analyze.py +0 -173
  87. hanzo_mcp/tools/shell/run_script.py +0 -215
  88. hanzo_mcp/tools/shell/script_tool.py +0 -244
  89. hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
  90. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  91. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
  92. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
  93. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
@@ -4,15 +4,19 @@ This module implements the AgentTool that allows Claude to delegate tasks to sub
4
4
  enabling concurrent execution of multiple operations and specialized processing.
5
5
  """
6
6
 
7
+ import asyncio
7
8
  import json
9
+ import re
8
10
  import time
9
11
  from collections.abc import Iterable
10
- from typing import Any, final, override
12
+ from typing import Annotated, TypedDict, Unpack, final, override
11
13
 
12
14
  import litellm
13
- from mcp.server.fastmcp import Context as MCPContext
14
- from mcp.server.fastmcp import FastMCP
15
+ from fastmcp import Context as MCPContext
16
+ from fastmcp import FastMCP
17
+ from fastmcp.server.dependencies import get_context
15
18
  from openai.types.chat import ChatCompletionMessageParam, ChatCompletionToolParam
19
+ from pydantic import Field
16
20
 
17
21
  from hanzo_mcp.tools.agent.prompt import (
18
22
  get_allowed_agent_tools,
@@ -24,12 +28,32 @@ from hanzo_mcp.tools.agent.tool_adapter import (
24
28
  convert_tools_to_openai_functions,
25
29
  )
26
30
  from hanzo_mcp.tools.common.base import BaseTool
27
- from hanzo_mcp.tools.common.context import DocumentContext, ToolContext, create_tool_context
31
+ from hanzo_mcp.tools.common.batch_tool import BatchTool
32
+ from hanzo_mcp.tools.common.context import (
33
+ ToolContext,
34
+ create_tool_context,
35
+ )
28
36
  from hanzo_mcp.tools.common.permissions import PermissionManager
29
37
  from hanzo_mcp.tools.filesystem import get_read_only_filesystem_tools
30
38
  from hanzo_mcp.tools.jupyter import get_read_only_jupyter_tools
31
- from hanzo_mcp.tools.project import get_project_tools
32
- from hanzo_mcp.tools.shell.command_executor import CommandExecutor
39
+
40
+ Prompt = Annotated[
41
+ str,
42
+ Field(
43
+ description="Task for the agent to perform (must include absolute paths starting with /)",
44
+ min_length=1,
45
+ ),
46
+ ]
47
+
48
+
49
+ class AgentToolParams(TypedDict, total=False):
50
+ """Parameters for the AgentTool.
51
+
52
+ Attributes:
53
+ prompts: Task(s) for the agent to perform (must include absolute paths starting with /)
54
+ """
55
+
56
+ prompts: str | list[str]
33
57
 
34
58
 
35
59
  @final
@@ -58,96 +82,73 @@ class AgentTool(BaseTool):
58
82
  Returns:
59
83
  Tool description
60
84
  """
61
- return """Launch one or more agents that can perform tasks using read-only tools.
62
-
63
- This tool creates agents for delegation of tasks such as multi-step searches, complex analyses,
64
- or other operations that benefit from focused processing. Multiple agents can work concurrently
65
- on independent tasks, improving performance for complex operations.
85
+ # TODO: Add glob when it is implemented
86
+ at = [t.name for t in self.available_tools]
66
87
 
67
- Each agent works with its own context and provides a response containing the results of its work.
68
- Results from all agents are combined in the final response.
88
+ return f"""Launch a new agent that has access to the following tools: {at} When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.
69
89
 
70
- Args:
71
- prompts: A list of task descriptions, where each item launches an independent agent.
90
+ When to use the Agent tool:
91
+ - If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended
72
92
 
73
- Returns:
74
- Combined results from all agent executions
75
- """
93
+ When NOT to use the Agent tool:
94
+ - If you want to read a specific file path, use the read or glob tool instead of the Agent tool, to find the match more quickly
95
+ - If you are searching for a specific class definition like \"class Foo\", use the glob tool instead, to find the match more quickly
96
+ - If you are searching for code within a specific file or set of 2-3 files, use the read tool instead of the Agent tool, to find the match more quickly
76
97
 
77
- @property
78
- @override
79
- def parameters(self) -> dict[str, Any]:
80
- """Get the parameter specifications for the tool.
81
-
82
- Returns:
83
- Parameter specifications
84
- """
85
- return {
86
- "properties": {
87
- "prompts": {
88
- "anyOf": [
89
- {
90
- "type": "array",
91
- "items": {
92
- "type": "string"
93
- },
94
- "description": "List of tasks for agents to perform concurrently"
95
- },
96
- {
97
- "type": "string",
98
- "description": "Single task for the agent to perform"
99
- }
100
- ],
101
- "description": "Task(s) for agent(s) to perform"
102
- }
103
- },
104
- "required": ["prompts"],
105
- "type": "object",
106
- }
107
-
108
- @property
109
- @override
110
- def required(self) -> list[str]:
111
- """Get the list of required parameter names.
112
-
113
- Returns:
114
- List of required parameter names
115
- """
116
- return ["prompts"]
98
+ Usage notes:
99
+ 1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses
100
+ 2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.
101
+ 3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.
102
+ 4. The agent's outputs should generally be trusted
103
+ 5.IMPORTANT: The Agent has no awareness of your context, so you must explicitly specify absolute project/file/directory paths and detailed background information about the current task. """
117
104
 
118
105
  def __init__(
119
- self, document_context: DocumentContext, permission_manager: PermissionManager, command_executor: CommandExecutor,
120
- model: str | None = None, api_key: str | None = None, max_tokens: int | None = None,
121
- max_iterations: int = 10, max_tool_uses: int = 30
106
+ self,
107
+ permission_manager: PermissionManager,
108
+ model: str | None = None,
109
+ api_key: str | None = None,
110
+ base_url: str | None = None,
111
+ max_tokens: int | None = None,
112
+ max_iterations: int = 10,
113
+ max_tool_uses: int = 30,
122
114
  ) -> None:
123
115
  """Initialize the agent tool.
124
116
 
125
117
  Args:
126
- document_context: Document context for tracking file contents
118
+
127
119
  permission_manager: Permission manager for access control
128
- command_executor: Command executor for running shell commands
129
120
  model: Optional model name override in LiteLLM format (e.g., "openai/gpt-4o")
130
121
  api_key: Optional API key for the model provider
122
+ base_url: Optional base URL for the model provider API endpoint
131
123
  max_tokens: Optional maximum tokens for model responses
132
124
  max_iterations: Maximum number of iterations for agent (default: 10)
133
125
  max_tool_uses: Maximum number of total tool uses for agent (default: 30)
134
126
  """
135
- self.document_context = document_context
127
+
136
128
  self.permission_manager = permission_manager
137
- self.command_executor = command_executor
138
129
  self.model_override = model
139
130
  self.api_key_override = api_key
131
+ self.base_url_override = base_url
140
132
  self.max_tokens_override = max_tokens
141
133
  self.max_iterations = max_iterations
142
134
  self.max_tool_uses = max_tool_uses
143
- self.available_tools :list[BaseTool] = []
144
- self.available_tools.extend(get_read_only_filesystem_tools(self.document_context, self.permission_manager))
145
- self.available_tools.extend(get_read_only_jupyter_tools(self.document_context, self.permission_manager))
146
- self.available_tools.extend(get_project_tools(self.document_context, self.permission_manager,self.command_executor))
147
-
135
+ self.available_tools: list[BaseTool] = []
136
+ self.available_tools.extend(
137
+ get_read_only_filesystem_tools(self.permission_manager)
138
+ )
139
+ self.available_tools.extend(
140
+ get_read_only_jupyter_tools(self.permission_manager)
141
+ )
142
+ self.available_tools.append(
143
+ BatchTool({t.name: t for t in self.available_tools})
144
+ )
148
145
 
149
146
  @override
150
- async def call(self, ctx: MCPContext, **params: Any) -> str:
147
+ async def call(
148
+ self,
149
+ ctx: MCPContext,
150
+ **params: Unpack[AgentToolParams],
151
+ ) -> str:
151
152
  """Execute the tool with the given parameters.
152
153
 
153
154
  Args:
@@ -158,132 +159,180 @@ Returns:
158
159
  Tool execution result
159
160
  """
160
161
  start_time = time.time()
161
-
162
+
162
163
  # Create tool context
163
164
  tool_ctx = create_tool_context(ctx)
164
- tool_ctx.set_tool_info(self.name)
165
+ await tool_ctx.set_tool_info(self.name)
165
166
 
166
- # Extract parameters
167
+ # Extract and validate parameters
167
168
  prompts = params.get("prompts")
169
+
168
170
  if prompts is None:
169
- await tool_ctx.error("Parameter 'prompts' is required but was not provided")
170
- return "Error: Parameter 'prompts' is required but was not provided"
171
+ await tool_ctx.error("No prompts provided")
172
+ return """Error: At least one prompt must be provided.
173
+
174
+ IMPORTANT REMINDER FOR CLAUDE:
175
+ The dispatch_agent tool requires prompts parameter. Please provide either:
176
+ - A single prompt as a string
177
+ - Multiple prompts as an array of strings
171
178
 
172
- if not isinstance(prompts, list) and not isinstance(prompts, str):
173
- await tool_ctx.error("Parameter 'prompts' must be a string or an array of strings")
174
- return "Error: Parameter 'prompts' must be a string or an array of strings"
179
+ Each prompt must contain absolute paths starting with /.
180
+ Example of correct usage:
181
+ - prompts: "Search for all instances of the 'config' variable in /Users/bytedance/project/hanzo-mcp"
182
+ - prompts: ["Find files in /path/to/project", "Search code in /path/to/src"]"""
175
183
 
184
+ # Handle both string and list inputs
176
185
  if isinstance(prompts, str):
177
- prompts = [prompts]
178
-
179
- if not prompts: # Empty list
180
- await tool_ctx.error("At least one prompt must be provided in the array")
181
- return "Error: At least one prompt must be provided in the array"
182
-
183
- # Check for empty strings in the list
184
- if any(not isinstance(p, str) or not p.strip() for p in prompts):
185
- await tool_ctx.error("All prompts must be non-empty strings")
186
- return "Error: All prompts must be non-empty strings"
187
-
188
- # Always use _execute_multiple_agents, treating single agent as a special case
189
- await tool_ctx.info(f"Launching {len(prompts)} agent{'s' if len(prompts) > 1 else ''}")
190
- result = await self._execute_multiple_agents(prompts, tool_ctx)
191
-
192
- # Calculate execution time
193
- execution_time = time.time() - start_time
194
-
195
- # Format the result
196
- formatted_result = self._format_result(result, execution_time, len(prompts))
197
-
198
- # Log completion
199
- await tool_ctx.info(f"Agent execution completed in {execution_time:.2f}s")
200
-
201
- return formatted_result
186
+ prompt_list = [prompts]
187
+ elif isinstance(prompts, list):
188
+ if not prompts:
189
+ await tool_ctx.error("Empty prompts list provided")
190
+ return "Error: At least one prompt must be provided when using a list."
191
+ if not all(isinstance(p, str) for p in prompts):
192
+ await tool_ctx.error("All prompts must be strings")
193
+ return "Error: All prompts in the list must be strings."
194
+ prompt_list = prompts
195
+ else:
196
+ await tool_ctx.error("Invalid prompts parameter type")
197
+ return "Error: Parameter 'prompts' must be a string or an array of strings."
198
+
199
+ # Validate absolute paths in all prompts
200
+ absolute_path_pattern = r"/(?:[^/\s]+/)*[^/\s]+"
201
+ for prompt in prompt_list:
202
+ if not re.search(absolute_path_pattern, prompt):
203
+ await tool_ctx.error(f"Prompt does not contain absolute path: {prompt[:50]}...")
204
+ return """Error: All prompts must contain at least one absolute path.
205
+
206
+ IMPORTANT REMINDER FOR CLAUDE:
207
+ When using the dispatch_agent tool, always include absolute paths in your prompts.
208
+ Example of correct usage:
209
+ - "Search for all instances of the 'config' variable in /Users/bytedance/project/hanzo-mcp"
210
+ - "Find files that import the database module in /Users/bytedance/project/hanzo-mcp/src"
211
+
212
+ The agent cannot access files without knowing their absolute locations."""
213
+
214
+ # Execute agent(s) - always use _execute_multiple_agents for list inputs
215
+ if len(prompt_list) == 1:
216
+ await tool_ctx.info("Launching agent")
217
+ result = await self._execute_multiple_agents(prompt_list, tool_ctx)
218
+ execution_time = time.time() - start_time
219
+ formatted_result = f"""Agent execution completed in {execution_time:.2f} seconds.
220
+
221
+ AGENT RESPONSE:
222
+ {result}"""
223
+ await tool_ctx.info(f"Agent execution completed in {execution_time:.2f}s")
224
+ return formatted_result
225
+ else:
226
+ await tool_ctx.info(f"Launching {len(prompt_list)} agents in parallel")
227
+ result = await self._execute_multiple_agents(prompt_list, tool_ctx)
228
+ execution_time = time.time() - start_time
229
+ formatted_result = f"""Multi-agent execution completed in {execution_time:.2f} seconds ({len(prompt_list)} agents).
230
+
231
+ AGENT RESPONSES:
232
+ {result}"""
233
+ await tool_ctx.info(f"Multi-agent execution completed in {execution_time:.2f}s")
234
+ return formatted_result
235
+
236
+ async def _execute_agent(self, prompt: str, tool_ctx: ToolContext) -> str:
237
+ """Execute a single agent with the given prompt.
238
+
239
+ Args:
240
+ prompt: The task prompt for the agent
241
+ tool_ctx: Tool context for logging
242
+
243
+ Returns:
244
+ Agent execution result
245
+ """
246
+ # Get available tools for the agent
247
+ agent_tools = get_allowed_agent_tools(
248
+ self.available_tools,
249
+ self.permission_manager,
250
+ )
251
+
252
+ # Convert tools to OpenAI format
253
+ openai_tools = convert_tools_to_openai_functions(agent_tools)
254
+
255
+ # Log execution start
256
+ await tool_ctx.info("Starting agent execution")
257
+
258
+ # Create a result container
259
+ result = ""
260
+
261
+ try:
262
+ # Create system prompt for this agent
263
+ system_prompt = get_system_prompt(
264
+ agent_tools,
265
+ self.permission_manager,
266
+ )
267
+
268
+ # Execute agent
269
+ await tool_ctx.info(f"Executing agent task: {prompt[:50]}...")
270
+ result = await self._execute_agent_with_tools(
271
+ system_prompt, prompt, agent_tools, openai_tools, tool_ctx
272
+ )
273
+ except Exception as e:
274
+ # Log and return error result
275
+ error_message = f"Error executing agent: {str(e)}"
276
+ await tool_ctx.error(error_message)
277
+ return f"Error: {error_message}"
278
+
279
+ return result if result else "No results returned from agent"
202
280
 
203
281
  async def _execute_multiple_agents(self, prompts: list[str], tool_ctx: ToolContext) -> str:
204
- """Execute multiple agents concurrently with the given prompts.
282
+ """Execute multiple agents concurrently.
205
283
 
206
284
  Args:
207
285
  prompts: List of prompts for the agents
208
286
  tool_ctx: Tool context for logging
209
287
 
210
288
  Returns:
211
- Combined agent execution results
289
+ Combined results from all agents
212
290
  """
213
- # Get available tools for the agents (do this once to avoid redundant work)
291
+ # Get available tools for the agents
214
292
  agent_tools = get_allowed_agent_tools(
215
- self.available_tools,
293
+ self.available_tools,
216
294
  self.permission_manager,
217
295
  )
218
-
219
- # Convert tools to OpenAI format (do this once to avoid redundant work)
296
+
297
+ # Convert tools to OpenAI format
220
298
  openai_tools = convert_tools_to_openai_functions(agent_tools)
221
-
222
- # Log execution start
223
- await tool_ctx.info(f"Starting execution of {len(prompts)} agent{'s' if len(prompts) > 1 else ''}")
224
-
225
- # Create a list to store the tasks
299
+
300
+ # Create system prompt for the agents
301
+ system_prompt = get_system_prompt(
302
+ agent_tools,
303
+ self.permission_manager,
304
+ )
305
+
306
+ # Create tasks for parallel execution
226
307
  tasks = []
227
- results = []
228
-
229
- # Handle exceptions for individual agent executions
230
308
  for i, prompt in enumerate(prompts):
231
- try:
232
- # Create system prompt for this agent
233
- system_prompt = get_system_prompt(
234
- agent_tools,
235
- self.permission_manager,
236
- )
237
-
238
- # Execute agent and collect the task
239
- await tool_ctx.info(f"Launching agent {i+1}/{len(prompts)}: {prompt[:50]}...")
240
- task = self._execute_agent_with_tools(
241
- system_prompt,
242
- prompt,
243
- agent_tools,
244
- openai_tools,
245
- tool_ctx
246
- )
247
- tasks.append(task)
248
- except Exception as e:
249
- # Log and add error result
250
- error_message = f"Error preparing agent {i+1}: {str(e)}"
251
- await tool_ctx.error(error_message)
252
- results.append(f"Agent {i+1} Error: {error_message}")
253
-
254
- # Execute all pending tasks concurrently
255
- if tasks:
256
- import asyncio
257
- try:
258
- # Wait for all tasks to complete
259
- completed_results = await asyncio.gather(*tasks, return_exceptions=True)
260
-
261
- # Process results, handling any exceptions
262
- for i, result in enumerate(completed_results):
263
- if isinstance(result, Exception):
264
- results.append(f"Agent {i+1} Error: {str(result)}")
265
- else:
266
- # For multi-agent case, add agent number prefix
267
- if len(prompts) > 1:
268
- results.append(f"Agent {i+1} Result:\n{result}")
269
- else:
270
- # For single agent case, just add the result
271
- results.append(result)
272
- except Exception as e:
273
- # Handle any unexpected exceptions during gathering
274
- error_message = f"Error executing agents concurrently: {str(e)}"
275
- await tool_ctx.error(error_message)
276
- results.append(f"Error: {error_message}")
277
-
278
- # Combine results - different handling for single vs multi-agent
279
- if len(prompts) > 1:
280
- # Multi-agent: add separator between results
281
- combined_result = "\n\n" + "\n\n---\n\n".join(results)
282
- else:
283
- # Single agent: just return the result
284
- combined_result = results[0] if results else "No results returned from agent"
285
-
286
- return combined_result
309
+ await tool_ctx.info(f"Creating agent task {i+1}: {prompt[:50]}...")
310
+ task = self._execute_agent_with_tools(
311
+ system_prompt, prompt, agent_tools, openai_tools, tool_ctx
312
+ )
313
+ tasks.append(task)
314
+
315
+ # Execute all agents concurrently
316
+ await tool_ctx.info(f"Executing {len(tasks)} agents in parallel")
317
+ results = await asyncio.gather(*tasks, return_exceptions=True)
318
+
319
+ # Handle single agent case
320
+ if len(results) == 1:
321
+ if isinstance(results[0], Exception):
322
+ await tool_ctx.error(f"Agent execution failed: {str(results[0])}")
323
+ return f"Error: {str(results[0])}"
324
+ return results[0]
325
+
326
+ # Format results for multiple agents
327
+ formatted_results = []
328
+ for i, result in enumerate(results):
329
+ if isinstance(result, Exception):
330
+ formatted_results.append(f"Agent {i+1} Error:\n{str(result)}")
331
+ await tool_ctx.error(f"Agent {i+1} failed: {str(result)}")
332
+ else:
333
+ formatted_results.append(f"Agent {i+1} Result:\n{result}")
334
+
335
+ return "\n\n---\n\n".join(formatted_results)
287
336
 
288
337
  async def _execute_agent_with_tools(
289
338
  self,
@@ -308,24 +357,26 @@ Returns:
308
357
  # Get model parameters and name
309
358
  model = get_default_model(self.model_override)
310
359
  params = get_model_parameters(max_tokens=self.max_tokens_override)
311
-
360
+
312
361
  # Initialize messages
313
- messages:Iterable[ChatCompletionMessageParam] = []
362
+ messages: Iterable[ChatCompletionMessageParam] = []
314
363
  messages.append({"role": "system", "content": system_prompt})
315
364
  messages.append({"role": "user", "content": user_prompt})
316
-
365
+
317
366
  # Track tool usage for metrics
318
367
  tool_usage = {}
319
368
  total_tool_use_count = 0
320
369
  iteration_count = 0
321
370
  max_tool_uses = self.max_tool_uses # Safety limit to prevent infinite loops
322
- max_iterations = self.max_iterations # Add a maximum number of iterations for safety
371
+ max_iterations = (
372
+ self.max_iterations
373
+ ) # Add a maximum number of iterations for safety
323
374
 
324
375
  # Execute until the agent completes or reaches the limit
325
376
  while total_tool_use_count < max_tool_uses and iteration_count < max_iterations:
326
377
  iteration_count += 1
327
378
  await tool_ctx.info(f"Calling model (iteration {iteration_count})...")
328
-
379
+
329
380
  try:
330
381
  # Configure model parameters based on capabilities
331
382
  completion_params = {
@@ -339,88 +390,99 @@ Returns:
339
390
 
340
391
  if self.api_key_override:
341
392
  completion_params["api_key"] = self.api_key_override
342
-
393
+
343
394
  # Add max_tokens if provided
344
395
  if params.get("max_tokens"):
345
396
  completion_params["max_tokens"] = params.get("max_tokens")
346
-
397
+
398
+ # Add base_url if provided
399
+ if self.base_url_override:
400
+ completion_params["base_url"] = self.base_url_override
401
+
347
402
  # Make the model call
348
403
  response = litellm.completion(
349
- **completion_params #pyright: ignore
404
+ **completion_params # pyright: ignore
350
405
  )
351
406
 
352
- if len(response.choices) == 0: #pyright: ignore
407
+ if len(response.choices) == 0: # pyright: ignore
353
408
  raise ValueError("No response choices returned")
354
409
 
355
- message = response.choices[0].message #pyright: ignore
410
+ message = response.choices[0].message # pyright: ignore
356
411
 
357
412
  # Add message to conversation history
358
- messages.append(message) #pyright: ignore
413
+ messages.append(message) # pyright: ignore
359
414
 
360
415
  # If no tool calls, we're done
361
416
  if not message.tool_calls:
362
417
  return message.content or "Agent completed with no response."
363
-
418
+
364
419
  # Process tool calls
365
420
  tool_call_count = len(message.tool_calls)
366
421
  await tool_ctx.info(f"Processing {tool_call_count} tool calls")
367
-
422
+
368
423
  for tool_call in message.tool_calls:
369
424
  total_tool_use_count += 1
370
425
  function_name = tool_call.function.name
371
-
426
+
372
427
  # Track usage
373
428
  tool_usage[function_name] = tool_usage.get(function_name, 0) + 1
374
-
429
+
375
430
  # Log tool usage
376
431
  await tool_ctx.info(f"Agent using tool: {function_name}")
377
-
432
+
378
433
  # Parse the arguments
379
434
  try:
380
435
  function_args = json.loads(tool_call.function.arguments)
381
436
  except json.JSONDecodeError:
382
437
  function_args = {}
383
-
438
+
384
439
  # Find the matching tool
385
- tool = next((t for t in available_tools if t.name == function_name), None)
440
+ tool = next(
441
+ (t for t in available_tools if t.name == function_name), None
442
+ )
386
443
  if not tool:
387
444
  tool_result = f"Error: Tool '{function_name}' not found"
388
445
  else:
389
446
  try:
390
- tool_result = await tool.call(ctx=tool_ctx.mcp_context, **function_args)
447
+ tool_result = await tool.call(
448
+ ctx=tool_ctx.mcp_context, **function_args
449
+ )
391
450
  except Exception as e:
392
451
  tool_result = f"Error executing {function_name}: {str(e)}"
393
-
452
+
453
+ await tool_ctx.info(
454
+ f"tool {function_name} run with args {function_args} and return {tool_result[: min(100, len(tool_result))]}"
455
+ )
394
456
  # Add the tool result to messages
395
457
  messages.append(
396
458
  {
397
459
  "role": "tool",
398
460
  "tool_call_id": tool_call.id,
461
+ "name": function_name,
399
462
  "content": tool_result,
400
463
  }
401
464
  )
402
-
465
+
403
466
  # Log progress
404
- await tool_ctx.info(f"Processed {len(message.tool_calls)} tool calls. Total: {total_tool_use_count}")
405
-
467
+ await tool_ctx.info(
468
+ f"Processed {len(message.tool_calls)} tool calls. Total: {total_tool_use_count}"
469
+ )
470
+
406
471
  except Exception as e:
407
472
  await tool_ctx.error(f"Error in model call: {str(e)}")
408
473
  # Avoid trying to JSON serialize message objects
409
474
  await tool_ctx.error(f"Message count: {len(messages)}")
410
475
  return f"Error in agent execution: {str(e)}"
411
-
476
+
412
477
  # If we've reached the limit, add a warning and get final response
413
478
  if total_tool_use_count >= max_tool_uses or iteration_count >= max_iterations:
414
- limit_type = "tool usage" if total_tool_use_count >= max_tool_uses else "iterations"
415
- await tool_ctx.info(f"Reached maximum {limit_type} limit. Getting final response.")
416
-
417
479
  messages.append(
418
480
  {
419
481
  "role": "system",
420
- "content": f"You have reached the maximum number of {limit_type}. Please provide your final response.",
482
+ "content": "You have reached the maximum iteration. Please provide your final response.",
421
483
  }
422
484
  )
423
-
485
+
424
486
  try:
425
487
  # Make a final call to get the result
426
488
  final_response = litellm.completion(
@@ -430,45 +492,49 @@ Returns:
430
492
  timeout=params["timeout"],
431
493
  max_tokens=params.get("max_tokens"),
432
494
  )
433
-
434
- return final_response.choices[0].message.content or f"Agent reached {limit_type} limit without a response." #pyright: ignore
495
+
496
+ return (
497
+ final_response.choices[0].message.content
498
+ or "Agent reached max iteration limit without a response."
499
+ ) # pyright: ignore
435
500
  except Exception as e:
436
501
  await tool_ctx.error(f"Error in final model call: {str(e)}")
437
502
  return f"Error in final response: {str(e)}"
438
-
503
+
439
504
  # Should not reach here but just in case
440
505
  return "Agent execution completed after maximum iterations."
441
506
 
442
- def _format_result(self, result: str, execution_time: float, agent_count: int) -> str:
507
+ def _format_result(self, result: str, execution_time: float) -> str:
443
508
  """Format agent result with metrics.
444
509
 
445
510
  Args:
446
- result: Raw result from agent(s)
511
+ result: Raw result from agent
447
512
  execution_time: Execution time in seconds
448
- agent_count: Number of agents used
449
513
 
450
514
  Returns:
451
515
  Formatted result with metrics
452
516
  """
453
- # Different format based on agent count
454
- if agent_count > 1:
455
- # Multi-agent response
456
- return f"""Multi-agent execution completed in {execution_time:.2f} seconds ({agent_count} agents).
457
-
458
- {result}
459
- """
460
- else:
461
- # Single agent response
462
- return f"""Agent execution completed in {execution_time:.2f} seconds.
517
+ return f"""Agent execution completed in {execution_time:.2f} seconds.
463
518
 
464
519
  AGENT RESPONSE:
465
520
  {result}
466
521
  """
467
-
522
+
468
523
  @override
469
524
  def register(self, mcp_server: FastMCP) -> None:
525
+ """Register this agent tool with the MCP server.
526
+
527
+ Creates a wrapper function with explicitly defined parameters that match
528
+ the tool's parameter schema and registers it with the MCP server.
529
+
530
+ Args:
531
+ mcp_server: The FastMCP server instance
532
+ """
470
533
  tool_self = self # Create a reference to self for use in the closure
471
-
472
- @mcp_server.tool(name=self.name, description=self.mcp_description)
473
- async def dispatch_agent(ctx: MCPContext, prompts: list[str] | str) -> str:
474
- return await tool_self.call(ctx, prompts=prompts)
534
+
535
+ @mcp_server.tool(name=self.name, description=self.description)
536
+ async def dispatch_agent(
537
+ prompts: str | list[str],
538
+ ) -> str:
539
+ ctx = get_context()
540
+ return await tool_self.call(ctx, prompts=prompts)