hanzo-mcp 0.7.3__py3-none-any.whl → 0.7.7__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 -1
- hanzo_mcp/cli.py +10 -0
- hanzo_mcp/prompts/__init__.py +43 -0
- hanzo_mcp/prompts/example_custom_prompt.py +40 -0
- hanzo_mcp/prompts/tool_explorer.py +603 -0
- hanzo_mcp/tools/__init__.py +52 -51
- hanzo_mcp/tools/agent/__init__.py +3 -16
- hanzo_mcp/tools/agent/agent_tool.py +365 -525
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +641 -0
- hanzo_mcp/tools/agent/network_tool.py +3 -5
- hanzo_mcp/tools/agent/swarm_tool.py +447 -349
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +535 -0
- hanzo_mcp/tools/agent/tool_adapter.py +21 -2
- hanzo_mcp/tools/common/forgiving_edit.py +24 -14
- hanzo_mcp/tools/common/permissions.py +8 -0
- hanzo_mcp/tools/filesystem/__init__.py +5 -5
- hanzo_mcp/tools/filesystem/{symbols.py → ast_tool.py} +8 -8
- hanzo_mcp/tools/filesystem/batch_search.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree.py +8 -1
- hanzo_mcp/tools/filesystem/find.py +1 -0
- hanzo_mcp/tools/filesystem/grep.py +11 -2
- hanzo_mcp/tools/filesystem/read.py +8 -1
- hanzo_mcp/tools/filesystem/search_tool.py +1 -1
- hanzo_mcp/tools/jupyter/__init__.py +5 -1
- hanzo_mcp/tools/jupyter/base.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +89 -18
- hanzo_mcp/tools/search/find_tool.py +49 -8
- hanzo_mcp/tools/shell/base_process.py +7 -1
- hanzo_mcp/tools/shell/streaming_command.py +34 -1
- {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/METADATA +7 -1
- {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/RECORD +34 -32
- hanzo_mcp/tools/agent/agent_tool_v2.py +0 -492
- hanzo_mcp/tools/agent/swarm_tool_v2.py +0 -654
- {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.3.dist-info → hanzo_mcp-0.7.7.dist-info}/top_level.txt +0 -0
|
@@ -66,8 +66,8 @@ class GrepAstToolParams(TypedDict):
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
@final
|
|
69
|
-
class
|
|
70
|
-
"""Tool for searching and querying code
|
|
69
|
+
class ASTTool(FilesystemBaseTool):
|
|
70
|
+
"""Tool for searching and querying code structures using tree-sitter AST parsing."""
|
|
71
71
|
|
|
72
72
|
@property
|
|
73
73
|
@override
|
|
@@ -77,7 +77,7 @@ class SymbolsTool(FilesystemBaseTool):
|
|
|
77
77
|
Returns:
|
|
78
78
|
Tool name
|
|
79
79
|
"""
|
|
80
|
-
return "
|
|
80
|
+
return "ast"
|
|
81
81
|
|
|
82
82
|
@property
|
|
83
83
|
@override
|
|
@@ -87,14 +87,14 @@ class SymbolsTool(FilesystemBaseTool):
|
|
|
87
87
|
Returns:
|
|
88
88
|
Tool description
|
|
89
89
|
"""
|
|
90
|
-
return """
|
|
90
|
+
return """AST-based code structure search using tree-sitter. Find functions, classes, methods with full context.
|
|
91
91
|
|
|
92
92
|
Usage:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
ast "function_name" ./src
|
|
94
|
+
ast "class.*Service" ./src
|
|
95
|
+
ast "def test_" ./tests
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Searches code structure intelligently, understanding syntax and providing semantic context."""
|
|
98
98
|
|
|
99
99
|
@override
|
|
100
100
|
async def call(
|
|
@@ -26,7 +26,7 @@ from typing_extensions import Annotated, TypedDict, Unpack, final, override
|
|
|
26
26
|
|
|
27
27
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
28
28
|
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
29
|
-
from hanzo_mcp.tools.filesystem.
|
|
29
|
+
from hanzo_mcp.tools.filesystem.ast_tool import ASTTool
|
|
30
30
|
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
31
31
|
from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
|
|
32
32
|
from hanzo_mcp.tools.vector.ast_analyzer import ASTAnalyzer, Symbol
|
|
@@ -122,7 +122,7 @@ class BatchSearchTool(FilesystemBaseTool):
|
|
|
122
122
|
|
|
123
123
|
# Initialize component search tools
|
|
124
124
|
self.grep_tool = Grep(permission_manager)
|
|
125
|
-
self.grep_ast_tool =
|
|
125
|
+
self.grep_ast_tool = ASTTool(permission_manager)
|
|
126
126
|
self.git_search_tool = GitSearchTool(permission_manager)
|
|
127
127
|
self.ast_analyzer = ASTAnalyzer()
|
|
128
128
|
|
|
@@ -11,6 +11,7 @@ from mcp.server import FastMCP
|
|
|
11
11
|
from pydantic import Field
|
|
12
12
|
|
|
13
13
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
14
|
+
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
14
15
|
|
|
15
16
|
DirectoryPath = Annotated[
|
|
16
17
|
str,
|
|
@@ -279,7 +280,13 @@ requested. Only works within allowed directories."""
|
|
|
279
280
|
f"Generated directory tree for {path} (depth: {depth}, include_filtered: {include_filtered})"
|
|
280
281
|
)
|
|
281
282
|
|
|
282
|
-
|
|
283
|
+
# Truncate response to stay within token limits
|
|
284
|
+
full_response = formatted_output + summary
|
|
285
|
+
return truncate_response(
|
|
286
|
+
full_response,
|
|
287
|
+
max_tokens=25000,
|
|
288
|
+
truncation_message="\n\n[Response truncated due to token limit. Please use a smaller depth, specific subdirectory, or the paginated version of this tool.]"
|
|
289
|
+
)
|
|
283
290
|
except Exception as e:
|
|
284
291
|
await tool_ctx.error(f"Error generating directory tree: {str(e)}")
|
|
285
292
|
return f"Error generating directory tree: {str(e)}"
|
|
@@ -17,6 +17,7 @@ from mcp.server.fastmcp import Context as MCPContext
|
|
|
17
17
|
from pydantic import Field
|
|
18
18
|
|
|
19
19
|
from hanzo_mcp.tools.common.context import ToolContext
|
|
20
|
+
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
20
21
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
21
22
|
|
|
22
23
|
|
|
@@ -17,6 +17,7 @@ from mcp.server import FastMCP
|
|
|
17
17
|
from pydantic import Field
|
|
18
18
|
|
|
19
19
|
from hanzo_mcp.tools.common.context import ToolContext
|
|
20
|
+
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
20
21
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
21
22
|
|
|
22
23
|
Pattern = Annotated[
|
|
@@ -422,13 +423,21 @@ When you are doing an open ended search that may require multiple rounds of glob
|
|
|
422
423
|
if self.is_ripgrep_installed():
|
|
423
424
|
await tool_ctx.info("ripgrep is installed, using ripgrep for search")
|
|
424
425
|
result = await self.run_ripgrep(pattern, path, tool_ctx, include)
|
|
425
|
-
return
|
|
426
|
+
return truncate_response(
|
|
427
|
+
result,
|
|
428
|
+
max_tokens=25000,
|
|
429
|
+
truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]"
|
|
430
|
+
)
|
|
426
431
|
else:
|
|
427
432
|
await tool_ctx.info(
|
|
428
433
|
"ripgrep is not installed, using fallback implementation"
|
|
429
434
|
)
|
|
430
435
|
result = await self.fallback_grep(pattern, path, tool_ctx, include)
|
|
431
|
-
return
|
|
436
|
+
return truncate_response(
|
|
437
|
+
result,
|
|
438
|
+
max_tokens=25000,
|
|
439
|
+
truncation_message="\n\n[Grep results truncated due to token limit. Use more specific patterns or paths to reduce output.]"
|
|
440
|
+
)
|
|
432
441
|
except Exception as e:
|
|
433
442
|
await tool_ctx.error(f"Error in grep tool: {str(e)}")
|
|
434
443
|
return f"Error in grep tool: {str(e)}"
|
|
@@ -11,6 +11,7 @@ from mcp.server import FastMCP
|
|
|
11
11
|
from pydantic import Field
|
|
12
12
|
|
|
13
13
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
14
|
+
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
14
15
|
|
|
15
16
|
FilePath = Annotated[
|
|
16
17
|
str,
|
|
@@ -219,7 +220,13 @@ Usage:
|
|
|
219
220
|
result += f"\n... (output truncated, showing {limit} of {limit + truncated_lines}+ lines)"
|
|
220
221
|
|
|
221
222
|
await tool_ctx.info(f"Successfully read file: {file_path}")
|
|
222
|
-
|
|
223
|
+
|
|
224
|
+
# Apply token limit to prevent excessive output
|
|
225
|
+
return truncate_response(
|
|
226
|
+
result,
|
|
227
|
+
max_tokens=25000,
|
|
228
|
+
truncation_message="\n\n[File content truncated due to token limit. Use offset/limit parameters to read specific sections.]"
|
|
229
|
+
)
|
|
223
230
|
|
|
224
231
|
except Exception as e:
|
|
225
232
|
await tool_ctx.error(f"Error reading file: {str(e)}")
|
|
@@ -23,7 +23,7 @@ from pydantic import Field
|
|
|
23
23
|
|
|
24
24
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
25
25
|
from hanzo_mcp.tools.filesystem.grep import Grep
|
|
26
|
-
from hanzo_mcp.tools.filesystem.
|
|
26
|
+
from hanzo_mcp.tools.filesystem.symbols_tool import SymbolsTool
|
|
27
27
|
from hanzo_mcp.tools.filesystem.git_search import GitSearchTool
|
|
28
28
|
from hanzo_mcp.tools.vector.vector_search import VectorSearchTool
|
|
29
29
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
@@ -70,13 +70,17 @@ def register_jupyter_tools(
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
tools = []
|
|
73
|
+
added_classes = set() # Track which tool classes have been added
|
|
73
74
|
|
|
74
75
|
if enabled_tools:
|
|
75
76
|
# Use individual tool configuration
|
|
76
77
|
for tool_name, enabled in enabled_tools.items():
|
|
77
78
|
if enabled and tool_name in tool_classes:
|
|
78
79
|
tool_class = tool_classes[tool_name]
|
|
79
|
-
|
|
80
|
+
# Avoid adding the same tool class multiple times
|
|
81
|
+
if tool_class not in added_classes:
|
|
82
|
+
tools.append(tool_class(permission_manager))
|
|
83
|
+
added_classes.add(tool_class)
|
|
80
84
|
else:
|
|
81
85
|
# Use all tools (backward compatibility)
|
|
82
86
|
tools = get_jupyter_tools(permission_manager)
|
hanzo_mcp/tools/jupyter/base.py
CHANGED
|
@@ -12,7 +12,7 @@ from typing import Any, final
|
|
|
12
12
|
|
|
13
13
|
from mcp.server.fastmcp import Context as MCPContext
|
|
14
14
|
|
|
15
|
-
from hanzo_mcp.tools.
|
|
15
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
16
16
|
from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
|
|
17
17
|
|
|
18
18
|
|
|
@@ -101,7 +101,7 @@ class NotebookCellSource:
|
|
|
101
101
|
self.outputs = outputs or []
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
class JupyterBaseTool(
|
|
104
|
+
class JupyterBaseTool(FilesystemBaseTool, ABC):
|
|
105
105
|
"""Base class for Jupyter notebook tools.
|
|
106
106
|
|
|
107
107
|
Provides common functionality for working with Jupyter notebooks, including
|
|
@@ -185,10 +185,11 @@ jupyter --action create "new.ipynb"
|
|
|
185
185
|
return error_msg
|
|
186
186
|
|
|
187
187
|
source = params.get("source")
|
|
188
|
-
if not source:
|
|
189
|
-
return "Error: source is required for edit action"
|
|
190
|
-
|
|
191
188
|
edit_mode = params.get("edit_mode", "replace")
|
|
189
|
+
|
|
190
|
+
# Only require source for non-delete operations
|
|
191
|
+
if edit_mode != "delete" and not source:
|
|
192
|
+
return "Error: source is required for edit action"
|
|
192
193
|
cell_id = params.get("cell_id")
|
|
193
194
|
cell_index = params.get("cell_index")
|
|
194
195
|
cell_type = params.get("cell_type")
|
|
@@ -233,18 +234,18 @@ jupyter --action create "new.ipynb"
|
|
|
233
234
|
if cell_id:
|
|
234
235
|
for cell in nb.cells:
|
|
235
236
|
if cell.get("id") == cell_id:
|
|
236
|
-
cell
|
|
237
|
+
cell["source"] = source
|
|
237
238
|
if cell_type:
|
|
238
|
-
cell
|
|
239
|
+
cell["cell_type"] = cell_type
|
|
239
240
|
self.write_notebook(nb, notebook_path)
|
|
240
241
|
return f"Successfully updated cell with ID '{cell_id}'"
|
|
241
242
|
return f"Error: Cell with ID '{cell_id}' not found"
|
|
242
243
|
|
|
243
244
|
elif cell_index is not None:
|
|
244
245
|
if 0 <= cell_index < len(nb.cells):
|
|
245
|
-
nb.cells[cell_index]
|
|
246
|
+
nb.cells[cell_index]["source"] = source
|
|
246
247
|
if cell_type:
|
|
247
|
-
nb.cells[cell_index]
|
|
248
|
+
nb.cells[cell_index]["cell_type"] = cell_type
|
|
248
249
|
self.write_notebook(nb, notebook_path)
|
|
249
250
|
return f"Successfully updated cell at index {cell_index}"
|
|
250
251
|
else:
|
|
@@ -303,21 +304,91 @@ jupyter --action create "new.ipynb"
|
|
|
303
304
|
|
|
304
305
|
def _format_cell(self, cell: dict, index: int) -> str:
|
|
305
306
|
"""Format a single cell for display."""
|
|
306
|
-
output = [f"Cell {index} ({cell.cell_type})"]
|
|
307
|
+
output = [f"Cell {index} ({cell.get('cell_type', 'unknown')})"]
|
|
307
308
|
if cell.get("id"):
|
|
308
|
-
output.append(f"ID: {cell.id}")
|
|
309
|
+
output.append(f"ID: {cell.get('id')}")
|
|
309
310
|
output.append("-" * 40)
|
|
310
|
-
|
|
311
|
+
# Get source content
|
|
312
|
+
source = cell.get("source", "")
|
|
313
|
+
if isinstance(source, list):
|
|
314
|
+
source = "".join(source)
|
|
315
|
+
output.append(source)
|
|
311
316
|
|
|
312
|
-
if cell.cell_type == "code" and cell.get("outputs"):
|
|
317
|
+
if cell.get("cell_type") == "code" and cell.get("outputs"):
|
|
313
318
|
output.append("\nOutputs:")
|
|
314
|
-
for out in cell.outputs:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
319
|
+
for out in cell.get("outputs", []):
|
|
320
|
+
out_type = out.get("output_type", "")
|
|
321
|
+
|
|
322
|
+
if out_type == "stream":
|
|
323
|
+
text = out.get("text", "")
|
|
324
|
+
if isinstance(text, list):
|
|
325
|
+
text = "".join(text)
|
|
326
|
+
name = out.get("name", "stdout")
|
|
327
|
+
output.append(f"[{name}]: {text}")
|
|
328
|
+
|
|
329
|
+
elif out_type == "execute_result":
|
|
330
|
+
exec_count = out.get("execution_count", "?")
|
|
331
|
+
data = out.get("data", {})
|
|
332
|
+
# Try to get plain text representation
|
|
333
|
+
if "text/plain" in data:
|
|
334
|
+
text_data = data["text/plain"]
|
|
335
|
+
if isinstance(text_data, list):
|
|
336
|
+
text_data = "".join(text_data)
|
|
337
|
+
output.append(f"[Out {exec_count}]: {text_data}")
|
|
338
|
+
else:
|
|
339
|
+
output.append(f"[Out {exec_count}]: {data}")
|
|
340
|
+
|
|
341
|
+
elif out_type == "error":
|
|
342
|
+
ename = out.get("ename", "Error")
|
|
343
|
+
evalue = out.get("evalue", "")
|
|
344
|
+
output.append(f"[Error]: {ename}: {evalue}")
|
|
345
|
+
# Include traceback if available
|
|
346
|
+
traceback = out.get("traceback", [])
|
|
347
|
+
if traceback:
|
|
348
|
+
output.append("Traceback:")
|
|
349
|
+
for line in traceback:
|
|
350
|
+
output.append(f" {line}")
|
|
351
|
+
|
|
352
|
+
return "\n".join(output)
|
|
353
|
+
|
|
354
|
+
def read_notebook(self, notebook_path: str) -> Any:
|
|
355
|
+
"""Read a notebook from disk using nbformat.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
notebook_path: Path to the notebook file
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
Notebook object
|
|
362
|
+
"""
|
|
363
|
+
with open(notebook_path, 'r') as f:
|
|
364
|
+
return nbformat.read(f, as_version=4)
|
|
365
|
+
|
|
366
|
+
def write_notebook(self, nb: Any, notebook_path: str) -> None:
|
|
367
|
+
"""Write a notebook to disk using nbformat.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
nb: Notebook object to write
|
|
371
|
+
notebook_path: Path to write the notebook to
|
|
372
|
+
"""
|
|
373
|
+
with open(notebook_path, 'w') as f:
|
|
374
|
+
nbformat.write(nb, f)
|
|
375
|
+
|
|
376
|
+
def format_notebook(self, nb: Any) -> str:
|
|
377
|
+
"""Format an entire notebook for display.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
nb: Notebook object
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Formatted string representation of the notebook
|
|
384
|
+
"""
|
|
385
|
+
output = []
|
|
386
|
+
output.append(f"Notebook with {len(nb.cells)} cells")
|
|
387
|
+
output.append("=" * 50)
|
|
388
|
+
|
|
389
|
+
for i, cell in enumerate(nb.cells):
|
|
390
|
+
output.append("")
|
|
391
|
+
output.append(self._format_cell(cell, i))
|
|
321
392
|
|
|
322
393
|
return "\n".join(output)
|
|
323
394
|
|
|
@@ -3,22 +3,25 @@
|
|
|
3
3
|
import os
|
|
4
4
|
import time
|
|
5
5
|
import fnmatch
|
|
6
|
+
import re
|
|
6
7
|
from typing import List, Optional, Dict, Any, Set
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from dataclasses import dataclass
|
|
9
10
|
import subprocess
|
|
10
11
|
import json
|
|
11
12
|
from datetime import datetime
|
|
13
|
+
from difflib import SequenceMatcher
|
|
12
14
|
|
|
13
15
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
14
16
|
from hanzo_mcp.tools.common.paginated_response import AutoPaginatedResponse
|
|
15
17
|
from hanzo_mcp.tools.common.decorators import with_context_normalization
|
|
16
18
|
from hanzo_mcp.types import MCPResourceDocument
|
|
17
19
|
|
|
20
|
+
# Check if ffind command is available
|
|
18
21
|
try:
|
|
19
|
-
|
|
22
|
+
subprocess.run(['ffind', '--version'], capture_output=True, check=True)
|
|
20
23
|
FFIND_AVAILABLE = True
|
|
21
|
-
except
|
|
24
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
22
25
|
FFIND_AVAILABLE = False
|
|
23
26
|
|
|
24
27
|
|
|
@@ -256,8 +259,8 @@ class FindTool(BaseTool):
|
|
|
256
259
|
if FFIND_AVAILABLE and not in_content:
|
|
257
260
|
# Use ffind for fast file discovery
|
|
258
261
|
matches = await self._find_with_ffind(
|
|
259
|
-
pattern, root_path, type, case_sensitive, regex,
|
|
260
|
-
max_depth, follow_symlinks, ignore_patterns
|
|
262
|
+
pattern, root_path, type, case_sensitive, regex, fuzzy,
|
|
263
|
+
max_depth, follow_symlinks, respect_gitignore, ignore_patterns
|
|
261
264
|
)
|
|
262
265
|
else:
|
|
263
266
|
# Fall back to Python implementation
|
|
@@ -392,8 +395,10 @@ class FindTool(BaseTool):
|
|
|
392
395
|
file_type: Optional[str],
|
|
393
396
|
case_sensitive: bool,
|
|
394
397
|
regex: bool,
|
|
398
|
+
fuzzy: bool,
|
|
395
399
|
max_depth: Optional[int],
|
|
396
400
|
follow_symlinks: bool,
|
|
401
|
+
respect_gitignore: bool,
|
|
397
402
|
ignore_patterns: Set[str]) -> List[FileMatch]:
|
|
398
403
|
"""Use ffind for fast file discovery."""
|
|
399
404
|
matches = []
|
|
@@ -407,15 +412,51 @@ class FindTool(BaseTool):
|
|
|
407
412
|
'follow_symlinks': follow_symlinks,
|
|
408
413
|
}
|
|
409
414
|
|
|
415
|
+
if fuzzy:
|
|
416
|
+
ffind_args['fuzzy'] = True
|
|
417
|
+
|
|
410
418
|
if max_depth:
|
|
411
419
|
ffind_args['max_depth'] = max_depth
|
|
412
420
|
|
|
413
421
|
try:
|
|
414
|
-
#
|
|
415
|
-
|
|
422
|
+
# Build ffind command
|
|
423
|
+
cmd = ['ffind']
|
|
424
|
+
|
|
425
|
+
if not case_sensitive:
|
|
426
|
+
cmd.append('-i')
|
|
416
427
|
|
|
417
|
-
|
|
418
|
-
|
|
428
|
+
if regex:
|
|
429
|
+
cmd.append('-E')
|
|
430
|
+
|
|
431
|
+
if fuzzy:
|
|
432
|
+
cmd.append('-f')
|
|
433
|
+
|
|
434
|
+
if follow_symlinks:
|
|
435
|
+
cmd.append('-L')
|
|
436
|
+
|
|
437
|
+
if max_depth:
|
|
438
|
+
cmd.extend(['-D', str(max_depth)])
|
|
439
|
+
|
|
440
|
+
# Add pattern and path
|
|
441
|
+
cmd.append(pattern)
|
|
442
|
+
cmd.append(str(root))
|
|
443
|
+
|
|
444
|
+
# Run ffind command
|
|
445
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
446
|
+
|
|
447
|
+
if result.returncode != 0:
|
|
448
|
+
# Fall back to Python implementation on error
|
|
449
|
+
return await self._find_with_python(
|
|
450
|
+
pattern, root, file_type, case_sensitive, regex, fuzzy,
|
|
451
|
+
False, max_depth, follow_symlinks, respect_gitignore, ignore_patterns
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
# Parse results
|
|
455
|
+
results = result.stdout.strip().split('\n') if result.stdout else []
|
|
456
|
+
|
|
457
|
+
for path in results:
|
|
458
|
+
if not path: # Skip empty lines
|
|
459
|
+
continue
|
|
419
460
|
|
|
420
461
|
# Check ignore patterns
|
|
421
462
|
if self._should_ignore(path, ignore_patterns):
|
|
@@ -14,6 +14,7 @@ from mcp.server.fastmcp import Context as MCPContext
|
|
|
14
14
|
|
|
15
15
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
16
16
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
17
|
+
from hanzo_mcp.tools.common.truncate import truncate_response
|
|
17
18
|
# Import moved to __init__ to avoid circular import
|
|
18
19
|
|
|
19
20
|
|
|
@@ -184,7 +185,12 @@ class BaseProcessTool(BaseTool):
|
|
|
184
185
|
else:
|
|
185
186
|
if output.startswith("Command failed"):
|
|
186
187
|
raise RuntimeError(output)
|
|
187
|
-
|
|
188
|
+
# Truncate output to prevent token limit issues
|
|
189
|
+
return truncate_response(
|
|
190
|
+
output,
|
|
191
|
+
max_tokens=25000,
|
|
192
|
+
truncation_message="\n\n[Command output truncated due to token limit. Output may be available in logs or files.]"
|
|
193
|
+
)
|
|
188
194
|
|
|
189
195
|
async def execute_background(
|
|
190
196
|
self,
|
|
@@ -174,6 +174,18 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
174
174
|
|
|
175
175
|
return None
|
|
176
176
|
|
|
177
|
+
async def call(self, ctx: Any, **kwargs) -> Dict[str, Any]:
|
|
178
|
+
"""MCP tool entry point.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
ctx: MCP context
|
|
182
|
+
**kwargs: Tool arguments
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
Tool result
|
|
186
|
+
"""
|
|
187
|
+
return await self.run(**kwargs)
|
|
188
|
+
|
|
177
189
|
async def run(
|
|
178
190
|
self,
|
|
179
191
|
command: Optional[str] = None,
|
|
@@ -591,4 +603,25 @@ class StreamingCommandTool(BaseProcessTool):
|
|
|
591
603
|
},
|
|
592
604
|
},
|
|
593
605
|
"required": [], # No required fields for maximum forgiveness
|
|
594
|
-
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
def get_command_args(self, command: str, **kwargs) -> List[str]:
|
|
609
|
+
"""Get the command arguments for subprocess.
|
|
610
|
+
|
|
611
|
+
Args:
|
|
612
|
+
command: The command or script to run
|
|
613
|
+
**kwargs: Additional arguments (not used for shell commands)
|
|
614
|
+
|
|
615
|
+
Returns:
|
|
616
|
+
List of command arguments for subprocess
|
|
617
|
+
"""
|
|
618
|
+
# For shell commands, we use shell=True, so return the command as-is
|
|
619
|
+
return [command]
|
|
620
|
+
|
|
621
|
+
def get_tool_name(self) -> str:
|
|
622
|
+
"""Get the name of the tool being used.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
Tool name
|
|
626
|
+
"""
|
|
627
|
+
return "streaming_command"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hanzo-mcp
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.7
|
|
4
4
|
Summary: The Zen of Hanzo MCP: One server to rule them all. The ultimate MCP that orchestrates all others.
|
|
5
5
|
Author-email: Hanzo Industries Inc <dev@hanzo.ai>
|
|
6
6
|
License: MIT
|
|
@@ -40,6 +40,10 @@ Requires-Dist: sphinx>=8.0.0; extra == "dev"
|
|
|
40
40
|
Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == "dev"
|
|
41
41
|
Requires-Dist: myst-parser>=4.0.0; extra == "dev"
|
|
42
42
|
Requires-Dist: sphinx-copybutton>=0.5.0; extra == "dev"
|
|
43
|
+
Requires-Dist: mypy>=1.10.0; extra == "dev"
|
|
44
|
+
Requires-Dist: types-aiofiles>=23.2.0; extra == "dev"
|
|
45
|
+
Requires-Dist: types-psutil>=5.9.5; extra == "dev"
|
|
46
|
+
Requires-Dist: types-setuptools>=69.5.0; extra == "dev"
|
|
43
47
|
Provides-Extra: docs
|
|
44
48
|
Requires-Dist: sphinx>=8.0.0; extra == "docs"
|
|
45
49
|
Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == "docs"
|
|
@@ -55,6 +59,8 @@ Requires-Dist: pytest-asyncio>=0.25.3; extra == "test"
|
|
|
55
59
|
Requires-Dist: twisted; extra == "test"
|
|
56
60
|
Provides-Extra: agents
|
|
57
61
|
Requires-Dist: hanzo-agents>=0.1.0; extra == "agents"
|
|
62
|
+
Provides-Extra: memory
|
|
63
|
+
Requires-Dist: hanzo-memory>=1.0.0; extra == "memory"
|
|
58
64
|
Provides-Extra: performance
|
|
59
65
|
Requires-Dist: ujson>=5.7.0; extra == "performance"
|
|
60
66
|
Requires-Dist: orjson>=3.9.0; extra == "performance"
|