hanzo-mcp 0.8.11__py3-none-any.whl → 0.8.13__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.
- hanzo_mcp/__init__.py +1 -3
- hanzo_mcp/analytics/posthog_analytics.py +3 -9
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +6 -15
- hanzo_mcp/cli_enhanced.py +5 -14
- hanzo_mcp/cli_plugin.py +3 -9
- hanzo_mcp/config/settings.py +6 -20
- hanzo_mcp/config/tool_config.py +1 -3
- hanzo_mcp/core/base_agent.py +88 -88
- hanzo_mcp/core/model_registry.py +238 -210
- hanzo_mcp/dev_server.py +5 -15
- hanzo_mcp/prompts/__init__.py +2 -6
- hanzo_mcp/prompts/project_todo_reminder.py +3 -9
- hanzo_mcp/prompts/tool_explorer.py +1 -3
- hanzo_mcp/prompts/utils.py +7 -21
- hanzo_mcp/server.py +2 -6
- hanzo_mcp/tools/__init__.py +10 -24
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +5 -15
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +14 -41
- hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
- hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
- hanzo_mcp/tools/agent/cli_tools.py +75 -74
- hanzo_mcp/tools/agent/code_auth.py +1 -3
- hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
- hanzo_mcp/tools/agent/critic_tool.py +8 -24
- hanzo_mcp/tools/agent/iching_tool.py +12 -36
- hanzo_mcp/tools/agent/network_tool.py +7 -18
- hanzo_mcp/tools/agent/prompt.py +1 -5
- hanzo_mcp/tools/agent/review_tool.py +10 -25
- hanzo_mcp/tools/agent/swarm_alias.py +1 -3
- hanzo_mcp/tools/agent/swarm_tool.py +9 -29
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +11 -39
- hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
- hanzo_mcp/tools/common/batch_tool.py +15 -45
- hanzo_mcp/tools/common/config_tool.py +9 -28
- hanzo_mcp/tools/common/context.py +1 -3
- hanzo_mcp/tools/common/critic_tool.py +1 -3
- hanzo_mcp/tools/common/decorators.py +2 -6
- hanzo_mcp/tools/common/enhanced_base.py +2 -6
- hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
- hanzo_mcp/tools/common/forgiving_edit.py +9 -28
- hanzo_mcp/tools/common/mode.py +1 -5
- hanzo_mcp/tools/common/paginated_base.py +3 -11
- hanzo_mcp/tools/common/paginated_response.py +10 -30
- hanzo_mcp/tools/common/pagination.py +3 -9
- hanzo_mcp/tools/common/permissions.py +3 -9
- hanzo_mcp/tools/common/personality.py +9 -34
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +6 -18
- hanzo_mcp/tools/common/thinking_tool.py +1 -3
- hanzo_mcp/tools/common/tool_disable.py +2 -6
- hanzo_mcp/tools/common/tool_list.py +2 -6
- hanzo_mcp/tools/common/validation.py +1 -3
- hanzo_mcp/tools/config/config_tool.py +7 -13
- hanzo_mcp/tools/config/index_config.py +1 -3
- hanzo_mcp/tools/config/mode_tool.py +5 -15
- hanzo_mcp/tools/database/database_manager.py +3 -9
- hanzo_mcp/tools/database/graph.py +1 -3
- hanzo_mcp/tools/database/graph_add.py +3 -9
- hanzo_mcp/tools/database/graph_query.py +11 -34
- hanzo_mcp/tools/database/graph_remove.py +3 -9
- hanzo_mcp/tools/database/graph_search.py +6 -20
- hanzo_mcp/tools/database/graph_stats.py +11 -33
- hanzo_mcp/tools/database/sql.py +4 -12
- hanzo_mcp/tools/database/sql_query.py +6 -10
- hanzo_mcp/tools/database/sql_search.py +2 -6
- hanzo_mcp/tools/database/sql_stats.py +5 -15
- hanzo_mcp/tools/editor/neovim_command.py +1 -3
- hanzo_mcp/tools/editor/neovim_session.py +7 -13
- hanzo_mcp/tools/filesystem/__init__.py +2 -3
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/base.py +4 -12
- hanzo_mcp/tools/filesystem/batch_search.py +35 -115
- hanzo_mcp/tools/filesystem/content_replace.py +4 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +9 -27
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +5 -15
- hanzo_mcp/tools/filesystem/edit.py +6 -18
- hanzo_mcp/tools/filesystem/find.py +3 -9
- hanzo_mcp/tools/filesystem/find_files.py +2 -6
- hanzo_mcp/tools/filesystem/git_search.py +9 -24
- hanzo_mcp/tools/filesystem/grep.py +9 -27
- hanzo_mcp/tools/filesystem/multi_edit.py +6 -18
- hanzo_mcp/tools/filesystem/read.py +8 -26
- hanzo_mcp/tools/filesystem/rules_tool.py +6 -17
- hanzo_mcp/tools/filesystem/search_tool.py +18 -62
- hanzo_mcp/tools/filesystem/symbols_tool.py +5 -15
- hanzo_mcp/tools/filesystem/tree.py +1 -3
- hanzo_mcp/tools/filesystem/watch.py +1 -3
- hanzo_mcp/tools/filesystem/write.py +1 -3
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- hanzo_mcp/tools/jupyter/notebook_edit.py +11 -35
- hanzo_mcp/tools/jupyter/notebook_read.py +2 -6
- hanzo_mcp/tools/llm/consensus_tool.py +8 -24
- hanzo_mcp/tools/llm/llm_manage.py +2 -6
- hanzo_mcp/tools/llm/llm_tool.py +17 -58
- hanzo_mcp/tools/llm/llm_unified.py +18 -59
- hanzo_mcp/tools/llm/provider_tools.py +1 -3
- hanzo_mcp/tools/lsp/lsp_tool.py +5 -17
- hanzo_mcp/tools/mcp/mcp_add.py +1 -3
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +10 -27
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +6 -18
- hanzo_mcp/tools/search/find_tool.py +10 -32
- hanzo_mcp/tools/search/unified_search.py +24 -78
- hanzo_mcp/tools/shell/__init__.py +2 -2
- hanzo_mcp/tools/shell/auto_background.py +2 -6
- hanzo_mcp/tools/shell/base.py +1 -5
- hanzo_mcp/tools/shell/base_process.py +5 -7
- hanzo_mcp/tools/shell/bash_session.py +7 -24
- hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
- hanzo_mcp/tools/shell/bash_tool.py +3 -7
- hanzo_mcp/tools/shell/command_executor.py +26 -79
- hanzo_mcp/tools/shell/logs.py +4 -16
- hanzo_mcp/tools/shell/npx.py +2 -8
- hanzo_mcp/tools/shell/npx_tool.py +1 -3
- hanzo_mcp/tools/shell/pkill.py +4 -12
- hanzo_mcp/tools/shell/process_tool.py +2 -8
- hanzo_mcp/tools/shell/processes.py +5 -17
- hanzo_mcp/tools/shell/run_background.py +1 -3
- hanzo_mcp/tools/shell/run_command.py +1 -3
- hanzo_mcp/tools/shell/run_command_windows.py +1 -3
- hanzo_mcp/tools/shell/session_manager.py +2 -6
- hanzo_mcp/tools/shell/session_storage.py +2 -6
- hanzo_mcp/tools/shell/streaming_command.py +7 -23
- hanzo_mcp/tools/shell/uvx.py +4 -14
- hanzo_mcp/tools/shell/uvx_background.py +2 -6
- hanzo_mcp/tools/shell/uvx_tool.py +1 -3
- hanzo_mcp/tools/shell/zsh_tool.py +12 -20
- hanzo_mcp/tools/todo/todo.py +1 -3
- hanzo_mcp/tools/todo/todo_read.py +3 -9
- hanzo_mcp/tools/todo/todo_write.py +6 -18
- hanzo_mcp/tools/vector/__init__.py +3 -9
- hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
- hanzo_mcp/tools/vector/git_ingester.py +10 -30
- hanzo_mcp/tools/vector/index_tool.py +3 -9
- hanzo_mcp/tools/vector/infinity_store.py +7 -27
- hanzo_mcp/tools/vector/mock_infinity.py +1 -3
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/vector.py +2 -6
- hanzo_mcp/tools/vector/vector_index.py +8 -8
- hanzo_mcp/tools/vector/vector_search.py +7 -21
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/METADATA +2 -2
- hanzo_mcp-0.8.13.dist-info/RECORD +193 -0
- hanzo_mcp-0.8.11.dist-info/RECORD +0 -193
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.11.dist-info → hanzo_mcp-0.8.13.dist-info}/top_level.txt +0 -0
|
@@ -21,10 +21,10 @@ from ...core.model_registry import registry
|
|
|
21
21
|
|
|
22
22
|
class UnifiedCLITool(BaseTool, CLIAgent):
|
|
23
23
|
"""Unified CLI tool that combines BaseTool and CLIAgent functionality.
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
MRO: BaseTool first for proper method resolution order.
|
|
26
26
|
"""
|
|
27
|
-
|
|
27
|
+
|
|
28
28
|
def __init__(
|
|
29
29
|
self,
|
|
30
30
|
name: str,
|
|
@@ -34,7 +34,7 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
34
34
|
permission_manager: Optional[PermissionManager] = None,
|
|
35
35
|
):
|
|
36
36
|
"""Initialize unified CLI tool.
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
Args:
|
|
39
39
|
name: Tool name
|
|
40
40
|
description: Tool description
|
|
@@ -45,47 +45,47 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
45
45
|
# Initialize CLIAgent with config
|
|
46
46
|
config = AgentConfig(model=default_model)
|
|
47
47
|
CLIAgent.__init__(self, config)
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
# Store tool metadata
|
|
50
50
|
self._name = name
|
|
51
51
|
self._description = description
|
|
52
52
|
self._cli_command = cli_command
|
|
53
53
|
self.permission_manager = permission_manager
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
@property
|
|
56
56
|
def name(self) -> str:
|
|
57
57
|
return self._name
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
@property
|
|
60
60
|
def description(self) -> str:
|
|
61
61
|
return self._description
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
@property
|
|
64
64
|
def cli_command(self) -> str:
|
|
65
65
|
return self._cli_command
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
def build_command(self, prompt: str, **kwargs: Any) -> List[str]:
|
|
68
68
|
"""Build the CLI command with model-specific formatting.
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
Args:
|
|
71
71
|
prompt: The prompt
|
|
72
72
|
**kwargs: Additional parameters
|
|
73
|
-
|
|
73
|
+
|
|
74
74
|
Returns:
|
|
75
75
|
Command arguments list
|
|
76
76
|
"""
|
|
77
77
|
command = [self.cli_command]
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
# Get model config from registry
|
|
80
80
|
model_config = registry.get(self.config.model)
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
# Handle different CLI tool formats
|
|
83
83
|
if self.cli_command == "claude":
|
|
84
84
|
if model_config:
|
|
85
85
|
command.extend(["--model", model_config.full_name])
|
|
86
86
|
# Claude takes prompt via stdin
|
|
87
87
|
return command
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
elif self.cli_command == "openai":
|
|
90
90
|
# OpenAI CLI format
|
|
91
91
|
command.extend(["api", "chat.completions.create"])
|
|
@@ -93,14 +93,14 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
93
93
|
command.extend(["-m", model_config.full_name])
|
|
94
94
|
command.extend(["-g", "user", prompt])
|
|
95
95
|
return command
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
elif self.cli_command in ["gemini", "grok"]:
|
|
98
98
|
# Simple format: command --model MODEL prompt
|
|
99
99
|
if model_config:
|
|
100
100
|
command.extend(["--model", model_config.full_name])
|
|
101
101
|
command.append(prompt)
|
|
102
102
|
return command
|
|
103
|
-
|
|
103
|
+
|
|
104
104
|
elif self.cli_command == "openhands":
|
|
105
105
|
# OpenHands format
|
|
106
106
|
command.extend(["run", prompt])
|
|
@@ -109,7 +109,7 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
109
109
|
if self.config.working_dir:
|
|
110
110
|
command.extend(["--workspace", str(self.config.working_dir)])
|
|
111
111
|
return command
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
elif self.cli_command == "hanzo":
|
|
114
114
|
# Hanzo dev format
|
|
115
115
|
command.append("dev")
|
|
@@ -117,13 +117,13 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
117
117
|
command.extend(["--model", model_config.full_name])
|
|
118
118
|
command.extend(["--prompt", prompt])
|
|
119
119
|
return command
|
|
120
|
-
|
|
120
|
+
|
|
121
121
|
elif self.cli_command == "cline":
|
|
122
122
|
# Cline format
|
|
123
123
|
command.append(prompt)
|
|
124
124
|
command.append("--no-interactive")
|
|
125
125
|
return command
|
|
126
|
-
|
|
126
|
+
|
|
127
127
|
elif self.cli_command == "aider":
|
|
128
128
|
# Aider format
|
|
129
129
|
if model_config:
|
|
@@ -131,24 +131,24 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
131
131
|
command.extend(["--message", prompt])
|
|
132
132
|
command.extend(["--yes", "--no-stream"])
|
|
133
133
|
return command
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
elif self.cli_command == "ollama":
|
|
136
136
|
# Ollama format for local models
|
|
137
137
|
command.extend(["run", self.config.model.replace("ollama/", "")])
|
|
138
138
|
command.append(prompt)
|
|
139
139
|
return command
|
|
140
|
-
|
|
140
|
+
|
|
141
141
|
# Default format
|
|
142
142
|
command.append(prompt)
|
|
143
143
|
return command
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
async def call(self, ctx: Context[Any, Any, Any], **params: Any) -> str:
|
|
146
146
|
"""Execute the CLI tool via MCP interface.
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
Args:
|
|
149
149
|
ctx: MCP context
|
|
150
150
|
**params: Tool parameters
|
|
151
|
-
|
|
151
|
+
|
|
152
152
|
Returns:
|
|
153
153
|
Execution result
|
|
154
154
|
"""
|
|
@@ -159,23 +159,23 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
159
159
|
self.config.working_dir = Path(params["working_dir"])
|
|
160
160
|
if params.get("timeout"):
|
|
161
161
|
self.config.timeout = params["timeout"]
|
|
162
|
-
|
|
162
|
+
|
|
163
163
|
# Execute using base agent
|
|
164
164
|
result = await self.execute(
|
|
165
165
|
params.get("prompt", ""),
|
|
166
166
|
context=ctx,
|
|
167
167
|
)
|
|
168
|
-
|
|
168
|
+
|
|
169
169
|
return result.content
|
|
170
|
-
|
|
170
|
+
|
|
171
171
|
def register(self, mcp_server: FastMCP) -> None:
|
|
172
172
|
"""Register this tool with the MCP server.
|
|
173
|
-
|
|
173
|
+
|
|
174
174
|
Args:
|
|
175
175
|
mcp_server: The FastMCP server instance
|
|
176
176
|
"""
|
|
177
177
|
tool_self = self
|
|
178
|
-
|
|
178
|
+
|
|
179
179
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
180
180
|
async def tool_wrapper(
|
|
181
181
|
prompt: str,
|
|
@@ -195,15 +195,15 @@ class UnifiedCLITool(BaseTool, CLIAgent):
|
|
|
195
195
|
|
|
196
196
|
def create_cli_tools(permission_manager: Optional[PermissionManager] = None) -> Dict[str, UnifiedCLITool]:
|
|
197
197
|
"""Create all CLI tools with unified implementation.
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
Args:
|
|
200
200
|
permission_manager: Permission manager for access control
|
|
201
|
-
|
|
201
|
+
|
|
202
202
|
Returns:
|
|
203
203
|
Dictionary of tool name to tool instance
|
|
204
204
|
"""
|
|
205
205
|
tools = {}
|
|
206
|
-
|
|
206
|
+
|
|
207
207
|
# Define all tools with their configurations
|
|
208
208
|
tool_configs = [
|
|
209
209
|
("claude", "Execute Claude CLI for AI assistance", "claude", "claude"),
|
|
@@ -217,7 +217,7 @@ def create_cli_tools(permission_manager: Optional[PermissionManager] = None) ->
|
|
|
217
217
|
("cline", "Execute Cline for autonomous coding", "cline", "claude"),
|
|
218
218
|
("aider", "Execute Aider for AI pair programming", "aider", "gpt-4-turbo"),
|
|
219
219
|
]
|
|
220
|
-
|
|
220
|
+
|
|
221
221
|
for name, description, cli_command, default_model in tool_configs:
|
|
222
222
|
tools[name] = UnifiedCLITool(
|
|
223
223
|
name=name,
|
|
@@ -226,7 +226,7 @@ def create_cli_tools(permission_manager: Optional[PermissionManager] = None) ->
|
|
|
226
226
|
default_model=default_model,
|
|
227
227
|
permission_manager=permission_manager,
|
|
228
228
|
)
|
|
229
|
-
|
|
229
|
+
|
|
230
230
|
return tools
|
|
231
231
|
|
|
232
232
|
|
|
@@ -235,20 +235,20 @@ def register_cli_tools(
|
|
|
235
235
|
permission_manager: Optional[PermissionManager] = None,
|
|
236
236
|
) -> List[BaseTool]:
|
|
237
237
|
"""Register all CLI tools with the MCP server.
|
|
238
|
-
|
|
238
|
+
|
|
239
239
|
Args:
|
|
240
240
|
mcp_server: The FastMCP server instance
|
|
241
241
|
permission_manager: Permission manager for access control
|
|
242
|
-
|
|
242
|
+
|
|
243
243
|
Returns:
|
|
244
244
|
List of registered CLI tools
|
|
245
245
|
"""
|
|
246
246
|
tools = create_cli_tools(permission_manager)
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
# Register each tool
|
|
249
249
|
for tool in tools.values():
|
|
250
250
|
tool.register(mcp_server)
|
|
251
|
-
|
|
251
|
+
|
|
252
252
|
return list(tools.values())
|
|
253
253
|
|
|
254
254
|
|
|
@@ -256,4 +256,4 @@ __all__ = [
|
|
|
256
256
|
"UnifiedCLITool",
|
|
257
257
|
"create_cli_tools",
|
|
258
258
|
"register_cli_tools",
|
|
259
|
-
]
|
|
259
|
+
]
|
|
@@ -177,30 +177,22 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
177
177
|
|
|
178
178
|
# Validate required parameters
|
|
179
179
|
if not description:
|
|
180
|
-
await tool_ctx.error(
|
|
181
|
-
"Parameter 'description' is required but was None or empty"
|
|
182
|
-
)
|
|
180
|
+
await tool_ctx.error("Parameter 'description' is required but was None or empty")
|
|
183
181
|
return "Error: Parameter 'description' is required but was None or empty"
|
|
184
182
|
|
|
185
183
|
if not invocations:
|
|
186
|
-
await tool_ctx.error(
|
|
187
|
-
"Parameter 'invocations' is required but was None or empty"
|
|
188
|
-
)
|
|
184
|
+
await tool_ctx.error("Parameter 'invocations' is required but was None or empty")
|
|
189
185
|
return "Error: Parameter 'invocations' is required but was None or empty"
|
|
190
186
|
|
|
191
187
|
if not isinstance(invocations, list) or len(invocations) == 0:
|
|
192
188
|
await tool_ctx.error("Parameter 'invocations' must be a non-empty list")
|
|
193
189
|
return "Error: Parameter 'invocations' must be a non-empty list"
|
|
194
190
|
|
|
195
|
-
await tool_ctx.info(
|
|
196
|
-
f"Executing batch operation: {description} ({len(invocations)} invocations)"
|
|
197
|
-
)
|
|
191
|
+
await tool_ctx.info(f"Executing batch operation: {description} ({len(invocations)} invocations)")
|
|
198
192
|
|
|
199
193
|
# Execute all tool invocations in parallel
|
|
200
194
|
tasks: list[asyncio.Future[dict[str, Any]]] = []
|
|
201
|
-
invocation_map: dict[asyncio.Future[dict[str, Any]], dict[str, Any]] =
|
|
202
|
-
{}
|
|
203
|
-
) # Map task Future to invocation
|
|
195
|
+
invocation_map: dict[asyncio.Future[dict[str, Any]], dict[str, Any]] = {} # Map task Future to invocation
|
|
204
196
|
|
|
205
197
|
for i, invocation in enumerate(invocations):
|
|
206
198
|
# Extract tool name and input from invocation
|
|
@@ -213,9 +205,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
213
205
|
await tool_ctx.error(error_message)
|
|
214
206
|
# Add direct result for this invocation
|
|
215
207
|
tasks.append(asyncio.Future())
|
|
216
|
-
tasks[-1].set_result(
|
|
217
|
-
{"invocation": invocation, "result": f"Error: {error_message}"}
|
|
218
|
-
)
|
|
208
|
+
tasks[-1].set_result({"invocation": invocation, "result": f"Error: {error_message}"})
|
|
219
209
|
invocation_map[tasks[-1]] = invocation
|
|
220
210
|
continue
|
|
221
211
|
|
|
@@ -225,9 +215,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
225
215
|
await tool_ctx.error(error_message)
|
|
226
216
|
# Add direct result for this invocation
|
|
227
217
|
tasks.append(asyncio.Future())
|
|
228
|
-
tasks[-1].set_result(
|
|
229
|
-
{"invocation": invocation, "result": f"Error: {error_message}"}
|
|
230
|
-
)
|
|
218
|
+
tasks[-1].set_result({"invocation": invocation, "result": f"Error: {error_message}"})
|
|
231
219
|
invocation_map[tasks[-1]] = invocation
|
|
232
220
|
continue
|
|
233
221
|
|
|
@@ -237,9 +225,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
237
225
|
await tool_ctx.info(f"Creating task for tool: {tool_name}")
|
|
238
226
|
|
|
239
227
|
# Create coroutine for this tool execution
|
|
240
|
-
async def execute_tool(
|
|
241
|
-
tool_obj: BaseTool, tool_name: str, tool_input: dict[str, Any]
|
|
242
|
-
):
|
|
228
|
+
async def execute_tool(tool_obj: BaseTool, tool_name: str, tool_input: dict[str, Any]):
|
|
243
229
|
try:
|
|
244
230
|
await tool_ctx.info(f"Executing tool: {tool_name}")
|
|
245
231
|
result = await tool_obj.call(ctx, **tool_input)
|
|
@@ -265,9 +251,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
265
251
|
await tool_ctx.error(error_message)
|
|
266
252
|
# Add direct result for this invocation
|
|
267
253
|
tasks.append(asyncio.Future())
|
|
268
|
-
tasks[-1].set_result(
|
|
269
|
-
{"invocation": invocation, "result": f"Error: {error_message}"}
|
|
270
|
-
)
|
|
254
|
+
tasks[-1].set_result({"invocation": invocation, "result": f"Error: {error_message}"})
|
|
271
255
|
invocation_map[tasks[-1]] = invocation
|
|
272
256
|
|
|
273
257
|
# Wait for all tasks to complete
|
|
@@ -284,9 +268,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
284
268
|
tool_name: str = invocation.get("tool_name", "unknown")
|
|
285
269
|
error_message = f"Unexpected error in tool '{tool_name}': {str(e)}"
|
|
286
270
|
await tool_ctx.error(error_message)
|
|
287
|
-
results.append(
|
|
288
|
-
{"invocation": invocation, "result": f"Error: {error_message}"}
|
|
289
|
-
)
|
|
271
|
+
results.append({"invocation": invocation, "result": f"Error: {error_message}"})
|
|
290
272
|
|
|
291
273
|
# Extract cursor if provided
|
|
292
274
|
cursor = params.get("cursor")
|
|
@@ -314,9 +296,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
314
296
|
)
|
|
315
297
|
|
|
316
298
|
# Create paginated response with token awareness
|
|
317
|
-
paginated_response = create_paginated_response(
|
|
318
|
-
formatted_results, cursor=cursor, use_token_limit=True
|
|
319
|
-
)
|
|
299
|
+
paginated_response = create_paginated_response(formatted_results, cursor=cursor, use_token_limit=True)
|
|
320
300
|
|
|
321
301
|
# Convert paginated response to string format for MCP
|
|
322
302
|
if isinstance(paginated_response, dict) and "items" in paginated_response:
|
|
@@ -326,13 +306,9 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
326
306
|
# Add header
|
|
327
307
|
result_parts.append(f"=== Batch operation: {description} ===")
|
|
328
308
|
result_parts.append(f"Total invocations: {len(invocations)}")
|
|
329
|
-
result_parts.append(
|
|
330
|
-
f"Showing results: {len(paginated_response['items'])} of {len(results)}"
|
|
331
|
-
)
|
|
309
|
+
result_parts.append(f"Showing results: {len(paginated_response['items'])} of {len(results)}")
|
|
332
310
|
if paginated_response.get("hasMore"):
|
|
333
|
-
result_parts.append(
|
|
334
|
-
f"More results available - use cursor: {paginated_response.get('nextCursor')}"
|
|
335
|
-
)
|
|
311
|
+
result_parts.append(f"More results available - use cursor: {paginated_response.get('nextCursor')}")
|
|
336
312
|
result_parts.append("")
|
|
337
313
|
|
|
338
314
|
# Format each result
|
|
@@ -352,12 +328,8 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
352
328
|
|
|
353
329
|
# If there's a next cursor, we need to preserve it in the response
|
|
354
330
|
# For now, append it as a note at the end
|
|
355
|
-
if paginated_response.get("hasMore") and paginated_response.get(
|
|
356
|
-
"nextCursor"
|
|
357
|
-
):
|
|
358
|
-
formatted_output += (
|
|
359
|
-
f"\n\n[To continue, use cursor: {paginated_response['nextCursor']}]"
|
|
360
|
-
)
|
|
331
|
+
if paginated_response.get("hasMore") and paginated_response.get("nextCursor"):
|
|
332
|
+
formatted_output += f"\n\n[To continue, use cursor: {paginated_response['nextCursor']}]"
|
|
361
333
|
|
|
362
334
|
await tool_ctx.info(
|
|
363
335
|
f"Batch operation '{description}' completed with {len(paginated_response['items'])} results"
|
|
@@ -424,6 +396,4 @@ Not available: think,write,edit,multi_edit,notebook_edit
|
|
|
424
396
|
cursor: Cursor,
|
|
425
397
|
ctx: MCPContext,
|
|
426
398
|
) -> str:
|
|
427
|
-
return await tool_self.call(
|
|
428
|
-
ctx, description=description, invocations=invocations, cursor=cursor
|
|
429
|
-
)
|
|
399
|
+
return await tool_self.call(ctx, description=description, invocations=invocations, cursor=cursor)
|
|
@@ -87,17 +87,11 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
87
87
|
|
|
88
88
|
try:
|
|
89
89
|
if action == "get":
|
|
90
|
-
return await self._get_config(
|
|
91
|
-
scope, setting_path, project_name, project_path
|
|
92
|
-
)
|
|
90
|
+
return await self._get_config(scope, setting_path, project_name, project_path)
|
|
93
91
|
elif action == "set":
|
|
94
|
-
return await self._set_config(
|
|
95
|
-
scope, setting_path, value, project_name, project_path
|
|
96
|
-
)
|
|
92
|
+
return await self._set_config(scope, setting_path, value, project_name, project_path)
|
|
97
93
|
elif action == "add_server":
|
|
98
|
-
return await self._add_mcp_server(
|
|
99
|
-
server_name, server_config, scope, project_name
|
|
100
|
-
)
|
|
94
|
+
return await self._add_mcp_server(server_name, server_config, scope, project_name)
|
|
101
95
|
elif action == "remove_server":
|
|
102
96
|
return await self._remove_mcp_server(server_name, scope, project_name)
|
|
103
97
|
elif action == "enable_server":
|
|
@@ -158,10 +152,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
158
152
|
"agent": settings.agent.__dict__,
|
|
159
153
|
"vector_store": settings.vector_store.__dict__,
|
|
160
154
|
"hub_enabled": settings.hub_enabled,
|
|
161
|
-
"mcp_servers": {
|
|
162
|
-
name: server.__dict__
|
|
163
|
-
for name, server in settings.mcp_servers.items()
|
|
164
|
-
},
|
|
155
|
+
"mcp_servers": {name: server.__dict__ for name, server in settings.mcp_servers.items()},
|
|
165
156
|
"current_project": settings.current_project,
|
|
166
157
|
"projects": list(settings.projects.keys()),
|
|
167
158
|
}
|
|
@@ -239,9 +230,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
239
230
|
else:
|
|
240
231
|
return f"Error: MCP server '{server_name}' already exists"
|
|
241
232
|
|
|
242
|
-
async def _remove_mcp_server(
|
|
243
|
-
self, server_name: Optional[str], scope: str, project_name: Optional[str]
|
|
244
|
-
) -> str:
|
|
233
|
+
async def _remove_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
|
|
245
234
|
"""Remove an MCP server."""
|
|
246
235
|
if not server_name:
|
|
247
236
|
return "Error: server_name is required"
|
|
@@ -254,9 +243,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
254
243
|
else:
|
|
255
244
|
return f"Error: MCP server '{server_name}' not found"
|
|
256
245
|
|
|
257
|
-
async def _enable_mcp_server(
|
|
258
|
-
self, server_name: Optional[str], scope: str, project_name: Optional[str]
|
|
259
|
-
) -> str:
|
|
246
|
+
async def _enable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
|
|
260
247
|
"""Enable an MCP server."""
|
|
261
248
|
if not server_name:
|
|
262
249
|
return "Error: server_name is required"
|
|
@@ -269,9 +256,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
269
256
|
else:
|
|
270
257
|
return f"Error: MCP server '{server_name}' not found"
|
|
271
258
|
|
|
272
|
-
async def _disable_mcp_server(
|
|
273
|
-
self, server_name: Optional[str], scope: str, project_name: Optional[str]
|
|
274
|
-
) -> str:
|
|
259
|
+
async def _disable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
|
|
275
260
|
"""Disable an MCP server."""
|
|
276
261
|
if not server_name:
|
|
277
262
|
return "Error: server_name is required"
|
|
@@ -297,9 +282,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
297
282
|
else:
|
|
298
283
|
return f"Error: MCP server '{server_name}' not found"
|
|
299
284
|
|
|
300
|
-
async def _add_project(
|
|
301
|
-
self, project_name: Optional[str], project_path: Optional[str]
|
|
302
|
-
) -> str:
|
|
285
|
+
async def _add_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
|
|
303
286
|
"""Add a project configuration."""
|
|
304
287
|
if not project_path:
|
|
305
288
|
return "Error: project_path is required"
|
|
@@ -325,9 +308,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
|
|
|
325
308
|
else:
|
|
326
309
|
return f"Error: Project '{project_name}' already exists"
|
|
327
310
|
|
|
328
|
-
async def _set_current_project(
|
|
329
|
-
self, project_name: Optional[str], project_path: Optional[str]
|
|
330
|
-
) -> str:
|
|
311
|
+
async def _set_current_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
|
|
331
312
|
"""Set the current active project."""
|
|
332
313
|
settings = load_settings()
|
|
333
314
|
|
|
@@ -67,9 +67,7 @@ class ToolContext:
|
|
|
67
67
|
"""
|
|
68
68
|
return self._mcp_context.client_id
|
|
69
69
|
|
|
70
|
-
async def set_tool_info(
|
|
71
|
-
self, tool_name: str, execution_id: str | None = None
|
|
72
|
-
) -> None:
|
|
70
|
+
async def set_tool_info(self, tool_name: str, execution_id: str | None = None) -> None:
|
|
73
71
|
"""Set information about the currently executing tool.
|
|
74
72
|
|
|
75
73
|
Args:
|
|
@@ -153,9 +153,7 @@ Recommendations:
|
|
|
153
153
|
|
|
154
154
|
# Validate required analysis parameter
|
|
155
155
|
if not analysis:
|
|
156
|
-
await tool_ctx.error(
|
|
157
|
-
"Parameter 'analysis' is required but was None or empty"
|
|
158
|
-
)
|
|
156
|
+
await tool_ctx.error("Parameter 'analysis' is required but was None or empty")
|
|
159
157
|
return "Error: Parameter 'analysis' is required but was None or empty"
|
|
160
158
|
|
|
161
159
|
if analysis.strip() == "":
|
|
@@ -120,9 +120,7 @@ def _is_valid_context(ctx: Any) -> bool:
|
|
|
120
120
|
)
|
|
121
121
|
|
|
122
122
|
|
|
123
|
-
def mcp_tool(
|
|
124
|
-
server: Any, name: str | None = None, description: str | None = None
|
|
125
|
-
) -> Callable[[F], F]:
|
|
123
|
+
def mcp_tool(server: Any, name: str | None = None, description: str | None = None) -> Callable[[F], F]:
|
|
126
124
|
"""Enhanced MCP tool decorator that includes context normalization.
|
|
127
125
|
|
|
128
126
|
This decorator combines the standard MCP tool registration with
|
|
@@ -192,9 +190,7 @@ def create_tool_handler(server: Any, tool: Any) -> Callable[[], None]:
|
|
|
192
190
|
# Apply context normalization
|
|
193
191
|
normalized = with_context_normalization(func)
|
|
194
192
|
# Apply original decorator
|
|
195
|
-
return original_tool_decorator(name=name, description=description)(
|
|
196
|
-
normalized
|
|
197
|
-
)
|
|
193
|
+
return original_tool_decorator(name=name, description=description)(normalized)
|
|
198
194
|
|
|
199
195
|
return decorator
|
|
200
196
|
|
|
@@ -85,9 +85,7 @@ class AutoRegisterTool(BaseTool, ABC):
|
|
|
85
85
|
params = list(sig.parameters.items())
|
|
86
86
|
|
|
87
87
|
# Skip 'self' and 'ctx' parameters
|
|
88
|
-
tool_params = [
|
|
89
|
-
(name, param) for name, param in params if name not in ("self", "ctx")
|
|
90
|
-
]
|
|
88
|
+
tool_params = [(name, param) for name, param in params if name not in ("self", "ctx")]
|
|
91
89
|
|
|
92
90
|
# Create the handler function dynamically
|
|
93
91
|
async def handler(ctx: MCPContext, **kwargs: Any) -> Any:
|
|
@@ -98,6 +96,4 @@ class AutoRegisterTool(BaseTool, ABC):
|
|
|
98
96
|
normalized_handler = with_context_normalization(handler)
|
|
99
97
|
|
|
100
98
|
# Register with the server
|
|
101
|
-
mcp_server.tool(name=self.name, description=self.description)(
|
|
102
|
-
normalized_handler
|
|
103
|
-
)
|
|
99
|
+
mcp_server.tool(name=self.name, description=self.description)(normalized_handler)
|
|
@@ -143,9 +143,7 @@ class FastMCPPaginator(Generic[T]):
|
|
|
143
143
|
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
144
144
|
|
|
145
145
|
# Use provided page size or default
|
|
146
|
-
actual_page_size = min(
|
|
147
|
-
page_size or cursor_data.page_size or self.page_size, self.max_page_size
|
|
148
|
-
)
|
|
146
|
+
actual_page_size = min(page_size or cursor_data.page_size or self.page_size, self.max_page_size)
|
|
149
147
|
|
|
150
148
|
# Get starting position
|
|
151
149
|
start_idx = cursor_data.offset
|
|
@@ -207,9 +205,7 @@ class FastMCPPaginator(Generic[T]):
|
|
|
207
205
|
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
208
206
|
|
|
209
207
|
# Determine page size
|
|
210
|
-
limit = min(
|
|
211
|
-
page_size or cursor_data.page_size or self.page_size, self.max_page_size
|
|
212
|
-
)
|
|
208
|
+
limit = min(page_size or cursor_data.page_size or self.page_size, self.max_page_size)
|
|
213
209
|
|
|
214
210
|
# Execute query with cursor position
|
|
215
211
|
results = query_func(
|
|
@@ -258,9 +254,7 @@ class TokenAwarePaginator:
|
|
|
258
254
|
self.max_tokens = max_tokens
|
|
259
255
|
self.paginator = FastMCPPaginator()
|
|
260
256
|
|
|
261
|
-
def paginate_by_tokens(
|
|
262
|
-
self, items: List[Any], cursor: Optional[str] = None, estimate_func=None
|
|
263
|
-
) -> Dict[str, Any]:
|
|
257
|
+
def paginate_by_tokens(self, items: List[Any], cursor: Optional[str] = None, estimate_func=None) -> Dict[str, Any]:
|
|
264
258
|
"""Paginate items based on token count.
|
|
265
259
|
|
|
266
260
|
Args:
|
|
@@ -275,9 +269,7 @@ class TokenAwarePaginator:
|
|
|
275
269
|
|
|
276
270
|
# Default token estimation
|
|
277
271
|
if not estimate_func:
|
|
278
|
-
estimate_func = lambda x: estimate_tokens(
|
|
279
|
-
json.dumps(x) if not isinstance(x, str) else x
|
|
280
|
-
)
|
|
272
|
+
estimate_func = lambda x: estimate_tokens(json.dumps(x) if not isinstance(x, str) else x)
|
|
281
273
|
|
|
282
274
|
# Parse cursor
|
|
283
275
|
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
@@ -42,9 +42,7 @@ class ForgivingEditHelper:
|
|
|
42
42
|
return "\n".join(lines)
|
|
43
43
|
|
|
44
44
|
@staticmethod
|
|
45
|
-
def find_fuzzy_match(
|
|
46
|
-
haystack: str, needle: str, threshold: float = 0.85
|
|
47
|
-
) -> Optional[Tuple[int, int, str]]:
|
|
45
|
+
def find_fuzzy_match(haystack: str, needle: str, threshold: float = 0.85) -> Optional[Tuple[int, int, str]]:
|
|
48
46
|
"""Find a fuzzy match for the needle in the haystack.
|
|
49
47
|
|
|
50
48
|
Args:
|
|
@@ -81,13 +79,9 @@ class ForgivingEditHelper:
|
|
|
81
79
|
|
|
82
80
|
# Find end position by counting lines in needle
|
|
83
81
|
needle_lines = norm_needle.count("\n") + 1
|
|
84
|
-
end_pos = sum(
|
|
85
|
-
len(line) + 1 for line in original_lines[: lines_before + needle_lines]
|
|
86
|
-
)
|
|
82
|
+
end_pos = sum(len(line) + 1 for line in original_lines[: lines_before + needle_lines])
|
|
87
83
|
|
|
88
|
-
matched = "\n".join(
|
|
89
|
-
original_lines[lines_before : lines_before + needle_lines]
|
|
90
|
-
)
|
|
84
|
+
matched = "\n".join(original_lines[lines_before : lines_before + needle_lines])
|
|
91
85
|
return (start_pos, end_pos - 1, matched)
|
|
92
86
|
|
|
93
87
|
# Try fuzzy matching on lines
|
|
@@ -119,9 +113,7 @@ class ForgivingEditHelper:
|
|
|
119
113
|
candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
|
|
120
114
|
needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
|
|
121
115
|
|
|
122
|
-
ratio = difflib.SequenceMatcher(
|
|
123
|
-
None, candidate_norm, needle_norm
|
|
124
|
-
).ratio()
|
|
116
|
+
ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
|
|
125
117
|
|
|
126
118
|
if ratio >= threshold:
|
|
127
119
|
start_pos = sum(len(l) + 1 for l in haystack_lines[:i])
|
|
@@ -130,9 +122,7 @@ class ForgivingEditHelper:
|
|
|
130
122
|
return None
|
|
131
123
|
|
|
132
124
|
@staticmethod
|
|
133
|
-
def suggest_matches(
|
|
134
|
-
haystack: str, needle: str, max_suggestions: int = 3
|
|
135
|
-
) -> List[Tuple[float, str]]:
|
|
125
|
+
def suggest_matches(haystack: str, needle: str, max_suggestions: int = 3) -> List[Tuple[float, str]]:
|
|
136
126
|
"""Suggest possible matches when exact match fails.
|
|
137
127
|
|
|
138
128
|
Args:
|
|
@@ -154,9 +144,7 @@ class ForgivingEditHelper:
|
|
|
154
144
|
for line in haystack.split("\n"):
|
|
155
145
|
if line.strip(): # Skip empty lines
|
|
156
146
|
line_norm = ForgivingEditHelper.normalize_whitespace(line)
|
|
157
|
-
ratio = difflib.SequenceMatcher(
|
|
158
|
-
None, line_norm, needle_norm
|
|
159
|
-
).ratio()
|
|
147
|
+
ratio = difflib.SequenceMatcher(None, line_norm, needle_norm).ratio()
|
|
160
148
|
if ratio > 0.5: # Only reasonably similar lines
|
|
161
149
|
suggestions.append((ratio, line))
|
|
162
150
|
|
|
@@ -170,9 +158,7 @@ class ForgivingEditHelper:
|
|
|
170
158
|
candidate = "\n".join(candidate_lines)
|
|
171
159
|
candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
|
|
172
160
|
|
|
173
|
-
ratio = difflib.SequenceMatcher(
|
|
174
|
-
None, candidate_norm, needle_norm
|
|
175
|
-
).ratio()
|
|
161
|
+
ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
|
|
176
162
|
if ratio > 0.5:
|
|
177
163
|
suggestions.append((ratio, candidate))
|
|
178
164
|
|
|
@@ -181,9 +167,7 @@ class ForgivingEditHelper:
|
|
|
181
167
|
return suggestions[:max_suggestions]
|
|
182
168
|
|
|
183
169
|
@staticmethod
|
|
184
|
-
def create_edit_suggestion(
|
|
185
|
-
file_content: str, old_string: str, new_string: str
|
|
186
|
-
) -> dict:
|
|
170
|
+
def create_edit_suggestion(file_content: str, old_string: str, new_string: str) -> dict:
|
|
187
171
|
"""Create a helpful edit suggestion when match fails.
|
|
188
172
|
|
|
189
173
|
Args:
|
|
@@ -212,10 +196,7 @@ class ForgivingEditHelper:
|
|
|
212
196
|
if suggestions:
|
|
213
197
|
return {
|
|
214
198
|
"error": "Could not find exact or fuzzy match",
|
|
215
|
-
"suggestions": [
|
|
216
|
-
{"similarity": f"{score:.0%}", "text": text}
|
|
217
|
-
for score, text in suggestions
|
|
218
|
-
],
|
|
199
|
+
"suggestions": [{"similarity": f"{score:.0%}", "text": text} for score, text in suggestions],
|
|
219
200
|
"hint": "Try using one of these suggestions as old_string",
|
|
220
201
|
}
|
|
221
202
|
|