hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.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 -1
- hanzo_mcp/cli.py +118 -170
- hanzo_mcp/cli_enhanced.py +438 -0
- hanzo_mcp/config/__init__.py +19 -0
- hanzo_mcp/config/settings.py +388 -0
- hanzo_mcp/config/tool_config.py +197 -0
- hanzo_mcp/prompts/__init__.py +117 -0
- hanzo_mcp/prompts/compact_conversation.py +77 -0
- hanzo_mcp/prompts/create_release.py +38 -0
- hanzo_mcp/prompts/project_system.py +120 -0
- hanzo_mcp/prompts/project_todo_reminder.py +111 -0
- hanzo_mcp/prompts/utils.py +286 -0
- hanzo_mcp/server.py +117 -99
- hanzo_mcp/tools/__init__.py +105 -32
- hanzo_mcp/tools/agent/__init__.py +8 -11
- hanzo_mcp/tools/agent/agent_tool.py +290 -224
- hanzo_mcp/tools/agent/prompt.py +16 -13
- hanzo_mcp/tools/agent/tool_adapter.py +9 -9
- hanzo_mcp/tools/common/__init__.py +17 -16
- hanzo_mcp/tools/common/base.py +79 -110
- hanzo_mcp/tools/common/batch_tool.py +330 -0
- hanzo_mcp/tools/common/context.py +26 -292
- hanzo_mcp/tools/common/permissions.py +12 -12
- hanzo_mcp/tools/common/thinking_tool.py +153 -0
- hanzo_mcp/tools/common/validation.py +1 -63
- hanzo_mcp/tools/filesystem/__init__.py +88 -57
- hanzo_mcp/tools/filesystem/base.py +32 -24
- hanzo_mcp/tools/filesystem/content_replace.py +114 -107
- hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
- hanzo_mcp/tools/filesystem/edit.py +279 -0
- hanzo_mcp/tools/filesystem/grep.py +458 -0
- hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
- hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
- hanzo_mcp/tools/filesystem/read.py +255 -0
- hanzo_mcp/tools/filesystem/write.py +156 -0
- hanzo_mcp/tools/jupyter/__init__.py +41 -29
- hanzo_mcp/tools/jupyter/base.py +66 -57
- hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
- hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
- hanzo_mcp/tools/shell/__init__.py +29 -20
- hanzo_mcp/tools/shell/base.py +87 -45
- hanzo_mcp/tools/shell/bash_session.py +731 -0
- hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
- hanzo_mcp/tools/shell/command_executor.py +435 -384
- hanzo_mcp/tools/shell/run_command.py +284 -131
- hanzo_mcp/tools/shell/run_command_windows.py +328 -0
- hanzo_mcp/tools/shell/session_manager.py +196 -0
- hanzo_mcp/tools/shell/session_storage.py +325 -0
- hanzo_mcp/tools/todo/__init__.py +66 -0
- hanzo_mcp/tools/todo/base.py +319 -0
- hanzo_mcp/tools/todo/todo_read.py +148 -0
- hanzo_mcp/tools/todo/todo_write.py +378 -0
- hanzo_mcp/tools/vector/__init__.py +95 -0
- hanzo_mcp/tools/vector/infinity_store.py +365 -0
- hanzo_mcp/tools/vector/project_manager.py +361 -0
- hanzo_mcp/tools/vector/vector_index.py +115 -0
- hanzo_mcp/tools/vector/vector_search.py +215 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
- hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
- hanzo_mcp/tools/agent/base_provider.py +0 -73
- hanzo_mcp/tools/agent/litellm_provider.py +0 -45
- hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
- hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
- hanzo_mcp/tools/agent/provider_registry.py +0 -120
- hanzo_mcp/tools/common/error_handling.py +0 -86
- hanzo_mcp/tools/common/logging_config.py +0 -115
- hanzo_mcp/tools/common/session.py +0 -91
- hanzo_mcp/tools/common/think_tool.py +0 -123
- hanzo_mcp/tools/common/version_tool.py +0 -120
- hanzo_mcp/tools/filesystem/edit_file.py +0 -287
- hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
- hanzo_mcp/tools/filesystem/read_files.py +0 -199
- hanzo_mcp/tools/filesystem/search_content.py +0 -275
- hanzo_mcp/tools/filesystem/write_file.py +0 -162
- hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
- hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
- hanzo_mcp/tools/project/__init__.py +0 -64
- hanzo_mcp/tools/project/analysis.py +0 -886
- hanzo_mcp/tools/project/base.py +0 -66
- hanzo_mcp/tools/project/project_analyze.py +0 -173
- hanzo_mcp/tools/shell/run_script.py +0 -215
- hanzo_mcp/tools/shell/script_tool.py +0 -244
- hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
|
@@ -1,514 +0,0 @@
|
|
|
1
|
-
"""Jupyter notebook operations tools for Hanzo MCP.
|
|
2
|
-
|
|
3
|
-
This module provides tools for reading and editing Jupyter notebook (.ipynb) files.
|
|
4
|
-
It supports reading notebook cells with their outputs and modifying notebook contents.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import json
|
|
8
|
-
import re
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Any, final, Literal
|
|
11
|
-
|
|
12
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
13
|
-
from mcp.server.fastmcp import FastMCP
|
|
14
|
-
|
|
15
|
-
from hanzo_mcp.tools.common.context import DocumentContext, create_tool_context
|
|
16
|
-
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
17
|
-
from hanzo_mcp.tools.common.validation import validate_path_parameter
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# Pattern to match ANSI escape sequences
|
|
21
|
-
ANSI_ESCAPE_PATTERN = re.compile(r'\x1B\[[0-9;]*[a-zA-Z]')
|
|
22
|
-
|
|
23
|
-
# Function to clean ANSI escape codes from text
|
|
24
|
-
def clean_ansi_escapes(text: str) -> str:
|
|
25
|
-
"""Remove ANSI escape sequences from text.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
text: Text containing ANSI escape sequences
|
|
29
|
-
|
|
30
|
-
Returns:
|
|
31
|
-
Text with ANSI escape sequences removed
|
|
32
|
-
"""
|
|
33
|
-
return ANSI_ESCAPE_PATTERN.sub('', text)
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# Type definitions for Jupyter notebooks based on the nbformat spec
|
|
37
|
-
CellType = Literal["code", "markdown"]
|
|
38
|
-
OutputType = Literal["stream", "display_data", "execute_result", "error"]
|
|
39
|
-
EditMode = Literal["replace", "insert", "delete"]
|
|
40
|
-
|
|
41
|
-
# Define a structure for notebook cell outputs
|
|
42
|
-
@final
|
|
43
|
-
class NotebookOutputImage:
|
|
44
|
-
"""Representation of an image output in a notebook cell."""
|
|
45
|
-
|
|
46
|
-
def __init__(self, image_data: str, media_type: str):
|
|
47
|
-
"""Initialize a notebook output image.
|
|
48
|
-
|
|
49
|
-
Args:
|
|
50
|
-
image_data: Base64-encoded image data
|
|
51
|
-
media_type: Media type of the image (e.g., "image/png")
|
|
52
|
-
"""
|
|
53
|
-
self.image_data = image_data
|
|
54
|
-
self.media_type = media_type
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
@final
|
|
58
|
-
class NotebookCellOutput:
|
|
59
|
-
"""Representation of an output from a notebook cell."""
|
|
60
|
-
|
|
61
|
-
def __init__(
|
|
62
|
-
self,
|
|
63
|
-
output_type: OutputType,
|
|
64
|
-
text: str | None = None,
|
|
65
|
-
image: NotebookOutputImage | None = None
|
|
66
|
-
):
|
|
67
|
-
"""Initialize a notebook cell output.
|
|
68
|
-
|
|
69
|
-
Args:
|
|
70
|
-
output_type: Type of output
|
|
71
|
-
text: Text output (if any)
|
|
72
|
-
image: Image output (if any)
|
|
73
|
-
"""
|
|
74
|
-
self.output_type = output_type
|
|
75
|
-
self.text = text
|
|
76
|
-
self.image = image
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@final
|
|
80
|
-
class NotebookCellSource:
|
|
81
|
-
"""Representation of a source cell from a notebook."""
|
|
82
|
-
|
|
83
|
-
def __init__(
|
|
84
|
-
self,
|
|
85
|
-
cell_index: int,
|
|
86
|
-
cell_type: CellType,
|
|
87
|
-
source: str,
|
|
88
|
-
language: str,
|
|
89
|
-
execution_count: int | None = None,
|
|
90
|
-
outputs: list[NotebookCellOutput] | None = None
|
|
91
|
-
):
|
|
92
|
-
"""Initialize a notebook cell source.
|
|
93
|
-
|
|
94
|
-
Args:
|
|
95
|
-
cell_index: Index of the cell in the notebook
|
|
96
|
-
cell_type: Type of cell (code or markdown)
|
|
97
|
-
source: Source code or text of the cell
|
|
98
|
-
language: Programming language of the cell
|
|
99
|
-
execution_count: Execution count of the cell (if any)
|
|
100
|
-
outputs: Outputs from the cell (if any)
|
|
101
|
-
"""
|
|
102
|
-
self.cell_index = cell_index
|
|
103
|
-
self.cell_type = cell_type
|
|
104
|
-
self.source = source
|
|
105
|
-
self.language = language
|
|
106
|
-
self.execution_count = execution_count
|
|
107
|
-
self.outputs = outputs or []
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
@final
|
|
111
|
-
class JupyterNotebookTools:
|
|
112
|
-
"""Tools for working with Jupyter notebooks."""
|
|
113
|
-
|
|
114
|
-
def __init__(
|
|
115
|
-
self, document_context: DocumentContext, permission_manager: PermissionManager
|
|
116
|
-
) -> None:
|
|
117
|
-
"""Initialize notebook tools.
|
|
118
|
-
|
|
119
|
-
Args:
|
|
120
|
-
document_context: Document context for tracking file contents
|
|
121
|
-
permission_manager: Permission manager for access control
|
|
122
|
-
"""
|
|
123
|
-
self.document_context: DocumentContext = document_context
|
|
124
|
-
self.permission_manager: PermissionManager = permission_manager
|
|
125
|
-
|
|
126
|
-
def register_tools(self, mcp_server: FastMCP) -> None:
|
|
127
|
-
"""Register jupyter notebook tools with the MCP server.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
mcp_server: The FastMCP server instance
|
|
131
|
-
"""
|
|
132
|
-
|
|
133
|
-
@mcp_server.tool()
|
|
134
|
-
async def read_notebook(path: str, ctx: MCPContext) -> str:
|
|
135
|
-
"""Extract and read source code from all cells in a Jupyter notebook.
|
|
136
|
-
|
|
137
|
-
Reads a Jupyter notebook (.ipynb file) and returns all of the cells with
|
|
138
|
-
their outputs. Jupyter notebooks are interactive documents that combine
|
|
139
|
-
code, text, and visualizations, commonly used for data analysis and
|
|
140
|
-
scientific computing.
|
|
141
|
-
|
|
142
|
-
Args:
|
|
143
|
-
path: Absolute path to the notebook file (must be absolute, not relative)
|
|
144
|
-
|
|
145
|
-
Returns:
|
|
146
|
-
Formatted content of all notebook cells with their outputs
|
|
147
|
-
"""
|
|
148
|
-
tool_ctx = create_tool_context(ctx)
|
|
149
|
-
tool_ctx.set_tool_info("read_notebook")
|
|
150
|
-
|
|
151
|
-
# Validate path parameter
|
|
152
|
-
path_validation = validate_path_parameter(path)
|
|
153
|
-
if path_validation.is_error:
|
|
154
|
-
await tool_ctx.error(path_validation.error_message)
|
|
155
|
-
return f"Error: {path_validation.error_message}"
|
|
156
|
-
|
|
157
|
-
await tool_ctx.info(f"Reading notebook: {path}")
|
|
158
|
-
|
|
159
|
-
# Check if path is allowed to be read
|
|
160
|
-
if not self.permission_manager.is_path_allowed(path):
|
|
161
|
-
await tool_ctx.error(
|
|
162
|
-
f"Access denied - path outside allowed directories: {path}"
|
|
163
|
-
)
|
|
164
|
-
return f"Error: Access denied - path outside allowed directories: {path}"
|
|
165
|
-
|
|
166
|
-
try:
|
|
167
|
-
file_path = Path(path)
|
|
168
|
-
|
|
169
|
-
if not file_path.exists():
|
|
170
|
-
await tool_ctx.error(f"File does not exist: {path}")
|
|
171
|
-
return f"Error: File does not exist: {path}"
|
|
172
|
-
|
|
173
|
-
if not file_path.is_file():
|
|
174
|
-
await tool_ctx.error(f"Path is not a file: {path}")
|
|
175
|
-
return f"Error: Path is not a file: {path}"
|
|
176
|
-
|
|
177
|
-
# Check file extension
|
|
178
|
-
if file_path.suffix.lower() != ".ipynb":
|
|
179
|
-
await tool_ctx.error(f"File is not a Jupyter notebook: {path}")
|
|
180
|
-
return f"Error: File is not a Jupyter notebook: {path}"
|
|
181
|
-
|
|
182
|
-
# Read and parse the notebook
|
|
183
|
-
try:
|
|
184
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
185
|
-
content = f.read()
|
|
186
|
-
notebook = json.loads(content)
|
|
187
|
-
except json.JSONDecodeError:
|
|
188
|
-
await tool_ctx.error(f"Invalid notebook format: {path}")
|
|
189
|
-
return f"Error: Invalid notebook format: {path}"
|
|
190
|
-
except UnicodeDecodeError:
|
|
191
|
-
await tool_ctx.error(f"Cannot read notebook file: {path}")
|
|
192
|
-
return f"Error: Cannot read notebook file: {path}"
|
|
193
|
-
|
|
194
|
-
# Add to document context
|
|
195
|
-
self.document_context.add_document(path, content)
|
|
196
|
-
|
|
197
|
-
# Process notebook cells
|
|
198
|
-
# Get notebook language
|
|
199
|
-
language = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
|
|
200
|
-
cells = notebook.get("cells", [])
|
|
201
|
-
processed_cells = []
|
|
202
|
-
|
|
203
|
-
for i, cell in enumerate(cells):
|
|
204
|
-
cell_type = cell.get("cell_type", "code")
|
|
205
|
-
|
|
206
|
-
# Skip if not code or markdown
|
|
207
|
-
if cell_type not in ["code", "markdown"]:
|
|
208
|
-
continue
|
|
209
|
-
|
|
210
|
-
# Get source
|
|
211
|
-
source = cell.get("source", "")
|
|
212
|
-
if isinstance(source, list):
|
|
213
|
-
source = "".join(source)
|
|
214
|
-
|
|
215
|
-
# Get execution count for code cells
|
|
216
|
-
execution_count = None
|
|
217
|
-
if cell_type == "code":
|
|
218
|
-
execution_count = cell.get("execution_count")
|
|
219
|
-
|
|
220
|
-
# Process outputs for code cells
|
|
221
|
-
outputs = []
|
|
222
|
-
if cell_type == "code" and "outputs" in cell:
|
|
223
|
-
for output in cell["outputs"]:
|
|
224
|
-
output_type = output.get("output_type", "")
|
|
225
|
-
|
|
226
|
-
# Process different output types
|
|
227
|
-
if output_type == "stream":
|
|
228
|
-
text = output.get("text", "")
|
|
229
|
-
if isinstance(text, list):
|
|
230
|
-
text = "".join(text)
|
|
231
|
-
outputs.append(NotebookCellOutput(output_type="stream", text=text))
|
|
232
|
-
|
|
233
|
-
elif output_type in ["execute_result", "display_data"]:
|
|
234
|
-
# Process text output
|
|
235
|
-
text = None
|
|
236
|
-
if "data" in output and "text/plain" in output["data"]:
|
|
237
|
-
text_data = output["data"]["text/plain"]
|
|
238
|
-
if isinstance(text_data, list):
|
|
239
|
-
text = "".join(text_data)
|
|
240
|
-
else:
|
|
241
|
-
text = text_data
|
|
242
|
-
|
|
243
|
-
# Process image output
|
|
244
|
-
image = None
|
|
245
|
-
if "data" in output:
|
|
246
|
-
if "image/png" in output["data"]:
|
|
247
|
-
image = NotebookOutputImage(
|
|
248
|
-
image_data=output["data"]["image/png"],
|
|
249
|
-
media_type="image/png"
|
|
250
|
-
)
|
|
251
|
-
elif "image/jpeg" in output["data"]:
|
|
252
|
-
image = NotebookOutputImage(
|
|
253
|
-
image_data=output["data"]["image/jpeg"],
|
|
254
|
-
media_type="image/jpeg"
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
outputs.append(
|
|
258
|
-
NotebookCellOutput(
|
|
259
|
-
output_type=output_type,
|
|
260
|
-
text=text,
|
|
261
|
-
image=image
|
|
262
|
-
)
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
elif output_type == "error":
|
|
266
|
-
# Format error traceback
|
|
267
|
-
ename = output.get("ename", "")
|
|
268
|
-
evalue = output.get("evalue", "")
|
|
269
|
-
traceback = output.get("traceback", [])
|
|
270
|
-
|
|
271
|
-
# Handle raw text strings and lists of strings
|
|
272
|
-
if isinstance(traceback, list):
|
|
273
|
-
# Clean ANSI escape codes and join the list but preserve the formatting
|
|
274
|
-
clean_traceback = [clean_ansi_escapes(line) for line in traceback]
|
|
275
|
-
traceback_text = "\n".join(clean_traceback)
|
|
276
|
-
else:
|
|
277
|
-
traceback_text = clean_ansi_escapes(str(traceback))
|
|
278
|
-
|
|
279
|
-
error_text = f"{ename}: {evalue}\n{traceback_text}"
|
|
280
|
-
outputs.append(NotebookCellOutput(output_type="error", text=error_text))
|
|
281
|
-
|
|
282
|
-
# Create cell object
|
|
283
|
-
processed_cell = NotebookCellSource(
|
|
284
|
-
cell_index=i,
|
|
285
|
-
cell_type=cell_type,
|
|
286
|
-
source=source,
|
|
287
|
-
language=language,
|
|
288
|
-
execution_count=execution_count,
|
|
289
|
-
outputs=outputs
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
processed_cells.append(processed_cell)
|
|
293
|
-
|
|
294
|
-
# Format the notebook content as a readable string
|
|
295
|
-
result = []
|
|
296
|
-
for cell in processed_cells:
|
|
297
|
-
# Format the cell header
|
|
298
|
-
cell_header = f"Cell [{cell.cell_index}] {cell.cell_type}"
|
|
299
|
-
if cell.execution_count is not None:
|
|
300
|
-
cell_header += f" (execution_count: {cell.execution_count})"
|
|
301
|
-
if cell.cell_type == "code" and cell.language != "python":
|
|
302
|
-
cell_header += f" [{cell.language}]"
|
|
303
|
-
|
|
304
|
-
# Add cell to result
|
|
305
|
-
result.append(f"{cell_header}:")
|
|
306
|
-
result.append(f"```{cell.language if cell.cell_type == 'code' else ''}")
|
|
307
|
-
result.append(cell.source)
|
|
308
|
-
result.append("```")
|
|
309
|
-
|
|
310
|
-
# Add outputs if any
|
|
311
|
-
if cell.outputs:
|
|
312
|
-
result.append("Outputs:")
|
|
313
|
-
for output in cell.outputs:
|
|
314
|
-
if output.output_type == "error":
|
|
315
|
-
result.append("Error:")
|
|
316
|
-
result.append("```")
|
|
317
|
-
result.append(output.text)
|
|
318
|
-
result.append("```")
|
|
319
|
-
elif output.text:
|
|
320
|
-
result.append("Output:")
|
|
321
|
-
result.append("```")
|
|
322
|
-
result.append(output.text)
|
|
323
|
-
result.append("```")
|
|
324
|
-
if output.image:
|
|
325
|
-
result.append(f"[Image output: {output.image.media_type}]")
|
|
326
|
-
|
|
327
|
-
result.append("") # Empty line between cells
|
|
328
|
-
|
|
329
|
-
await tool_ctx.info(f"Successfully read notebook: {path} ({len(processed_cells)} cells)")
|
|
330
|
-
return "\n".join(result)
|
|
331
|
-
except Exception as e:
|
|
332
|
-
await tool_ctx.error(f"Error reading notebook: {str(e)}")
|
|
333
|
-
return f"Error reading notebook: {str(e)}"
|
|
334
|
-
|
|
335
|
-
@mcp_server.tool()
|
|
336
|
-
async def edit_notebook(
|
|
337
|
-
path: str,
|
|
338
|
-
cell_number: int,
|
|
339
|
-
new_source: str,
|
|
340
|
-
ctx: MCPContext,
|
|
341
|
-
cell_type: CellType | None = None,
|
|
342
|
-
edit_mode: EditMode = "replace"
|
|
343
|
-
) -> str:
|
|
344
|
-
"""Edit a specific cell in a Jupyter notebook.
|
|
345
|
-
|
|
346
|
-
Enables editing, inserting, or deleting cells in a Jupyter notebook (.ipynb file).
|
|
347
|
-
In replace mode, the specified cell's source is updated with the new content.
|
|
348
|
-
In insert mode, a new cell is added at the specified index.
|
|
349
|
-
In delete mode, the specified cell is removed.
|
|
350
|
-
|
|
351
|
-
Args:
|
|
352
|
-
path: Absolute path to the notebook file (must be absolute, not relative)
|
|
353
|
-
cell_number: Zero-based index of the cell to edit
|
|
354
|
-
new_source: New source code or text for the cell (ignored in delete mode)
|
|
355
|
-
cell_type: Type of cell (code or markdown), default is to keep existing type
|
|
356
|
-
edit_mode: Type of edit operation (replace, insert, delete), default is replace
|
|
357
|
-
|
|
358
|
-
Returns:
|
|
359
|
-
Result of the edit operation with details on changes made
|
|
360
|
-
"""
|
|
361
|
-
tool_ctx = create_tool_context(ctx)
|
|
362
|
-
tool_ctx.set_tool_info("edit_notebook")
|
|
363
|
-
|
|
364
|
-
# Validate path parameter
|
|
365
|
-
path_validation = validate_path_parameter(path)
|
|
366
|
-
if path_validation.is_error:
|
|
367
|
-
await tool_ctx.error(path_validation.error_message)
|
|
368
|
-
return f"Error: {path_validation.error_message}"
|
|
369
|
-
|
|
370
|
-
# Validate cell_number
|
|
371
|
-
if cell_number < 0:
|
|
372
|
-
await tool_ctx.error("Cell number must be non-negative")
|
|
373
|
-
return "Error: Cell number must be non-negative"
|
|
374
|
-
|
|
375
|
-
# Validate edit_mode
|
|
376
|
-
if edit_mode not in ["replace", "insert", "delete"]:
|
|
377
|
-
await tool_ctx.error("Edit mode must be replace, insert, or delete")
|
|
378
|
-
return "Error: Edit mode must be replace, insert, or delete"
|
|
379
|
-
|
|
380
|
-
# In insert mode, cell_type is required
|
|
381
|
-
if edit_mode == "insert" and cell_type is None:
|
|
382
|
-
await tool_ctx.error("Cell type is required when using insert mode")
|
|
383
|
-
return "Error: Cell type is required when using insert mode"
|
|
384
|
-
|
|
385
|
-
# Don't validate new_source for delete mode
|
|
386
|
-
if edit_mode != "delete" and not new_source:
|
|
387
|
-
await tool_ctx.error("New source is required for replace or insert operations")
|
|
388
|
-
return "Error: New source is required for replace or insert operations"
|
|
389
|
-
|
|
390
|
-
await tool_ctx.info(f"Editing notebook: {path} (cell: {cell_number}, mode: {edit_mode})")
|
|
391
|
-
|
|
392
|
-
# Check if path is allowed
|
|
393
|
-
if not self.permission_manager.is_path_allowed(path):
|
|
394
|
-
await tool_ctx.error(
|
|
395
|
-
f"Access denied - path outside allowed directories: {path}"
|
|
396
|
-
)
|
|
397
|
-
return f"Error: Access denied - path outside allowed directories: {path}"
|
|
398
|
-
|
|
399
|
-
try:
|
|
400
|
-
file_path = Path(path)
|
|
401
|
-
|
|
402
|
-
if not file_path.exists():
|
|
403
|
-
await tool_ctx.error(f"File does not exist: {path}")
|
|
404
|
-
return f"Error: File does not exist: {path}"
|
|
405
|
-
|
|
406
|
-
if not file_path.is_file():
|
|
407
|
-
await tool_ctx.error(f"Path is not a file: {path}")
|
|
408
|
-
return f"Error: Path is not a file: {path}"
|
|
409
|
-
|
|
410
|
-
# Check file extension
|
|
411
|
-
if file_path.suffix.lower() != ".ipynb":
|
|
412
|
-
await tool_ctx.error(f"File is not a Jupyter notebook: {path}")
|
|
413
|
-
return f"Error: File is not a Jupyter notebook: {path}"
|
|
414
|
-
|
|
415
|
-
# Read and parse the notebook
|
|
416
|
-
try:
|
|
417
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
418
|
-
content = f.read()
|
|
419
|
-
notebook = json.loads(content)
|
|
420
|
-
except json.JSONDecodeError:
|
|
421
|
-
await tool_ctx.error(f"Invalid notebook format: {path}")
|
|
422
|
-
return f"Error: Invalid notebook format: {path}"
|
|
423
|
-
except UnicodeDecodeError:
|
|
424
|
-
await tool_ctx.error(f"Cannot read notebook file: {path}")
|
|
425
|
-
return f"Error: Cannot read notebook file: {path}"
|
|
426
|
-
|
|
427
|
-
# Check cell_number is valid
|
|
428
|
-
cells = notebook.get("cells", [])
|
|
429
|
-
|
|
430
|
-
if edit_mode == "insert":
|
|
431
|
-
if cell_number > len(cells):
|
|
432
|
-
await tool_ctx.error(f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})")
|
|
433
|
-
return f"Error: Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
|
|
434
|
-
else: # replace or delete
|
|
435
|
-
if cell_number >= len(cells):
|
|
436
|
-
await tool_ctx.error(f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})")
|
|
437
|
-
return f"Error: Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
|
|
438
|
-
|
|
439
|
-
# Get notebook language (needed for context but not directly used in this block)
|
|
440
|
-
_ = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
|
|
441
|
-
|
|
442
|
-
# Perform the requested operation
|
|
443
|
-
if edit_mode == "replace":
|
|
444
|
-
# Get the target cell
|
|
445
|
-
target_cell = cells[cell_number]
|
|
446
|
-
|
|
447
|
-
# Store previous contents for reporting
|
|
448
|
-
old_type = target_cell.get("cell_type", "code")
|
|
449
|
-
old_source = target_cell.get("source", "")
|
|
450
|
-
if isinstance(old_source, list):
|
|
451
|
-
old_source = "".join(old_source)
|
|
452
|
-
|
|
453
|
-
# Update source
|
|
454
|
-
target_cell["source"] = new_source
|
|
455
|
-
|
|
456
|
-
# Update type if specified
|
|
457
|
-
if cell_type is not None:
|
|
458
|
-
target_cell["cell_type"] = cell_type
|
|
459
|
-
|
|
460
|
-
# If changing to markdown, remove code-specific fields
|
|
461
|
-
if cell_type == "markdown":
|
|
462
|
-
if "outputs" in target_cell:
|
|
463
|
-
del target_cell["outputs"]
|
|
464
|
-
if "execution_count" in target_cell:
|
|
465
|
-
del target_cell["execution_count"]
|
|
466
|
-
|
|
467
|
-
# If code cell, reset execution
|
|
468
|
-
if target_cell["cell_type"] == "code":
|
|
469
|
-
target_cell["outputs"] = []
|
|
470
|
-
target_cell["execution_count"] = None
|
|
471
|
-
|
|
472
|
-
change_description = f"Replaced cell {cell_number}"
|
|
473
|
-
if cell_type is not None and cell_type != old_type:
|
|
474
|
-
change_description += f" (changed type from {old_type} to {cell_type})"
|
|
475
|
-
|
|
476
|
-
elif edit_mode == "insert":
|
|
477
|
-
# Create new cell
|
|
478
|
-
new_cell: dict[str, Any] = {
|
|
479
|
-
"cell_type": cell_type,
|
|
480
|
-
"source": new_source,
|
|
481
|
-
"metadata": {}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
# Add code-specific fields
|
|
485
|
-
if cell_type == "code":
|
|
486
|
-
new_cell["outputs"] = []
|
|
487
|
-
new_cell["execution_count"] = None
|
|
488
|
-
|
|
489
|
-
# Insert the cell
|
|
490
|
-
cells.insert(cell_number, new_cell)
|
|
491
|
-
change_description = f"Inserted new {cell_type} cell at position {cell_number}"
|
|
492
|
-
|
|
493
|
-
else: # delete
|
|
494
|
-
# Store deleted cell info for reporting
|
|
495
|
-
deleted_cell = cells[cell_number]
|
|
496
|
-
deleted_type = deleted_cell.get("cell_type", "code")
|
|
497
|
-
|
|
498
|
-
# Remove the cell
|
|
499
|
-
del cells[cell_number]
|
|
500
|
-
change_description = f"Deleted {deleted_type} cell at position {cell_number}"
|
|
501
|
-
|
|
502
|
-
# Write the updated notebook back to file
|
|
503
|
-
with open(file_path, "w", encoding="utf-8") as f:
|
|
504
|
-
json.dump(notebook, f, indent=1)
|
|
505
|
-
|
|
506
|
-
# Update document context
|
|
507
|
-
updated_content = json.dumps(notebook, indent=1)
|
|
508
|
-
self.document_context.update_document(path, updated_content)
|
|
509
|
-
|
|
510
|
-
await tool_ctx.info(f"Successfully edited notebook: {path} - {change_description}")
|
|
511
|
-
return f"Successfully edited notebook: {path} - {change_description}"
|
|
512
|
-
except Exception as e:
|
|
513
|
-
await tool_ctx.error(f"Error editing notebook: {str(e)}")
|
|
514
|
-
return f"Error editing notebook: {str(e)}"
|
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
"""Read notebook tool implementation.
|
|
2
|
-
|
|
3
|
-
This module provides the ReadNotebookTool for reading Jupyter notebook files.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
import json
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Any, final, override
|
|
9
|
-
|
|
10
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
11
|
-
from mcp.server.fastmcp import FastMCP
|
|
12
|
-
|
|
13
|
-
from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@final
|
|
17
|
-
class ReadNotebookTool(JupyterBaseTool):
|
|
18
|
-
"""Tool for reading Jupyter notebook files."""
|
|
19
|
-
|
|
20
|
-
@property
|
|
21
|
-
@override
|
|
22
|
-
def name(self) -> str:
|
|
23
|
-
"""Get the tool name.
|
|
24
|
-
|
|
25
|
-
Returns:
|
|
26
|
-
Tool name
|
|
27
|
-
"""
|
|
28
|
-
return "read_notebook"
|
|
29
|
-
|
|
30
|
-
@property
|
|
31
|
-
@override
|
|
32
|
-
def description(self) -> str:
|
|
33
|
-
"""Get the tool description.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
Tool description
|
|
37
|
-
"""
|
|
38
|
-
return """Extract and read source code from all cells in a Jupyter notebook.
|
|
39
|
-
|
|
40
|
-
Reads a Jupyter notebook (.ipynb file) and returns all of the cells with
|
|
41
|
-
their outputs. Jupyter notebooks are interactive documents that combine
|
|
42
|
-
code, text, and visualizations, commonly used for data analysis and
|
|
43
|
-
scientific computing."""
|
|
44
|
-
|
|
45
|
-
@property
|
|
46
|
-
@override
|
|
47
|
-
def parameters(self) -> dict[str, Any]:
|
|
48
|
-
"""Get the parameter specifications for the tool.
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Parameter specifications
|
|
52
|
-
"""
|
|
53
|
-
return {
|
|
54
|
-
"properties": {
|
|
55
|
-
"path": {
|
|
56
|
-
"type": "string",
|
|
57
|
-
"description": "path to the Jupyter notebook file"
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
"required": ["path"],
|
|
61
|
-
"type": "object"
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@property
|
|
65
|
-
@override
|
|
66
|
-
def required(self) -> list[str]:
|
|
67
|
-
"""Get the list of required parameter names.
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
List of required parameter names
|
|
71
|
-
"""
|
|
72
|
-
return ["path"]
|
|
73
|
-
|
|
74
|
-
@override
|
|
75
|
-
async def call(self, ctx: MCPContext, **params: Any) -> str:
|
|
76
|
-
"""Execute the tool with the given parameters.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
ctx: MCP context
|
|
80
|
-
**params: Tool parameters
|
|
81
|
-
|
|
82
|
-
Returns:
|
|
83
|
-
Tool result
|
|
84
|
-
"""
|
|
85
|
-
tool_ctx = self.create_tool_context(ctx)
|
|
86
|
-
self.set_tool_context_info(tool_ctx)
|
|
87
|
-
|
|
88
|
-
# Extract parameters
|
|
89
|
-
path = params.get("path")
|
|
90
|
-
|
|
91
|
-
if not path:
|
|
92
|
-
await tool_ctx.error("Missing required parameter: path")
|
|
93
|
-
return "Error: Missing required parameter: path"
|
|
94
|
-
|
|
95
|
-
# Validate path parameter
|
|
96
|
-
path_validation = self.validate_path(path)
|
|
97
|
-
if path_validation.is_error:
|
|
98
|
-
await tool_ctx.error(path_validation.error_message)
|
|
99
|
-
return f"Error: {path_validation.error_message}"
|
|
100
|
-
|
|
101
|
-
await tool_ctx.info(f"Reading notebook: {path}")
|
|
102
|
-
|
|
103
|
-
# Check if path is allowed
|
|
104
|
-
if not self.is_path_allowed(path):
|
|
105
|
-
await tool_ctx.error(
|
|
106
|
-
f"Access denied - path outside allowed directories: {path}"
|
|
107
|
-
)
|
|
108
|
-
return f"Error: Access denied - path outside allowed directories: {path}"
|
|
109
|
-
|
|
110
|
-
try:
|
|
111
|
-
file_path = Path(path)
|
|
112
|
-
|
|
113
|
-
if not file_path.exists():
|
|
114
|
-
await tool_ctx.error(f"File does not exist: {path}")
|
|
115
|
-
return f"Error: File does not exist: {path}"
|
|
116
|
-
|
|
117
|
-
if not file_path.is_file():
|
|
118
|
-
await tool_ctx.error(f"Path is not a file: {path}")
|
|
119
|
-
return f"Error: Path is not a file: {path}"
|
|
120
|
-
|
|
121
|
-
# Check file extension
|
|
122
|
-
if file_path.suffix.lower() != ".ipynb":
|
|
123
|
-
await tool_ctx.error(f"File is not a Jupyter notebook: {path}")
|
|
124
|
-
return f"Error: File is not a Jupyter notebook: {path}"
|
|
125
|
-
|
|
126
|
-
# Read and parse the notebook
|
|
127
|
-
try:
|
|
128
|
-
# This will read the file, so we don't need to read it separately
|
|
129
|
-
_, processed_cells = await self.parse_notebook(file_path)
|
|
130
|
-
|
|
131
|
-
# Add to document context
|
|
132
|
-
with open(file_path, "r", encoding="utf-8") as f:
|
|
133
|
-
content = f.read()
|
|
134
|
-
self.document_context.add_document(path, content)
|
|
135
|
-
|
|
136
|
-
# Format the notebook content as a readable string
|
|
137
|
-
result = self.format_notebook_cells(processed_cells)
|
|
138
|
-
|
|
139
|
-
await tool_ctx.info(f"Successfully read notebook: {path} ({len(processed_cells)} cells)")
|
|
140
|
-
return result
|
|
141
|
-
except json.JSONDecodeError:
|
|
142
|
-
await tool_ctx.error(f"Invalid notebook format: {path}")
|
|
143
|
-
return f"Error: Invalid notebook format: {path}"
|
|
144
|
-
except UnicodeDecodeError:
|
|
145
|
-
await tool_ctx.error(f"Cannot read notebook file: {path}")
|
|
146
|
-
return f"Error: Cannot read notebook file: {path}"
|
|
147
|
-
except Exception as e:
|
|
148
|
-
await tool_ctx.error(f"Error reading notebook: {str(e)}")
|
|
149
|
-
return f"Error reading notebook: {str(e)}"
|
|
150
|
-
|
|
151
|
-
@override
|
|
152
|
-
def register(self, mcp_server: FastMCP) -> None:
|
|
153
|
-
"""Register this read notebook tool with the MCP server.
|
|
154
|
-
|
|
155
|
-
Creates a wrapper function with explicitly defined parameters that match
|
|
156
|
-
the tool's parameter schema and registers it with the MCP server.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
mcp_server: The FastMCP server instance
|
|
160
|
-
"""
|
|
161
|
-
tool_self = self # Create a reference to self for use in the closure
|
|
162
|
-
|
|
163
|
-
@mcp_server.tool(name=self.name, description=self.mcp_description)
|
|
164
|
-
async def read_notebook(path: str, ctx: MCPContext) -> str:
|
|
165
|
-
return await tool_self.call(ctx, path=path)
|