hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.1.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
"""Watch files for changes."""
|
|
2
2
|
|
|
3
|
+
import time
|
|
3
4
|
import asyncio
|
|
4
|
-
import os
|
|
5
|
-
from pathlib import Path
|
|
6
5
|
from typing import override
|
|
7
|
-
import
|
|
6
|
+
from pathlib import Path
|
|
8
7
|
from datetime import datetime
|
|
9
8
|
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
10
|
from mcp.server.fastmcp import Context as MCPContext
|
|
11
11
|
|
|
12
12
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
13
|
-
from mcp.server import FastMCP
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
class WatchTool(BaseTool):
|
|
17
16
|
"""Tool for watching files for changes."""
|
|
18
17
|
|
|
19
18
|
name = "watch"
|
|
20
|
-
|
|
19
|
+
|
|
21
20
|
def register(self, server: FastMCP) -> None:
|
|
22
21
|
"""Register the tool with the MCP server."""
|
|
23
|
-
|
|
22
|
+
|
|
24
23
|
@server.tool(name=self.name, description=self.description)
|
|
25
24
|
async def watch_handler(
|
|
26
25
|
ctx: MCPContext,
|
|
@@ -29,7 +28,7 @@ class WatchTool(BaseTool):
|
|
|
29
28
|
interval: int = 1,
|
|
30
29
|
recursive: bool = True,
|
|
31
30
|
exclude: str = "",
|
|
32
|
-
duration: int = 30
|
|
31
|
+
duration: int = 30,
|
|
33
32
|
) -> str:
|
|
34
33
|
"""Handle watch tool calls."""
|
|
35
34
|
return await self.run(
|
|
@@ -41,7 +40,7 @@ class WatchTool(BaseTool):
|
|
|
41
40
|
exclude=exclude,
|
|
42
41
|
duration=duration,
|
|
43
42
|
)
|
|
44
|
-
|
|
43
|
+
|
|
45
44
|
async def call(self, ctx: MCPContext, **params) -> str:
|
|
46
45
|
"""Call the tool with arguments."""
|
|
47
46
|
return await self.run(
|
|
@@ -51,7 +50,7 @@ class WatchTool(BaseTool):
|
|
|
51
50
|
interval=params.get("interval", 1),
|
|
52
51
|
recursive=params.get("recursive", True),
|
|
53
52
|
exclude=params.get("exclude", ""),
|
|
54
|
-
duration=params.get("duration", 30)
|
|
53
|
+
duration=params.get("duration", 30),
|
|
55
54
|
)
|
|
56
55
|
|
|
57
56
|
@property
|
|
@@ -68,8 +67,8 @@ watch . --recursive --exclude "__pycache__"
|
|
|
68
67
|
|
|
69
68
|
@override
|
|
70
69
|
async def run(
|
|
71
|
-
self,
|
|
72
|
-
ctx: MCPContext,
|
|
70
|
+
self,
|
|
71
|
+
ctx: MCPContext,
|
|
73
72
|
path: str,
|
|
74
73
|
pattern: str = "*",
|
|
75
74
|
interval: int = 1,
|
|
@@ -78,7 +77,7 @@ watch . --recursive --exclude "__pycache__"
|
|
|
78
77
|
duration: int = 30,
|
|
79
78
|
) -> str:
|
|
80
79
|
"""Watch files for changes.
|
|
81
|
-
|
|
80
|
+
|
|
82
81
|
Args:
|
|
83
82
|
ctx: MCP context
|
|
84
83
|
path: Path to watch (file or directory)
|
|
@@ -87,23 +86,23 @@ watch . --recursive --exclude "__pycache__"
|
|
|
87
86
|
recursive: Watch subdirectories (default: True)
|
|
88
87
|
exclude: Patterns to exclude (comma-separated)
|
|
89
88
|
duration: Max watch duration in seconds (default: 30)
|
|
90
|
-
|
|
89
|
+
|
|
91
90
|
Returns:
|
|
92
91
|
Report of file changes
|
|
93
92
|
"""
|
|
94
93
|
watch_path = Path(path).expanduser().resolve()
|
|
95
|
-
|
|
94
|
+
|
|
96
95
|
if not watch_path.exists():
|
|
97
96
|
raise ValueError(f"Path does not exist: {watch_path}")
|
|
98
|
-
|
|
97
|
+
|
|
99
98
|
# Parse exclude patterns
|
|
100
99
|
exclude_patterns = [p.strip() for p in exclude.split(",") if p.strip()]
|
|
101
|
-
|
|
100
|
+
|
|
102
101
|
# Track file states
|
|
103
102
|
file_states = {}
|
|
104
103
|
changes = []
|
|
105
104
|
start_time = time.time()
|
|
106
|
-
|
|
105
|
+
|
|
107
106
|
def should_exclude(file_path: Path) -> bool:
|
|
108
107
|
"""Check if file should be excluded."""
|
|
109
108
|
for pattern in exclude_patterns:
|
|
@@ -112,17 +111,17 @@ watch . --recursive --exclude "__pycache__"
|
|
|
112
111
|
if file_path.match(pattern):
|
|
113
112
|
return True
|
|
114
113
|
return False
|
|
115
|
-
|
|
114
|
+
|
|
116
115
|
def get_files() -> dict[Path, float]:
|
|
117
116
|
"""Get all matching files with their modification times."""
|
|
118
117
|
files = {}
|
|
119
|
-
|
|
118
|
+
|
|
120
119
|
if watch_path.is_file():
|
|
121
120
|
# Watching a single file
|
|
122
121
|
if not should_exclude(watch_path):
|
|
123
122
|
try:
|
|
124
123
|
files[watch_path] = watch_path.stat().st_mtime
|
|
125
|
-
except:
|
|
124
|
+
except Exception:
|
|
126
125
|
pass
|
|
127
126
|
else:
|
|
128
127
|
# Watching a directory
|
|
@@ -130,33 +129,33 @@ watch . --recursive --exclude "__pycache__"
|
|
|
130
129
|
paths = watch_path.rglob(pattern)
|
|
131
130
|
else:
|
|
132
131
|
paths = watch_path.glob(pattern)
|
|
133
|
-
|
|
132
|
+
|
|
134
133
|
for file_path in paths:
|
|
135
134
|
if file_path.is_file() and not should_exclude(file_path):
|
|
136
135
|
try:
|
|
137
136
|
files[file_path] = file_path.stat().st_mtime
|
|
138
|
-
except:
|
|
137
|
+
except Exception:
|
|
139
138
|
pass
|
|
140
|
-
|
|
139
|
+
|
|
141
140
|
return files
|
|
142
|
-
|
|
141
|
+
|
|
143
142
|
# Initial scan
|
|
144
143
|
file_states = get_files()
|
|
145
144
|
initial_count = len(file_states)
|
|
146
|
-
|
|
145
|
+
|
|
147
146
|
output = [f"Watching {watch_path} (pattern: {pattern})"]
|
|
148
147
|
output.append(f"Found {initial_count} files to monitor")
|
|
149
148
|
if exclude_patterns:
|
|
150
149
|
output.append(f"Excluding: {', '.join(exclude_patterns)}")
|
|
151
150
|
output.append(f"Monitoring for {duration} seconds...\n")
|
|
152
|
-
|
|
151
|
+
|
|
153
152
|
# Monitor for changes
|
|
154
153
|
try:
|
|
155
154
|
while (time.time() - start_time) < duration:
|
|
156
155
|
await asyncio.sleep(interval)
|
|
157
|
-
|
|
156
|
+
|
|
158
157
|
current_files = get_files()
|
|
159
|
-
|
|
158
|
+
|
|
160
159
|
# Check for new files
|
|
161
160
|
for file_path, mtime in current_files.items():
|
|
162
161
|
if file_path not in file_states:
|
|
@@ -164,7 +163,7 @@ watch . --recursive --exclude "__pycache__"
|
|
|
164
163
|
change = f"[{timestamp}] CREATED: {file_path.relative_to(watch_path.parent)}"
|
|
165
164
|
changes.append(change)
|
|
166
165
|
output.append(change)
|
|
167
|
-
|
|
166
|
+
|
|
168
167
|
# Check for deleted files
|
|
169
168
|
for file_path in list(file_states.keys()):
|
|
170
169
|
if file_path not in current_files:
|
|
@@ -173,7 +172,7 @@ watch . --recursive --exclude "__pycache__"
|
|
|
173
172
|
changes.append(change)
|
|
174
173
|
output.append(change)
|
|
175
174
|
del file_states[file_path]
|
|
176
|
-
|
|
175
|
+
|
|
177
176
|
# Check for modified files
|
|
178
177
|
for file_path, mtime in current_files.items():
|
|
179
178
|
if file_path in file_states and mtime != file_states[file_path]:
|
|
@@ -182,21 +181,23 @@ watch . --recursive --exclude "__pycache__"
|
|
|
182
181
|
changes.append(change)
|
|
183
182
|
output.append(change)
|
|
184
183
|
file_states[file_path] = mtime
|
|
185
|
-
|
|
184
|
+
|
|
186
185
|
# Update file states for new files
|
|
187
186
|
for file_path, mtime in current_files.items():
|
|
188
187
|
if file_path not in file_states:
|
|
189
188
|
file_states[file_path] = mtime
|
|
190
|
-
|
|
189
|
+
|
|
191
190
|
except asyncio.CancelledError:
|
|
192
191
|
output.append("\nWatch cancelled")
|
|
193
|
-
|
|
192
|
+
|
|
194
193
|
# Summary
|
|
195
|
-
output.append(
|
|
194
|
+
output.append(
|
|
195
|
+
f"\nWatch completed after {int(time.time() - start_time)} seconds"
|
|
196
|
+
)
|
|
196
197
|
output.append(f"Total changes detected: {len(changes)}")
|
|
197
|
-
|
|
198
|
+
|
|
198
199
|
return "\n".join(output)
|
|
199
200
|
|
|
200
201
|
|
|
201
202
|
# Create tool instance
|
|
202
|
-
watch_tool = WatchTool()
|
|
203
|
+
watch_tool = WatchTool()
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
This module provides the Write tool for creating or overwriting files.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from typing import Unpack, Annotated, TypedDict, final, override
|
|
6
7
|
from pathlib import Path
|
|
7
|
-
from typing import Annotated, TypedDict, Unpack, final, override
|
|
8
8
|
|
|
9
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
10
|
-
from mcp.server import FastMCP
|
|
11
9
|
from pydantic import Field
|
|
10
|
+
from mcp.server import FastMCP
|
|
11
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
12
12
|
|
|
13
13
|
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
14
14
|
|
|
@@ -146,9 +146,5 @@ Usage:
|
|
|
146
146
|
tool_self = self # Create a reference to self for use in the closure
|
|
147
147
|
|
|
148
148
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
149
|
-
async def write(
|
|
150
|
-
file_path: FilePath,
|
|
151
|
-
content: Content,
|
|
152
|
-
ctx: MCPContext
|
|
153
|
-
) -> str:
|
|
149
|
+
async def write(file_path: FilePath, content: Content, ctx: MCPContext) -> str:
|
|
154
150
|
return await tool_self.call(ctx, file_path=file_path, content=content)
|
|
@@ -7,8 +7,8 @@ including reading and editing notebook cells.
|
|
|
7
7
|
from mcp.server import FastMCP
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
10
|
-
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
11
10
|
from hanzo_mcp.tools.jupyter.jupyter import JupyterTool
|
|
11
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
12
12
|
|
|
13
13
|
# Export all tool classes
|
|
14
14
|
__all__ = [
|
|
@@ -68,10 +68,10 @@ def register_jupyter_tools(
|
|
|
68
68
|
"notebook_read": JupyterTool,
|
|
69
69
|
"notebook_edit": JupyterTool,
|
|
70
70
|
}
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
tools = []
|
|
73
73
|
added_classes = set() # Track which tool classes have been added
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
if enabled_tools:
|
|
76
76
|
# Use individual tool configuration
|
|
77
77
|
for tool_name, enabled in enabled_tools.items():
|
|
@@ -84,6 +84,6 @@ def register_jupyter_tools(
|
|
|
84
84
|
else:
|
|
85
85
|
# Use all tools (backward compatibility)
|
|
86
86
|
tools = get_jupyter_tools(permission_manager)
|
|
87
|
-
|
|
87
|
+
|
|
88
88
|
ToolRegistry.register_tools(mcp_server, tools)
|
|
89
89
|
return tools
|
hanzo_mcp/tools/jupyter/base.py
CHANGED
|
@@ -4,17 +4,16 @@ This module provides common functionality for Jupyter notebook tools, including
|
|
|
4
4
|
cell processing, and output formatting.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from abc import ABC
|
|
8
|
-
import json
|
|
9
7
|
import re
|
|
10
|
-
|
|
8
|
+
import json
|
|
9
|
+
from abc import ABC
|
|
11
10
|
from typing import Any, final
|
|
11
|
+
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from mcp.server.fastmcp import Context as MCPContext
|
|
14
14
|
|
|
15
|
-
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
16
15
|
from hanzo_mcp.tools.common.context import ToolContext, create_tool_context
|
|
17
|
-
|
|
16
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
18
17
|
|
|
19
18
|
# Pattern to match ANSI escape sequences
|
|
20
19
|
ANSI_ESCAPE_PATTERN = re.compile(r"\x1B\[[0-9;]*[a-zA-Z]")
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"""Unified Jupyter notebook tool."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Dict,
|
|
6
|
+
Unpack,
|
|
7
|
+
Optional,
|
|
8
|
+
Annotated,
|
|
9
|
+
TypedDict,
|
|
10
|
+
final,
|
|
11
|
+
override,
|
|
12
|
+
)
|
|
6
13
|
from pathlib import Path
|
|
7
14
|
|
|
8
|
-
|
|
15
|
+
import nbformat
|
|
9
16
|
from pydantic import Field
|
|
17
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
18
|
|
|
11
19
|
from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
|
|
12
20
|
|
|
13
|
-
|
|
14
21
|
# Parameter types
|
|
15
22
|
Action = Annotated[
|
|
16
23
|
str,
|
|
@@ -70,6 +77,7 @@ EditMode = Annotated[
|
|
|
70
77
|
|
|
71
78
|
class NotebookParams(TypedDict, total=False):
|
|
72
79
|
"""Parameters for notebook tool."""
|
|
80
|
+
|
|
73
81
|
action: str
|
|
74
82
|
notebook_path: str
|
|
75
83
|
cell_id: Optional[str]
|
|
@@ -114,7 +122,7 @@ jupyter --action create "new.ipynb"
|
|
|
114
122
|
# Extract parameters
|
|
115
123
|
action = params.get("action", "read")
|
|
116
124
|
notebook_path = params.get("notebook_path")
|
|
117
|
-
|
|
125
|
+
|
|
118
126
|
if not notebook_path:
|
|
119
127
|
return "Error: notebook_path is required"
|
|
120
128
|
|
|
@@ -143,7 +151,9 @@ jupyter --action create "new.ipynb"
|
|
|
143
151
|
else:
|
|
144
152
|
return f"Error: Unknown action '{action}'. Valid actions: read, edit, create, delete, execute"
|
|
145
153
|
|
|
146
|
-
async def _handle_read(
|
|
154
|
+
async def _handle_read(
|
|
155
|
+
self, notebook_path: str, params: Dict[str, Any], tool_ctx
|
|
156
|
+
) -> str:
|
|
147
157
|
"""Read notebook or specific cell."""
|
|
148
158
|
exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
|
|
149
159
|
if not exists:
|
|
@@ -151,34 +161,36 @@ jupyter --action create "new.ipynb"
|
|
|
151
161
|
|
|
152
162
|
try:
|
|
153
163
|
nb = self.read_notebook(notebook_path)
|
|
154
|
-
|
|
164
|
+
|
|
155
165
|
# Check if specific cell requested
|
|
156
166
|
cell_id = params.get("cell_id")
|
|
157
167
|
cell_index = params.get("cell_index")
|
|
158
|
-
|
|
168
|
+
|
|
159
169
|
if cell_id:
|
|
160
170
|
# Find cell by ID
|
|
161
171
|
for i, cell in enumerate(nb.cells):
|
|
162
172
|
if cell.get("id") == cell_id:
|
|
163
173
|
return self._format_cell(cell, i)
|
|
164
174
|
return f"Error: Cell with ID '{cell_id}' not found"
|
|
165
|
-
|
|
175
|
+
|
|
166
176
|
elif cell_index is not None:
|
|
167
177
|
# Get cell by index
|
|
168
178
|
if 0 <= cell_index < len(nb.cells):
|
|
169
179
|
return self._format_cell(nb.cells[cell_index], cell_index)
|
|
170
180
|
else:
|
|
171
181
|
return f"Error: Cell index {cell_index} out of range (notebook has {len(nb.cells)} cells)"
|
|
172
|
-
|
|
182
|
+
|
|
173
183
|
else:
|
|
174
184
|
# Return all cells
|
|
175
185
|
return self.format_notebook(nb)
|
|
176
|
-
|
|
186
|
+
|
|
177
187
|
except Exception as e:
|
|
178
188
|
await tool_ctx.error(f"Failed to read notebook: {str(e)}")
|
|
179
189
|
return f"Error reading notebook: {str(e)}"
|
|
180
190
|
|
|
181
|
-
async def _handle_edit(
|
|
191
|
+
async def _handle_edit(
|
|
192
|
+
self, notebook_path: str, params: Dict[str, Any], tool_ctx
|
|
193
|
+
) -> str:
|
|
182
194
|
"""Edit notebook cell."""
|
|
183
195
|
exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
|
|
184
196
|
if not exists:
|
|
@@ -186,7 +198,7 @@ jupyter --action create "new.ipynb"
|
|
|
186
198
|
|
|
187
199
|
source = params.get("source")
|
|
188
200
|
edit_mode = params.get("edit_mode", "replace")
|
|
189
|
-
|
|
201
|
+
|
|
190
202
|
# Only require source for non-delete operations
|
|
191
203
|
if edit_mode != "delete" and not source:
|
|
192
204
|
return "Error: source is required for edit action"
|
|
@@ -196,19 +208,23 @@ jupyter --action create "new.ipynb"
|
|
|
196
208
|
|
|
197
209
|
try:
|
|
198
210
|
nb = self.read_notebook(notebook_path)
|
|
199
|
-
|
|
211
|
+
|
|
200
212
|
if edit_mode == "insert":
|
|
201
213
|
# Insert new cell
|
|
202
|
-
new_cell =
|
|
203
|
-
|
|
214
|
+
new_cell = (
|
|
215
|
+
nbformat.v4.new_code_cell(source)
|
|
216
|
+
if cell_type != "markdown"
|
|
217
|
+
else nbformat.v4.new_markdown_cell(source)
|
|
218
|
+
)
|
|
219
|
+
|
|
204
220
|
if cell_index is not None:
|
|
205
221
|
nb.cells.insert(cell_index, new_cell)
|
|
206
222
|
else:
|
|
207
223
|
nb.cells.append(new_cell)
|
|
208
|
-
|
|
224
|
+
|
|
209
225
|
self.write_notebook(nb, notebook_path)
|
|
210
|
-
return f"Successfully inserted new cell at index {cell_index if cell_index is not None else len(nb.cells)-1}"
|
|
211
|
-
|
|
226
|
+
return f"Successfully inserted new cell at index {cell_index if cell_index is not None else len(nb.cells) - 1}"
|
|
227
|
+
|
|
212
228
|
elif edit_mode == "delete":
|
|
213
229
|
# Delete cell
|
|
214
230
|
if cell_id:
|
|
@@ -218,7 +234,7 @@ jupyter --action create "new.ipynb"
|
|
|
218
234
|
self.write_notebook(nb, notebook_path)
|
|
219
235
|
return f"Successfully deleted cell with ID '{cell_id}'"
|
|
220
236
|
return f"Error: Cell with ID '{cell_id}' not found"
|
|
221
|
-
|
|
237
|
+
|
|
222
238
|
elif cell_index is not None:
|
|
223
239
|
if 0 <= cell_index < len(nb.cells):
|
|
224
240
|
nb.cells.pop(cell_index)
|
|
@@ -228,7 +244,7 @@ jupyter --action create "new.ipynb"
|
|
|
228
244
|
return f"Error: Cell index {cell_index} out of range"
|
|
229
245
|
else:
|
|
230
246
|
return "Error: cell_id or cell_index required for delete"
|
|
231
|
-
|
|
247
|
+
|
|
232
248
|
else: # replace
|
|
233
249
|
# Replace cell content
|
|
234
250
|
if cell_id:
|
|
@@ -240,7 +256,7 @@ jupyter --action create "new.ipynb"
|
|
|
240
256
|
self.write_notebook(nb, notebook_path)
|
|
241
257
|
return f"Successfully updated cell with ID '{cell_id}'"
|
|
242
258
|
return f"Error: Cell with ID '{cell_id}' not found"
|
|
243
|
-
|
|
259
|
+
|
|
244
260
|
elif cell_index is not None:
|
|
245
261
|
if 0 <= cell_index < len(nb.cells):
|
|
246
262
|
nb.cells[cell_index]["source"] = source
|
|
@@ -252,7 +268,7 @@ jupyter --action create "new.ipynb"
|
|
|
252
268
|
return f"Error: Cell index {cell_index} out of range"
|
|
253
269
|
else:
|
|
254
270
|
return "Error: cell_id or cell_index required for replace"
|
|
255
|
-
|
|
271
|
+
|
|
256
272
|
except Exception as e:
|
|
257
273
|
await tool_ctx.error(f"Failed to edit notebook: {str(e)}")
|
|
258
274
|
return f"Error editing notebook: {str(e)}"
|
|
@@ -267,25 +283,27 @@ jupyter --action create "new.ipynb"
|
|
|
267
283
|
try:
|
|
268
284
|
# Create new notebook
|
|
269
285
|
nb = nbformat.v4.new_notebook()
|
|
270
|
-
|
|
286
|
+
|
|
271
287
|
# Ensure parent directory exists
|
|
272
288
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
273
|
-
|
|
289
|
+
|
|
274
290
|
# Write notebook
|
|
275
291
|
self.write_notebook(nb, notebook_path)
|
|
276
292
|
return f"Successfully created notebook at {notebook_path}"
|
|
277
|
-
|
|
293
|
+
|
|
278
294
|
except Exception as e:
|
|
279
295
|
await tool_ctx.error(f"Failed to create notebook: {str(e)}")
|
|
280
296
|
return f"Error creating notebook: {str(e)}"
|
|
281
297
|
|
|
282
|
-
async def _handle_delete(
|
|
298
|
+
async def _handle_delete(
|
|
299
|
+
self, notebook_path: str, params: Dict[str, Any], tool_ctx
|
|
300
|
+
) -> str:
|
|
283
301
|
"""Delete notebook or cell."""
|
|
284
302
|
# If cell specified, delegate to edit with delete mode
|
|
285
303
|
if params.get("cell_id") or params.get("cell_index") is not None:
|
|
286
304
|
params["edit_mode"] = "delete"
|
|
287
305
|
return await self._handle_edit(notebook_path, params, tool_ctx)
|
|
288
|
-
|
|
306
|
+
|
|
289
307
|
# Otherwise, delete entire notebook
|
|
290
308
|
exists, error_msg = await self.check_path_exists(notebook_path, tool_ctx)
|
|
291
309
|
if not exists:
|
|
@@ -298,7 +316,9 @@ jupyter --action create "new.ipynb"
|
|
|
298
316
|
await tool_ctx.error(f"Failed to delete notebook: {str(e)}")
|
|
299
317
|
return f"Error deleting notebook: {str(e)}"
|
|
300
318
|
|
|
301
|
-
async def _handle_execute(
|
|
319
|
+
async def _handle_execute(
|
|
320
|
+
self, notebook_path: str, params: Dict[str, Any], tool_ctx
|
|
321
|
+
) -> str:
|
|
302
322
|
"""Execute notebook cells (placeholder for future implementation)."""
|
|
303
323
|
return "Error: Cell execution not yet implemented. Use a Jupyter kernel or server for execution."
|
|
304
324
|
|
|
@@ -313,19 +333,19 @@ jupyter --action create "new.ipynb"
|
|
|
313
333
|
if isinstance(source, list):
|
|
314
334
|
source = "".join(source)
|
|
315
335
|
output.append(source)
|
|
316
|
-
|
|
336
|
+
|
|
317
337
|
if cell.get("cell_type") == "code" and cell.get("outputs"):
|
|
318
338
|
output.append("\nOutputs:")
|
|
319
339
|
for out in cell.get("outputs", []):
|
|
320
340
|
out_type = out.get("output_type", "")
|
|
321
|
-
|
|
341
|
+
|
|
322
342
|
if out_type == "stream":
|
|
323
343
|
text = out.get("text", "")
|
|
324
344
|
if isinstance(text, list):
|
|
325
345
|
text = "".join(text)
|
|
326
346
|
name = out.get("name", "stdout")
|
|
327
347
|
output.append(f"[{name}]: {text}")
|
|
328
|
-
|
|
348
|
+
|
|
329
349
|
elif out_type == "execute_result":
|
|
330
350
|
exec_count = out.get("execution_count", "?")
|
|
331
351
|
data = out.get("data", {})
|
|
@@ -337,7 +357,7 @@ jupyter --action create "new.ipynb"
|
|
|
337
357
|
output.append(f"[Out {exec_count}]: {text_data}")
|
|
338
358
|
else:
|
|
339
359
|
output.append(f"[Out {exec_count}]: {data}")
|
|
340
|
-
|
|
360
|
+
|
|
341
361
|
elif out_type == "error":
|
|
342
362
|
ename = out.get("ename", "Error")
|
|
343
363
|
evalue = out.get("evalue", "")
|
|
@@ -348,50 +368,50 @@ jupyter --action create "new.ipynb"
|
|
|
348
368
|
output.append("Traceback:")
|
|
349
369
|
for line in traceback:
|
|
350
370
|
output.append(f" {line}")
|
|
351
|
-
|
|
371
|
+
|
|
352
372
|
return "\n".join(output)
|
|
353
373
|
|
|
354
374
|
def read_notebook(self, notebook_path: str) -> Any:
|
|
355
375
|
"""Read a notebook from disk using nbformat.
|
|
356
|
-
|
|
376
|
+
|
|
357
377
|
Args:
|
|
358
378
|
notebook_path: Path to the notebook file
|
|
359
|
-
|
|
379
|
+
|
|
360
380
|
Returns:
|
|
361
381
|
Notebook object
|
|
362
382
|
"""
|
|
363
|
-
with open(notebook_path,
|
|
383
|
+
with open(notebook_path, "r") as f:
|
|
364
384
|
return nbformat.read(f, as_version=4)
|
|
365
|
-
|
|
385
|
+
|
|
366
386
|
def write_notebook(self, nb: Any, notebook_path: str) -> None:
|
|
367
387
|
"""Write a notebook to disk using nbformat.
|
|
368
|
-
|
|
388
|
+
|
|
369
389
|
Args:
|
|
370
390
|
nb: Notebook object to write
|
|
371
391
|
notebook_path: Path to write the notebook to
|
|
372
392
|
"""
|
|
373
|
-
with open(notebook_path,
|
|
393
|
+
with open(notebook_path, "w") as f:
|
|
374
394
|
nbformat.write(nb, f)
|
|
375
|
-
|
|
395
|
+
|
|
376
396
|
def format_notebook(self, nb: Any) -> str:
|
|
377
397
|
"""Format an entire notebook for display.
|
|
378
|
-
|
|
398
|
+
|
|
379
399
|
Args:
|
|
380
400
|
nb: Notebook object
|
|
381
|
-
|
|
401
|
+
|
|
382
402
|
Returns:
|
|
383
403
|
Formatted string representation of the notebook
|
|
384
404
|
"""
|
|
385
405
|
output = []
|
|
386
406
|
output.append(f"Notebook with {len(nb.cells)} cells")
|
|
387
407
|
output.append("=" * 50)
|
|
388
|
-
|
|
408
|
+
|
|
389
409
|
for i, cell in enumerate(nb.cells):
|
|
390
410
|
output.append("")
|
|
391
411
|
output.append(self._format_cell(cell, i))
|
|
392
|
-
|
|
412
|
+
|
|
393
413
|
return "\n".join(output)
|
|
394
414
|
|
|
395
415
|
def register(self, mcp_server) -> None:
|
|
396
416
|
"""Register this tool with the MCP server."""
|
|
397
|
-
pass
|
|
417
|
+
pass
|
|
@@ -4,12 +4,12 @@ This module provides the NoteBookEditTool for editing Jupyter notebook files.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
from typing import Any, Unpack, Literal, Annotated, TypedDict, final, override
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Annotated, Any, Literal, TypedDict, Unpack, final, override
|
|
9
9
|
|
|
10
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
11
|
-
from mcp.server import FastMCP
|
|
12
10
|
from pydantic import Field
|
|
11
|
+
from mcp.server import FastMCP
|
|
12
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
13
13
|
|
|
14
14
|
from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
|
|
15
15
|
|
|
@@ -305,7 +305,7 @@ class NoteBookEditTool(JupyterBaseTool):
|
|
|
305
305
|
new_source: NewSource,
|
|
306
306
|
cell_type: CellType,
|
|
307
307
|
edit_mode: EditMode,
|
|
308
|
-
ctx: MCPContext
|
|
308
|
+
ctx: MCPContext,
|
|
309
309
|
) -> str:
|
|
310
310
|
return await tool_self.call(
|
|
311
311
|
ctx,
|
|
@@ -4,12 +4,12 @@ This module provides the NotebookReadTool for reading Jupyter notebook files.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
+
from typing import Unpack, Annotated, TypedDict, final, override
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import Annotated, TypedDict, Unpack, final, override
|
|
9
9
|
|
|
10
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
11
|
-
from mcp.server import FastMCP
|
|
12
10
|
from pydantic import Field
|
|
11
|
+
from mcp.server import FastMCP
|
|
12
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
13
13
|
|
|
14
14
|
from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
|
|
15
15
|
|
|
@@ -143,8 +143,5 @@ class NotebookReadTool(JupyterBaseTool):
|
|
|
143
143
|
tool_self = self # Create a reference to self for use in the closure
|
|
144
144
|
|
|
145
145
|
@mcp_server.tool(name=self.name, description=self.description)
|
|
146
|
-
async def notebook_read(
|
|
147
|
-
notebook_path: NotebookPath,
|
|
148
|
-
ctx: MCPContext
|
|
149
|
-
) -> str:
|
|
146
|
+
async def notebook_read(notebook_path: NotebookPath, ctx: MCPContext) -> str:
|
|
150
147
|
return await tool_self.call(ctx, notebook_path=notebook_path)
|
hanzo_mcp/tools/llm/__init__.py
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
"""LLM tools for Hanzo AI."""
|
|
2
2
|
|
|
3
|
-
from hanzo_mcp.tools.llm.llm_tool import LLMTool
|
|
4
|
-
|
|
5
3
|
# Legacy imports for backwards compatibility
|
|
6
4
|
from hanzo_mcp.tools.llm.llm_tool import LLMTool
|
|
7
|
-
from hanzo_mcp.tools.llm.consensus_tool import ConsensusTool
|
|
8
5
|
from hanzo_mcp.tools.llm.llm_manage import LLMManageTool
|
|
6
|
+
from hanzo_mcp.tools.llm.consensus_tool import ConsensusTool
|
|
9
7
|
from hanzo_mcp.tools.llm.provider_tools import (
|
|
10
|
-
create_provider_tools,
|
|
11
|
-
OpenAITool,
|
|
12
|
-
AnthropicTool,
|
|
13
|
-
GeminiTool,
|
|
14
8
|
GroqTool,
|
|
9
|
+
GeminiTool,
|
|
10
|
+
OpenAITool,
|
|
15
11
|
MistralTool,
|
|
12
|
+
AnthropicTool,
|
|
16
13
|
PerplexityTool,
|
|
14
|
+
create_provider_tools,
|
|
17
15
|
)
|
|
18
16
|
|
|
19
17
|
__all__ = [
|