hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.0__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 +4 -17
- hanzo_mcp/bridge.py +9 -25
- hanzo_mcp/cli.py +8 -17
- 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 +2 -4
- 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 +6 -7
- hanzo_mcp/tools/__init__.py +29 -32
- hanzo_mcp/tools/agent/__init__.py +2 -1
- hanzo_mcp/tools/agent/agent.py +10 -30
- hanzo_mcp/tools/agent/agent_tool.py +23 -17
- 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 +76 -75
- 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/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/path_utils.py +34 -0
- hanzo_mcp/tools/common/permissions.py +14 -13
- hanzo_mcp/tools/common/personality.py +983 -701
- hanzo_mcp/tools/common/plugin_loader.py +3 -15
- hanzo_mcp/tools/common/stats.py +7 -19
- 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/compiler/__init__.py +8 -0
- hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
- 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/environment/__init__.py +8 -0
- hanzo_mcp/tools/environment/environment_detector.py +594 -0
- hanzo_mcp/tools/filesystem/__init__.py +28 -26
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
- hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
- hanzo_mcp/tools/filesystem/base.py +20 -12
- hanzo_mcp/tools/filesystem/content_replace.py +7 -12
- hanzo_mcp/tools/filesystem/diff.py +2 -10
- hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
- hanzo_mcp/tools/filesystem/edit.py +10 -18
- hanzo_mcp/tools/filesystem/find.py +312 -179
- hanzo_mcp/tools/filesystem/git_search.py +12 -24
- hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
- hanzo_mcp/tools/filesystem/read.py +14 -30
- hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
- hanzo_mcp/tools/filesystem/search.py +1160 -0
- hanzo_mcp/tools/filesystem/watch.py +2 -4
- hanzo_mcp/tools/filesystem/write.py +7 -10
- hanzo_mcp/tools/framework/__init__.py +8 -0
- hanzo_mcp/tools/framework/framework_modes.py +714 -0
- hanzo_mcp/tools/jupyter/base.py +6 -20
- hanzo_mcp/tools/jupyter/jupyter.py +4 -12
- 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 +621 -481
- hanzo_mcp/tools/mcp/mcp_add.py +3 -5
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
- hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
- hanzo_mcp/tools/memory/__init__.py +33 -40
- hanzo_mcp/tools/memory/conversation_memory.py +636 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
- hanzo_mcp/tools/memory/memory_tools.py +7 -19
- hanzo_mcp/tools/search/find_tool.py +12 -34
- hanzo_mcp/tools/search/unified_search.py +27 -81
- hanzo_mcp/tools/shell/__init__.py +16 -4
- 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/run_tool.py +56 -0
- 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/vector/__init__.py +97 -50
- 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 +11 -30
- hanzo_mcp/tools/vector/mock_infinity.py +159 -0
- hanzo_mcp/tools/vector/node_tool.py +538 -0
- hanzo_mcp/tools/vector/project_manager.py +4 -12
- hanzo_mcp/tools/vector/unified_vector.py +384 -0
- 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.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
- hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
- hanzo_mcp/tools/agent/swarm_tool.py +0 -723
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
- hanzo_mcp/tools/filesystem/batch_search.py +0 -900
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
- hanzo_mcp/tools/filesystem/find_files.py +0 -369
- hanzo_mcp/tools/filesystem/grep.py +0 -467
- hanzo_mcp/tools/filesystem/search_tool.py +0 -767
- hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
- hanzo_mcp/tools/filesystem/tree.py +0 -270
- hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
- hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
- hanzo_mcp/tools/todo/todo_read.py +0 -143
- hanzo_mcp/tools/todo/todo_write.py +0 -374
- hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
hanzo_mcp/tools/common/mode.py
CHANGED
|
@@ -94,11 +94,7 @@ def register_default_modes():
|
|
|
94
94
|
def get_mode_from_env() -> Optional[str]:
|
|
95
95
|
"""Get mode name from environment variables."""
|
|
96
96
|
# Check for HANZO_MODE, PERSONALITY, or MODE env vars
|
|
97
|
-
return (
|
|
98
|
-
os.environ.get("HANZO_MODE")
|
|
99
|
-
or os.environ.get("PERSONALITY")
|
|
100
|
-
or os.environ.get("MODE")
|
|
101
|
-
)
|
|
97
|
+
return os.environ.get("HANZO_MODE") or os.environ.get("PERSONALITY") or os.environ.get("MODE")
|
|
102
98
|
|
|
103
99
|
|
|
104
100
|
def activate_mode_from_env():
|
|
@@ -68,11 +68,7 @@ class PaginatedBaseTool(BaseTool):
|
|
|
68
68
|
if cursor:
|
|
69
69
|
# For continuation, check if we have cached results
|
|
70
70
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
71
|
-
if
|
|
72
|
-
cursor_data
|
|
73
|
-
and "tool" in cursor_data
|
|
74
|
-
and cursor_data["tool"] != self.name
|
|
75
|
-
):
|
|
71
|
+
if cursor_data and "tool" in cursor_data and cursor_data["tool"] != self.name:
|
|
76
72
|
return {"error": "Cursor is for a different tool", "code": -32602}
|
|
77
73
|
|
|
78
74
|
# Execute the tool
|
|
@@ -90,15 +86,11 @@ class PaginatedBaseTool(BaseTool):
|
|
|
90
86
|
if isinstance(paginated_result, dict) and "nextCursor" in paginated_result:
|
|
91
87
|
# Enhance the cursor with tool information
|
|
92
88
|
if "nextCursor" in paginated_result:
|
|
93
|
-
cursor_data = CursorManager.parse_cursor(
|
|
94
|
-
paginated_result["nextCursor"]
|
|
95
|
-
)
|
|
89
|
+
cursor_data = CursorManager.parse_cursor(paginated_result["nextCursor"])
|
|
96
90
|
if cursor_data:
|
|
97
91
|
cursor_data["tool"] = self.name
|
|
98
92
|
cursor_data["params"] = params # Store params for continuation
|
|
99
|
-
paginated_result["nextCursor"] = CursorManager.create_cursor(
|
|
100
|
-
cursor_data
|
|
101
|
-
)
|
|
93
|
+
paginated_result["nextCursor"] = CursorManager.create_cursor(cursor_data)
|
|
102
94
|
|
|
103
95
|
return paginated_result
|
|
104
96
|
else:
|
|
@@ -35,31 +35,21 @@ class AutoPaginatedResponse:
|
|
|
35
35
|
"""
|
|
36
36
|
# Handle different content types
|
|
37
37
|
if isinstance(content, str):
|
|
38
|
-
return AutoPaginatedResponse._handle_string_response(
|
|
39
|
-
content, cursor, max_tokens
|
|
40
|
-
)
|
|
38
|
+
return AutoPaginatedResponse._handle_string_response(content, cursor, max_tokens)
|
|
41
39
|
elif isinstance(content, list):
|
|
42
|
-
return AutoPaginatedResponse._handle_list_response(
|
|
43
|
-
content, cursor, max_tokens
|
|
44
|
-
)
|
|
40
|
+
return AutoPaginatedResponse._handle_list_response(content, cursor, max_tokens)
|
|
45
41
|
elif isinstance(content, dict):
|
|
46
42
|
# If dict already has pagination info, return as-is
|
|
47
43
|
if "nextCursor" in content or "cursor" in content:
|
|
48
44
|
return content
|
|
49
45
|
# Otherwise treat as single item
|
|
50
|
-
return AutoPaginatedResponse._handle_dict_response(
|
|
51
|
-
content, cursor, max_tokens
|
|
52
|
-
)
|
|
46
|
+
return AutoPaginatedResponse._handle_dict_response(content, cursor, max_tokens)
|
|
53
47
|
else:
|
|
54
48
|
# Convert to string for other types
|
|
55
|
-
return AutoPaginatedResponse._handle_string_response(
|
|
56
|
-
str(content), cursor, max_tokens
|
|
57
|
-
)
|
|
49
|
+
return AutoPaginatedResponse._handle_string_response(str(content), cursor, max_tokens)
|
|
58
50
|
|
|
59
51
|
@staticmethod
|
|
60
|
-
def _handle_string_response(
|
|
61
|
-
content: str, cursor: Optional[str], max_tokens: int
|
|
62
|
-
) -> Dict[str, Any]:
|
|
52
|
+
def _handle_string_response(content: str, cursor: Optional[str], max_tokens: int) -> Dict[str, Any]:
|
|
63
53
|
"""Handle pagination for string responses."""
|
|
64
54
|
# Parse cursor to get offset
|
|
65
55
|
offset = 0
|
|
@@ -124,9 +114,7 @@ class AutoPaginatedResponse:
|
|
|
124
114
|
return response
|
|
125
115
|
|
|
126
116
|
@staticmethod
|
|
127
|
-
def _handle_list_response(
|
|
128
|
-
items: List[Any], cursor: Optional[str], max_tokens: int
|
|
129
|
-
) -> Dict[str, Any]:
|
|
117
|
+
def _handle_list_response(items: List[Any], cursor: Optional[str], max_tokens: int) -> Dict[str, Any]:
|
|
130
118
|
"""Handle pagination for list responses."""
|
|
131
119
|
# Parse cursor to get offset
|
|
132
120
|
offset = 0
|
|
@@ -164,9 +152,7 @@ class AutoPaginatedResponse:
|
|
|
164
152
|
truncated = item[:5000] + "... [truncated]"
|
|
165
153
|
result_items.append(truncated)
|
|
166
154
|
else:
|
|
167
|
-
result_items.append(
|
|
168
|
-
{"error": "Item too large", "index": item_index}
|
|
169
|
-
)
|
|
155
|
+
result_items.append({"error": "Item too large", "index": item_index})
|
|
170
156
|
item_index += 1
|
|
171
157
|
break
|
|
172
158
|
|
|
@@ -199,9 +185,7 @@ class AutoPaginatedResponse:
|
|
|
199
185
|
return response
|
|
200
186
|
|
|
201
187
|
@staticmethod
|
|
202
|
-
def _handle_dict_response(
|
|
203
|
-
content: Dict[str, Any], cursor: Optional[str], max_tokens: int
|
|
204
|
-
) -> Dict[str, Any]:
|
|
188
|
+
def _handle_dict_response(content: Dict[str, Any], cursor: Optional[str], max_tokens: int) -> Dict[str, Any]:
|
|
205
189
|
"""Handle pagination for dict responses."""
|
|
206
190
|
# For dicts, check if it's too large as-is
|
|
207
191
|
content_str = json.dumps(content, indent=2)
|
|
@@ -280,17 +264,13 @@ def paginate_if_needed(
|
|
|
280
264
|
Original response if small enough, otherwise paginated dict
|
|
281
265
|
"""
|
|
282
266
|
# Quick check - if response is already paginated, return as-is
|
|
283
|
-
if isinstance(response, dict) and (
|
|
284
|
-
"nextCursor" in response or "pagination_info" in response
|
|
285
|
-
):
|
|
267
|
+
if isinstance(response, dict) and ("nextCursor" in response or "pagination_info" in response):
|
|
286
268
|
return response
|
|
287
269
|
|
|
288
270
|
# For small responses, don't paginate unless forced
|
|
289
271
|
if not force_pagination:
|
|
290
272
|
try:
|
|
291
|
-
response_str = (
|
|
292
|
-
json.dumps(response) if not isinstance(response, str) else response
|
|
293
|
-
)
|
|
273
|
+
response_str = json.dumps(response) if not isinstance(response, str) else response
|
|
294
274
|
if len(response_str) < 10000: # Quick heuristic
|
|
295
275
|
return response
|
|
296
276
|
except Exception:
|
|
@@ -154,9 +154,7 @@ class StreamPaginator(Generic[T]):
|
|
|
154
154
|
"""
|
|
155
155
|
self.page_size = page_size
|
|
156
156
|
|
|
157
|
-
def paginate_stream(
|
|
158
|
-
self, stream_generator, cursor: Optional[str] = None
|
|
159
|
-
) -> PaginatedResponse[T]:
|
|
157
|
+
def paginate_stream(self, stream_generator, cursor: Optional[str] = None) -> PaginatedResponse[T]:
|
|
160
158
|
"""Paginate results from a stream/generator.
|
|
161
159
|
|
|
162
160
|
Args:
|
|
@@ -185,18 +183,14 @@ class StreamPaginator(Generic[T]):
|
|
|
185
183
|
items.append(item)
|
|
186
184
|
if len(items) >= self.page_size:
|
|
187
185
|
# We have a full page, create cursor for next page
|
|
188
|
-
next_cursor = CursorManager.create_cursor(
|
|
189
|
-
{"skip": skip_count + len(items)}
|
|
190
|
-
)
|
|
186
|
+
next_cursor = CursorManager.create_cursor({"skip": skip_count + len(items)})
|
|
191
187
|
return PaginatedResponse(items=items, next_cursor=next_cursor)
|
|
192
188
|
|
|
193
189
|
# No more items
|
|
194
190
|
return PaginatedResponse(items=items, next_cursor=None)
|
|
195
191
|
|
|
196
192
|
|
|
197
|
-
def paginate_list(
|
|
198
|
-
items: List[T], cursor: Optional[str] = None, page_size: int = 100
|
|
199
|
-
) -> PaginatedResponse[T]:
|
|
193
|
+
def paginate_list(items: List[T], cursor: Optional[str] = None, page_size: int = 100) -> PaginatedResponse[T]:
|
|
200
194
|
"""Convenience function to paginate a list.
|
|
201
195
|
|
|
202
196
|
Args:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Path utility functions for consistent path handling across MCP tools."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_path(path: str) -> str:
|
|
8
|
+
"""Resolve a path by expanding user home directory (~) and environment variables.
|
|
9
|
+
|
|
10
|
+
This is the centralized path resolution function used by all filesystem tools
|
|
11
|
+
to ensure consistent path handling across the MCP system.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
path: The path to resolve, may contain ~ or environment variables
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
The fully resolved absolute path
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
>>> resolve_path('~/Documents/file.txt')
|
|
21
|
+
'/Users/username/Documents/file.txt'
|
|
22
|
+
|
|
23
|
+
>>> os.environ['MYDIR'] = '/tmp/test'
|
|
24
|
+
>>> resolve_path('$MYDIR/file.txt')
|
|
25
|
+
'/tmp/test/file.txt'
|
|
26
|
+
"""
|
|
27
|
+
# First expand environment variables, then user home directory
|
|
28
|
+
# This order is important for cases like $HOME_ALIAS where the env var contains ~
|
|
29
|
+
expanded = os.path.expandvars(path)
|
|
30
|
+
expanded = os.path.expanduser(expanded)
|
|
31
|
+
|
|
32
|
+
# Convert to Path object and resolve to get absolute path
|
|
33
|
+
# This handles relative paths and resolves symlinks
|
|
34
|
+
return str(Path(expanded).resolve())
|
|
@@ -8,6 +8,9 @@ from typing import Any, TypeVar, final
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from collections.abc import Callable, Awaitable
|
|
10
10
|
|
|
11
|
+
# Import centralized path resolution
|
|
12
|
+
from hanzo_mcp.tools.common.path_utils import resolve_path
|
|
13
|
+
|
|
11
14
|
# Define type variables for better type annotations
|
|
12
15
|
T = TypeVar("T")
|
|
13
16
|
P = TypeVar("P")
|
|
@@ -81,7 +84,8 @@ class PermissionManager:
|
|
|
81
84
|
Args:
|
|
82
85
|
path: The path to allow
|
|
83
86
|
"""
|
|
84
|
-
|
|
87
|
+
# Use centralized path resolution
|
|
88
|
+
resolved_path = Path(resolve_path(path))
|
|
85
89
|
self.allowed_paths.add(resolved_path)
|
|
86
90
|
|
|
87
91
|
def remove_allowed_path(self, path: str) -> None:
|
|
@@ -90,7 +94,8 @@ class PermissionManager:
|
|
|
90
94
|
Args:
|
|
91
95
|
path: The path to remove
|
|
92
96
|
"""
|
|
93
|
-
|
|
97
|
+
# Use centralized path resolution
|
|
98
|
+
resolved_path = Path(resolve_path(path))
|
|
94
99
|
if resolved_path in self.allowed_paths:
|
|
95
100
|
self.allowed_paths.remove(resolved_path)
|
|
96
101
|
|
|
@@ -100,7 +105,8 @@ class PermissionManager:
|
|
|
100
105
|
Args:
|
|
101
106
|
path: The path to exclude
|
|
102
107
|
"""
|
|
103
|
-
|
|
108
|
+
# Use centralized path resolution
|
|
109
|
+
resolved_path = Path(resolve_path(path))
|
|
104
110
|
self.excluded_paths.add(resolved_path)
|
|
105
111
|
|
|
106
112
|
def add_exclusion_pattern(self, pattern: str) -> None:
|
|
@@ -120,7 +126,8 @@ class PermissionManager:
|
|
|
120
126
|
Returns:
|
|
121
127
|
True if the path is allowed, False otherwise
|
|
122
128
|
"""
|
|
123
|
-
|
|
129
|
+
# Use centralized path resolution
|
|
130
|
+
resolved_path = Path(resolve_path(path))
|
|
124
131
|
|
|
125
132
|
# Check exclusions first
|
|
126
133
|
if self._is_path_excluded(resolved_path):
|
|
@@ -225,13 +232,9 @@ class PermissibleOperation:
|
|
|
225
232
|
"""
|
|
226
233
|
self.permission_manager: PermissionManager = permission_manager
|
|
227
234
|
self.operation: str = operation
|
|
228
|
-
self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None =
|
|
229
|
-
get_path_fn
|
|
230
|
-
)
|
|
235
|
+
self.get_path_fn: Callable[[list[Any], dict[str, Any]], str] | None = get_path_fn
|
|
231
236
|
|
|
232
|
-
def __call__(
|
|
233
|
-
self, func: Callable[..., Awaitable[T]]
|
|
234
|
-
) -> Callable[..., Awaitable[T]]:
|
|
237
|
+
def __call__(self, func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
|
|
235
238
|
"""Decorate the function.
|
|
236
239
|
|
|
237
240
|
Args:
|
|
@@ -255,9 +258,7 @@ class PermissibleOperation:
|
|
|
255
258
|
|
|
256
259
|
# Check permission
|
|
257
260
|
if not self.permission_manager.is_path_allowed(path):
|
|
258
|
-
raise PermissionError(
|
|
259
|
-
f"Operation '{self.operation}' not allowed for path: {path}"
|
|
260
|
-
)
|
|
261
|
+
raise PermissionError(f"Operation '{self.operation}' not allowed for path: {path}")
|
|
261
262
|
|
|
262
263
|
# Call the function
|
|
263
264
|
return await func(*args, **kwargs)
|