hanzo-mcp 0.1.25__py3-none-any.whl → 0.1.32__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 +2 -2
- hanzo_mcp/cli.py +80 -9
- hanzo_mcp/server.py +41 -10
- hanzo_mcp/tools/__init__.py +54 -32
- hanzo_mcp/tools/agent/__init__.py +59 -0
- hanzo_mcp/tools/agent/agent_tool.py +474 -0
- hanzo_mcp/tools/agent/prompt.py +137 -0
- hanzo_mcp/tools/agent/tool_adapter.py +75 -0
- hanzo_mcp/tools/common/__init__.py +29 -0
- hanzo_mcp/tools/common/base.py +216 -0
- hanzo_mcp/tools/common/context.py +7 -3
- hanzo_mcp/tools/common/permissions.py +63 -119
- hanzo_mcp/tools/common/session.py +91 -0
- hanzo_mcp/tools/common/thinking_tool.py +123 -0
- hanzo_mcp/tools/common/version_tool.py +62 -0
- hanzo_mcp/tools/filesystem/__init__.py +85 -5
- hanzo_mcp/tools/filesystem/base.py +113 -0
- hanzo_mcp/tools/filesystem/content_replace.py +287 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +286 -0
- hanzo_mcp/tools/filesystem/edit_file.py +287 -0
- hanzo_mcp/tools/filesystem/get_file_info.py +170 -0
- hanzo_mcp/tools/filesystem/read_files.py +198 -0
- hanzo_mcp/tools/filesystem/search_content.py +275 -0
- hanzo_mcp/tools/filesystem/write_file.py +162 -0
- hanzo_mcp/tools/jupyter/__init__.py +67 -4
- hanzo_mcp/tools/jupyter/base.py +284 -0
- hanzo_mcp/tools/jupyter/edit_notebook.py +295 -0
- hanzo_mcp/tools/jupyter/notebook_operations.py +72 -112
- hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
- hanzo_mcp/tools/project/__init__.py +64 -1
- hanzo_mcp/tools/project/analysis.py +9 -6
- hanzo_mcp/tools/project/base.py +66 -0
- hanzo_mcp/tools/project/project_analyze.py +173 -0
- hanzo_mcp/tools/shell/__init__.py +58 -1
- hanzo_mcp/tools/shell/base.py +148 -0
- hanzo_mcp/tools/shell/command_executor.py +203 -322
- hanzo_mcp/tools/shell/run_command.py +204 -0
- hanzo_mcp/tools/shell/run_script.py +215 -0
- hanzo_mcp/tools/shell/script_tool.py +244 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.32.dist-info}/METADATA +83 -77
- hanzo_mcp-0.1.32.dist-info/RECORD +46 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.32.dist-info}/licenses/LICENSE +2 -2
- hanzo_mcp/tools/common/thinking.py +0 -65
- hanzo_mcp/tools/filesystem/file_operations.py +0 -1050
- hanzo_mcp-0.1.25.dist-info/RECORD +0 -24
- hanzo_mcp-0.1.25.dist-info/zip-safe +0 -1
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.32.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.32.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.1.25.dist-info → hanzo_mcp-0.1.32.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Search content tool implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the SearchContentTool for finding text patterns in files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import fnmatch
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, final, override
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
13
|
+
from mcp.server.fastmcp import FastMCP
|
|
14
|
+
|
|
15
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class SearchContentTool(FilesystemBaseTool):
|
|
20
|
+
"""Tool for searching for text patterns in files."""
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
@override
|
|
24
|
+
def name(self) -> str:
|
|
25
|
+
"""Get the tool name.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Tool name
|
|
29
|
+
"""
|
|
30
|
+
return "search_content"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
@override
|
|
34
|
+
def description(self) -> str:
|
|
35
|
+
"""Get the tool description.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Tool description
|
|
39
|
+
"""
|
|
40
|
+
return """Search for a pattern in file contents.
|
|
41
|
+
|
|
42
|
+
Similar to grep, this tool searches for text patterns within files.
|
|
43
|
+
Searches recursively through all files in the specified directory
|
|
44
|
+
that match the file pattern. Returns matching lines with file and
|
|
45
|
+
line number references. Only searches within allowed directories."""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
@override
|
|
49
|
+
def parameters(self) -> dict[str, Any]:
|
|
50
|
+
"""Get the parameter specifications for the tool.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Parameter specifications
|
|
54
|
+
"""
|
|
55
|
+
return {
|
|
56
|
+
"properties": {
|
|
57
|
+
"pattern": {
|
|
58
|
+
"type": "string",
|
|
59
|
+
"description": "text pattern to search for"
|
|
60
|
+
},
|
|
61
|
+
"path": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"description": "path to the directory or file to search"
|
|
64
|
+
},
|
|
65
|
+
"file_pattern": {
|
|
66
|
+
"default": "*",
|
|
67
|
+
"type": "string"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"required": ["pattern", "path"],
|
|
71
|
+
"title": "search_contentArguments",
|
|
72
|
+
"type": "object"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
@override
|
|
77
|
+
def required(self) -> list[str]:
|
|
78
|
+
"""Get the list of required parameter names.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of required parameter names
|
|
82
|
+
"""
|
|
83
|
+
return ["pattern", "path"]
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
async def call(self, ctx: MCPContext, **params: Any) -> str:
|
|
87
|
+
"""Execute the tool with the given parameters.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
ctx: MCP context
|
|
91
|
+
**params: Tool parameters
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Tool result
|
|
95
|
+
"""
|
|
96
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
97
|
+
|
|
98
|
+
# Extract parameters
|
|
99
|
+
pattern = params.get("pattern")
|
|
100
|
+
path = params.get("path")
|
|
101
|
+
file_pattern = params.get("file_pattern", "*") # Default to all files
|
|
102
|
+
|
|
103
|
+
# Validate required parameters
|
|
104
|
+
if not pattern:
|
|
105
|
+
await tool_ctx.error("Parameter 'pattern' is required but was None")
|
|
106
|
+
return "Error: Parameter 'pattern' is required but was None"
|
|
107
|
+
|
|
108
|
+
if pattern.strip() == "":
|
|
109
|
+
await tool_ctx.error("Parameter 'pattern' cannot be empty")
|
|
110
|
+
return "Error: Parameter 'pattern' cannot be empty"
|
|
111
|
+
|
|
112
|
+
if not path:
|
|
113
|
+
await tool_ctx.error("Parameter 'path' is required but was None")
|
|
114
|
+
return "Error: Parameter 'path' is required but was None"
|
|
115
|
+
|
|
116
|
+
if path.strip() == "":
|
|
117
|
+
await tool_ctx.error("Parameter 'path' cannot be empty")
|
|
118
|
+
return "Error: Parameter 'path' cannot be empty"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
path_validation = self.validate_path(path)
|
|
122
|
+
if path_validation.is_error:
|
|
123
|
+
await tool_ctx.error(path_validation.error_message)
|
|
124
|
+
return f"Error: {path_validation.error_message}"
|
|
125
|
+
|
|
126
|
+
# file_pattern can be None safely as it has a default value
|
|
127
|
+
|
|
128
|
+
await tool_ctx.info(
|
|
129
|
+
f"Searching for pattern '{pattern}' in files matching '{file_pattern}' in {path}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Check if path is allowed
|
|
133
|
+
allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
|
|
134
|
+
if not allowed:
|
|
135
|
+
return error_msg
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
input_path = Path(path)
|
|
139
|
+
|
|
140
|
+
# Check if path exists
|
|
141
|
+
exists, error_msg = await self.check_path_exists(path, tool_ctx)
|
|
142
|
+
if not exists:
|
|
143
|
+
return error_msg
|
|
144
|
+
|
|
145
|
+
# Find matching files using optimized file finding
|
|
146
|
+
matching_files: list[Path] = []
|
|
147
|
+
|
|
148
|
+
# Process based on whether path is a file or directory
|
|
149
|
+
if input_path.is_file():
|
|
150
|
+
# Single file search
|
|
151
|
+
if file_pattern == "*" or fnmatch.fnmatch(input_path.name, file_pattern):
|
|
152
|
+
matching_files.append(input_path)
|
|
153
|
+
await tool_ctx.info(f"Searching single file: {path}")
|
|
154
|
+
else:
|
|
155
|
+
await tool_ctx.info(f"File does not match pattern '{file_pattern}': {path}")
|
|
156
|
+
return f"File does not match pattern '{file_pattern}': {path}"
|
|
157
|
+
elif input_path.is_dir():
|
|
158
|
+
# Directory search - optimized file finding
|
|
159
|
+
await tool_ctx.info(f"Finding files in directory: {path}")
|
|
160
|
+
|
|
161
|
+
# Keep track of allowed paths for filtering
|
|
162
|
+
allowed_paths: set[str] = set()
|
|
163
|
+
|
|
164
|
+
# Collect all allowed paths first for faster filtering
|
|
165
|
+
for entry in input_path.rglob("*"):
|
|
166
|
+
entry_path = str(entry)
|
|
167
|
+
if self.is_path_allowed(entry_path):
|
|
168
|
+
allowed_paths.add(entry_path)
|
|
169
|
+
|
|
170
|
+
# Find matching files efficiently
|
|
171
|
+
for entry in input_path.rglob("*"):
|
|
172
|
+
entry_path = str(entry)
|
|
173
|
+
if entry_path in allowed_paths and entry.is_file():
|
|
174
|
+
if file_pattern == "*" or fnmatch.fnmatch(entry.name, file_pattern):
|
|
175
|
+
matching_files.append(entry)
|
|
176
|
+
|
|
177
|
+
await tool_ctx.info(f"Found {len(matching_files)} matching files")
|
|
178
|
+
else:
|
|
179
|
+
# This shouldn't happen since we already checked for existence
|
|
180
|
+
await tool_ctx.error(f"Path is neither a file nor a directory: {path}")
|
|
181
|
+
return f"Error: Path is neither a file nor a directory: {path}"
|
|
182
|
+
|
|
183
|
+
# Report progress
|
|
184
|
+
total_files = len(matching_files)
|
|
185
|
+
if input_path.is_file():
|
|
186
|
+
await tool_ctx.info(f"Searching file: {path}")
|
|
187
|
+
else:
|
|
188
|
+
await tool_ctx.info(f"Searching through {total_files} files in directory")
|
|
189
|
+
|
|
190
|
+
# set up for parallel processing
|
|
191
|
+
results: list[str] = []
|
|
192
|
+
files_processed = 0
|
|
193
|
+
matches_found = 0
|
|
194
|
+
batch_size = 20 # Process files in batches to avoid overwhelming the system
|
|
195
|
+
|
|
196
|
+
# Use a semaphore to limit concurrent file operations
|
|
197
|
+
# Adjust the value based on system resources
|
|
198
|
+
semaphore = asyncio.Semaphore(10)
|
|
199
|
+
|
|
200
|
+
# Create an async function to search a single file
|
|
201
|
+
async def search_file(file_path: Path) -> list[str]:
|
|
202
|
+
nonlocal files_processed, matches_found
|
|
203
|
+
file_results: list[str] = []
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
async with semaphore: # Limit concurrent operations
|
|
207
|
+
try:
|
|
208
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
209
|
+
for line_num, line in enumerate(f, 1):
|
|
210
|
+
if re.search(pattern, line):
|
|
211
|
+
file_results.append(
|
|
212
|
+
f"{file_path}:{line_num}: {line.rstrip()}"
|
|
213
|
+
)
|
|
214
|
+
matches_found += 1
|
|
215
|
+
files_processed += 1
|
|
216
|
+
except UnicodeDecodeError:
|
|
217
|
+
# Skip binary files
|
|
218
|
+
files_processed += 1
|
|
219
|
+
except Exception as e:
|
|
220
|
+
await tool_ctx.warning(f"Error reading {file_path}: {str(e)}")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
await tool_ctx.warning(f"Error processing {file_path}: {str(e)}")
|
|
223
|
+
|
|
224
|
+
return file_results
|
|
225
|
+
|
|
226
|
+
# Process files in parallel batches
|
|
227
|
+
for i in range(0, len(matching_files), batch_size):
|
|
228
|
+
batch = matching_files[i:i+batch_size]
|
|
229
|
+
batch_tasks = [search_file(file_path) for file_path in batch]
|
|
230
|
+
|
|
231
|
+
# Report progress
|
|
232
|
+
await tool_ctx.report_progress(i, total_files)
|
|
233
|
+
|
|
234
|
+
# Wait for the batch to complete
|
|
235
|
+
batch_results = await asyncio.gather(*batch_tasks)
|
|
236
|
+
|
|
237
|
+
# Flatten and collect results
|
|
238
|
+
for file_result in batch_results:
|
|
239
|
+
results.extend(file_result)
|
|
240
|
+
|
|
241
|
+
# Final progress report
|
|
242
|
+
await tool_ctx.report_progress(total_files, total_files)
|
|
243
|
+
|
|
244
|
+
if not results:
|
|
245
|
+
if input_path.is_file():
|
|
246
|
+
return f"No matches found for pattern '{pattern}' in file: {path}"
|
|
247
|
+
else:
|
|
248
|
+
return f"No matches found for pattern '{pattern}' in files matching '{file_pattern}' in directory: {path}"
|
|
249
|
+
|
|
250
|
+
await tool_ctx.info(
|
|
251
|
+
f"Found {matches_found} matches in {files_processed} file{'s' if files_processed > 1 else ''}"
|
|
252
|
+
)
|
|
253
|
+
return (
|
|
254
|
+
f"Found {matches_found} matches in {files_processed} file{'s' if files_processed > 1 else ''}:\n\n"
|
|
255
|
+
+ "\n".join(results)
|
|
256
|
+
)
|
|
257
|
+
except Exception as e:
|
|
258
|
+
await tool_ctx.error(f"Error searching file contents: {str(e)}")
|
|
259
|
+
return f"Error searching file contents: {str(e)}"
|
|
260
|
+
|
|
261
|
+
@override
|
|
262
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
263
|
+
"""Register this search content tool with the MCP server.
|
|
264
|
+
|
|
265
|
+
Creates a wrapper function with explicitly defined parameters that match
|
|
266
|
+
the tool's parameter schema and registers it with the MCP server.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
mcp_server: The FastMCP server instance
|
|
270
|
+
"""
|
|
271
|
+
tool_self = self # Create a reference to self for use in the closure
|
|
272
|
+
|
|
273
|
+
@mcp_server.tool(name=self.name, description=self.mcp_description)
|
|
274
|
+
async def search_content(ctx: MCPContext, pattern: str, path: str, file_pattern: str = "*") -> str:
|
|
275
|
+
return await tool_self.call(ctx, pattern=pattern, path=path, file_pattern=file_pattern)
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Write file tool implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the WriteFileTool for creating or overwriting files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, final, override
|
|
8
|
+
|
|
9
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
|
+
from mcp.server.fastmcp import FastMCP
|
|
11
|
+
|
|
12
|
+
from hanzo_mcp.tools.filesystem.base import FilesystemBaseTool
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@final
|
|
16
|
+
class WriteFileTool(FilesystemBaseTool):
|
|
17
|
+
"""Tool for writing file contents."""
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
@override
|
|
21
|
+
def name(self) -> str:
|
|
22
|
+
"""Get the tool name.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Tool name
|
|
26
|
+
"""
|
|
27
|
+
return "write_file"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
@override
|
|
31
|
+
def description(self) -> str:
|
|
32
|
+
"""Get the tool description.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tool description
|
|
36
|
+
"""
|
|
37
|
+
return """Create a new file or completely overwrite an existing file with new content.
|
|
38
|
+
|
|
39
|
+
Use with caution as it will overwrite existing files without warning.
|
|
40
|
+
Handles text content with proper encoding. Only works within allowed directories."""
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
@override
|
|
44
|
+
def parameters(self) -> dict[str, Any]:
|
|
45
|
+
"""Get the parameter specifications for the tool.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Parameter specifications
|
|
49
|
+
"""
|
|
50
|
+
return {
|
|
51
|
+
"properties": {
|
|
52
|
+
"path": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "path to the file to write"
|
|
55
|
+
},
|
|
56
|
+
"content": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "content to write to the file"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"required": ["path", "content"],
|
|
62
|
+
"type": "object"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
@override
|
|
67
|
+
def required(self) -> list[str]:
|
|
68
|
+
"""Get the list of required parameter names.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of required parameter names
|
|
72
|
+
"""
|
|
73
|
+
return ["path", "content"]
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
async def call(self, ctx: MCPContext, **params: Any) -> str:
|
|
77
|
+
"""Execute the tool with the given parameters.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
ctx: MCP context
|
|
81
|
+
**params: Tool parameters
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Tool result
|
|
85
|
+
"""
|
|
86
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
87
|
+
self.set_tool_context_info(tool_ctx)
|
|
88
|
+
|
|
89
|
+
# Extract parameters
|
|
90
|
+
path = params.get("path")
|
|
91
|
+
content = params.get("content")
|
|
92
|
+
|
|
93
|
+
if not path:
|
|
94
|
+
await tool_ctx.error("Parameter 'path' is required but was None")
|
|
95
|
+
return "Error: Parameter 'path' is required but was None"
|
|
96
|
+
|
|
97
|
+
if path.strip() == "":
|
|
98
|
+
await tool_ctx.error("Parameter 'path' cannot be empty")
|
|
99
|
+
return "Error: Parameter 'path' cannot be empty"
|
|
100
|
+
|
|
101
|
+
# Validate parameters
|
|
102
|
+
path_validation = self.validate_path(path)
|
|
103
|
+
if path_validation.is_error:
|
|
104
|
+
await tool_ctx.error(path_validation.error_message)
|
|
105
|
+
return f"Error: {path_validation.error_message}"
|
|
106
|
+
|
|
107
|
+
if not content:
|
|
108
|
+
await tool_ctx.error("Parameter 'content' is required but was None")
|
|
109
|
+
return "Error: Parameter 'content' is required but was None"
|
|
110
|
+
|
|
111
|
+
await tool_ctx.info(f"Writing file: {path}")
|
|
112
|
+
|
|
113
|
+
# Check if file is allowed to be written
|
|
114
|
+
allowed, error_msg = await self.check_path_allowed(path, tool_ctx)
|
|
115
|
+
if not allowed:
|
|
116
|
+
return error_msg
|
|
117
|
+
|
|
118
|
+
# Additional check already verified by is_path_allowed above
|
|
119
|
+
await tool_ctx.info(f"Writing file: {path}")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
file_path = Path(path)
|
|
123
|
+
|
|
124
|
+
# Check if parent directory is allowed
|
|
125
|
+
parent_dir = str(file_path.parent)
|
|
126
|
+
if not self.is_path_allowed(parent_dir):
|
|
127
|
+
await tool_ctx.error(f"Parent directory not allowed: {parent_dir}")
|
|
128
|
+
return f"Error: Parent directory not allowed: {parent_dir}"
|
|
129
|
+
|
|
130
|
+
# Create parent directories if they don't exist
|
|
131
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
# Write the file
|
|
134
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
135
|
+
f.write(content)
|
|
136
|
+
|
|
137
|
+
# Add to document context
|
|
138
|
+
self.document_context.add_document(path, content)
|
|
139
|
+
|
|
140
|
+
await tool_ctx.info(
|
|
141
|
+
f"Successfully wrote file: {path} ({len(content)} bytes)"
|
|
142
|
+
)
|
|
143
|
+
return f"Successfully wrote file: {path} ({len(content)} bytes)"
|
|
144
|
+
except Exception as e:
|
|
145
|
+
await tool_ctx.error(f"Error writing file: {str(e)}")
|
|
146
|
+
return f"Error writing file: {str(e)}"
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
150
|
+
"""Register this tool with the MCP server.
|
|
151
|
+
|
|
152
|
+
Creates a wrapper function with explicitly defined parameters that match
|
|
153
|
+
the tool's parameter schema and registers it with the MCP server.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
mcp_server: The FastMCP server instance
|
|
157
|
+
"""
|
|
158
|
+
tool_self = self # Create a reference to self for use in the closure
|
|
159
|
+
|
|
160
|
+
@mcp_server.tool(name=self.name, description=self.mcp_description)
|
|
161
|
+
async def write_file(path: str, content: str, ctx: MCPContext) -> str:
|
|
162
|
+
return await tool_self.call(ctx, path=path, content=content)
|
|
@@ -1,8 +1,71 @@
|
|
|
1
|
-
"""Jupyter notebook tools for Hanzo MCP.
|
|
1
|
+
"""Jupyter notebook tools package for Hanzo MCP.
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This package provides tools for working with Jupyter notebooks (.ipynb files),
|
|
4
|
+
including reading and editing notebook cells.
|
|
4
5
|
"""
|
|
5
6
|
|
|
6
|
-
from
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
10
|
+
from hanzo_mcp.tools.common.context import DocumentContext
|
|
11
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
12
|
+
from hanzo_mcp.tools.jupyter.edit_notebook import EditNotebookTool
|
|
13
|
+
from hanzo_mcp.tools.jupyter.read_notebook import ReadNotebookTool
|
|
14
|
+
|
|
15
|
+
# Export all tool classes
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ReadNotebookTool",
|
|
18
|
+
"EditNotebookTool",
|
|
19
|
+
"get_jupyter_tools",
|
|
20
|
+
"register_jupyter_tools",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
def get_read_only_jupyter_tools(
|
|
24
|
+
document_context: DocumentContext, permission_manager: PermissionManager
|
|
25
|
+
) -> list[BaseTool]:
|
|
26
|
+
"""Create instances of read only Jupyter notebook tools.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
document_context: Document context for tracking file contents
|
|
30
|
+
permission_manager: Permission manager for access control
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of Jupyter notebook tool instances
|
|
34
|
+
"""
|
|
35
|
+
return [
|
|
36
|
+
ReadNotebookTool(document_context, permission_manager),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_jupyter_tools(
|
|
41
|
+
document_context: DocumentContext, permission_manager: PermissionManager
|
|
42
|
+
) -> list[BaseTool]:
|
|
43
|
+
"""Create instances of all Jupyter notebook tools.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
document_context: Document context for tracking file contents
|
|
47
|
+
permission_manager: Permission manager for access control
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
List of Jupyter notebook tool instances
|
|
51
|
+
"""
|
|
52
|
+
return [
|
|
53
|
+
ReadNotebookTool(document_context, permission_manager),
|
|
54
|
+
EditNotebookTool(document_context, permission_manager),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def register_jupyter_tools(
|
|
59
|
+
mcp_server: FastMCP,
|
|
60
|
+
document_context: DocumentContext,
|
|
61
|
+
permission_manager: PermissionManager,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Register all Jupyter notebook tools with the MCP server.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
mcp_server: The FastMCP server instance
|
|
67
|
+
document_context: Document context for tracking file contents
|
|
68
|
+
permission_manager: Permission manager for access control
|
|
69
|
+
"""
|
|
70
|
+
tools = get_jupyter_tools(document_context, permission_manager)
|
|
71
|
+
ToolRegistry.register_tools(mcp_server, tools)
|