hanzo-mcp 0.1.21__py3-none-any.whl → 0.1.30__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 +81 -10
- hanzo_mcp/server.py +42 -11
- hanzo_mcp/tools/__init__.py +51 -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 +18 -1
- hanzo_mcp/tools/common/base.py +216 -0
- hanzo_mcp/tools/common/context.py +9 -5
- hanzo_mcp/tools/common/permissions.py +7 -3
- hanzo_mcp/tools/common/session.py +91 -0
- hanzo_mcp/tools/common/thinking_tool.py +123 -0
- hanzo_mcp/tools/common/validation.py +1 -1
- 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 +73 -113
- hanzo_mcp/tools/jupyter/read_notebook.py +165 -0
- hanzo_mcp/tools/project/__init__.py +64 -1
- hanzo_mcp/tools/project/analysis.py +8 -5
- 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 +198 -317
- 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.21.dist-info → hanzo_mcp-0.1.30.dist-info}/METADATA +72 -77
- hanzo_mcp-0.1.30.dist-info/RECORD +45 -0
- {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/WHEEL +1 -1
- {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.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.21.dist-info/RECORD +0 -23
- {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.1.21.dist-info → hanzo_mcp-0.1.30.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Jupyter notebook operations tools for Hanzo
|
|
1
|
+
"""Jupyter notebook operations tools for Hanzo MCP.
|
|
2
2
|
|
|
3
3
|
This module provides tools for reading and editing Jupyter notebook (.ipynb) files.
|
|
4
4
|
It supports reading notebook cells with their outputs and modifying notebook contents.
|
|
@@ -18,20 +18,19 @@ from hanzo_mcp.tools.common.validation import validate_path_parameter
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
# Pattern to match ANSI escape sequences
|
|
21
|
-
ANSI_ESCAPE_PATTERN = re.compile(r
|
|
22
|
-
|
|
21
|
+
ANSI_ESCAPE_PATTERN = re.compile(r'\x1B\[[0-9;]*[a-zA-Z]')
|
|
23
22
|
|
|
24
23
|
# Function to clean ANSI escape codes from text
|
|
25
24
|
def clean_ansi_escapes(text: str) -> str:
|
|
26
25
|
"""Remove ANSI escape sequences from text.
|
|
27
|
-
|
|
26
|
+
|
|
28
27
|
Args:
|
|
29
28
|
text: Text containing ANSI escape sequences
|
|
30
|
-
|
|
29
|
+
|
|
31
30
|
Returns:
|
|
32
31
|
Text with ANSI escape sequences removed
|
|
33
32
|
"""
|
|
34
|
-
return ANSI_ESCAPE_PATTERN.sub(
|
|
33
|
+
return ANSI_ESCAPE_PATTERN.sub('', text)
|
|
35
34
|
|
|
36
35
|
|
|
37
36
|
# Type definitions for Jupyter notebooks based on the nbformat spec
|
|
@@ -39,15 +38,14 @@ CellType = Literal["code", "markdown"]
|
|
|
39
38
|
OutputType = Literal["stream", "display_data", "execute_result", "error"]
|
|
40
39
|
EditMode = Literal["replace", "insert", "delete"]
|
|
41
40
|
|
|
42
|
-
|
|
43
41
|
# Define a structure for notebook cell outputs
|
|
44
42
|
@final
|
|
45
43
|
class NotebookOutputImage:
|
|
46
44
|
"""Representation of an image output in a notebook cell."""
|
|
47
|
-
|
|
45
|
+
|
|
48
46
|
def __init__(self, image_data: str, media_type: str):
|
|
49
47
|
"""Initialize a notebook output image.
|
|
50
|
-
|
|
48
|
+
|
|
51
49
|
Args:
|
|
52
50
|
image_data: Base64-encoded image data
|
|
53
51
|
media_type: Media type of the image (e.g., "image/png")
|
|
@@ -59,15 +57,15 @@ class NotebookOutputImage:
|
|
|
59
57
|
@final
|
|
60
58
|
class NotebookCellOutput:
|
|
61
59
|
"""Representation of an output from a notebook cell."""
|
|
62
|
-
|
|
60
|
+
|
|
63
61
|
def __init__(
|
|
64
|
-
self,
|
|
62
|
+
self,
|
|
65
63
|
output_type: OutputType,
|
|
66
64
|
text: str | None = None,
|
|
67
|
-
image: NotebookOutputImage | None = None
|
|
65
|
+
image: NotebookOutputImage | None = None
|
|
68
66
|
):
|
|
69
67
|
"""Initialize a notebook cell output.
|
|
70
|
-
|
|
68
|
+
|
|
71
69
|
Args:
|
|
72
70
|
output_type: Type of output
|
|
73
71
|
text: Text output (if any)
|
|
@@ -81,7 +79,7 @@ class NotebookCellOutput:
|
|
|
81
79
|
@final
|
|
82
80
|
class NotebookCellSource:
|
|
83
81
|
"""Representation of a source cell from a notebook."""
|
|
84
|
-
|
|
82
|
+
|
|
85
83
|
def __init__(
|
|
86
84
|
self,
|
|
87
85
|
cell_index: int,
|
|
@@ -89,10 +87,10 @@ class NotebookCellSource:
|
|
|
89
87
|
source: str,
|
|
90
88
|
language: str,
|
|
91
89
|
execution_count: int | None = None,
|
|
92
|
-
outputs: list[NotebookCellOutput] | None = None
|
|
90
|
+
outputs: list[NotebookCellOutput] | None = None
|
|
93
91
|
):
|
|
94
92
|
"""Initialize a notebook cell source.
|
|
95
|
-
|
|
93
|
+
|
|
96
94
|
Args:
|
|
97
95
|
cell_index: Index of the cell in the notebook
|
|
98
96
|
cell_type: Type of cell (code or markdown)
|
|
@@ -136,9 +134,9 @@ class JupyterNotebookTools:
|
|
|
136
134
|
async def read_notebook(path: str, ctx: MCPContext) -> str:
|
|
137
135
|
"""Extract and read source code from all cells in a Jupyter notebook.
|
|
138
136
|
|
|
139
|
-
Reads a Jupyter notebook (.ipynb file) and returns all of the cells with
|
|
140
|
-
their outputs. Jupyter notebooks are interactive documents that combine
|
|
141
|
-
code, text, and visualizations, commonly used for data analysis and
|
|
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
|
|
142
140
|
scientific computing.
|
|
143
141
|
|
|
144
142
|
Args:
|
|
@@ -163,9 +161,7 @@ class JupyterNotebookTools:
|
|
|
163
161
|
await tool_ctx.error(
|
|
164
162
|
f"Access denied - path outside allowed directories: {path}"
|
|
165
163
|
)
|
|
166
|
-
return
|
|
167
|
-
f"Error: Access denied - path outside allowed directories: {path}"
|
|
168
|
-
)
|
|
164
|
+
return f"Error: Access denied - path outside allowed directories: {path}"
|
|
169
165
|
|
|
170
166
|
try:
|
|
171
167
|
file_path = Path(path)
|
|
@@ -199,46 +195,41 @@ class JupyterNotebookTools:
|
|
|
199
195
|
self.document_context.add_document(path, content)
|
|
200
196
|
|
|
201
197
|
# Process notebook cells
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
.get("language_info", {})
|
|
205
|
-
.get("name", "python")
|
|
206
|
-
)
|
|
198
|
+
# Get notebook language
|
|
199
|
+
language = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
|
|
207
200
|
cells = notebook.get("cells", [])
|
|
208
201
|
processed_cells = []
|
|
209
202
|
|
|
210
203
|
for i, cell in enumerate(cells):
|
|
211
204
|
cell_type = cell.get("cell_type", "code")
|
|
212
|
-
|
|
205
|
+
|
|
213
206
|
# Skip if not code or markdown
|
|
214
207
|
if cell_type not in ["code", "markdown"]:
|
|
215
208
|
continue
|
|
216
|
-
|
|
209
|
+
|
|
217
210
|
# Get source
|
|
218
211
|
source = cell.get("source", "")
|
|
219
212
|
if isinstance(source, list):
|
|
220
213
|
source = "".join(source)
|
|
221
|
-
|
|
214
|
+
|
|
222
215
|
# Get execution count for code cells
|
|
223
216
|
execution_count = None
|
|
224
217
|
if cell_type == "code":
|
|
225
218
|
execution_count = cell.get("execution_count")
|
|
226
|
-
|
|
219
|
+
|
|
227
220
|
# Process outputs for code cells
|
|
228
221
|
outputs = []
|
|
229
222
|
if cell_type == "code" and "outputs" in cell:
|
|
230
223
|
for output in cell["outputs"]:
|
|
231
224
|
output_type = output.get("output_type", "")
|
|
232
|
-
|
|
225
|
+
|
|
233
226
|
# Process different output types
|
|
234
227
|
if output_type == "stream":
|
|
235
228
|
text = output.get("text", "")
|
|
236
229
|
if isinstance(text, list):
|
|
237
230
|
text = "".join(text)
|
|
238
|
-
outputs.append(
|
|
239
|
-
|
|
240
|
-
)
|
|
241
|
-
|
|
231
|
+
outputs.append(NotebookCellOutput(output_type="stream", text=text))
|
|
232
|
+
|
|
242
233
|
elif output_type in ["execute_result", "display_data"]:
|
|
243
234
|
# Process text output
|
|
244
235
|
text = None
|
|
@@ -248,50 +239,46 @@ class JupyterNotebookTools:
|
|
|
248
239
|
text = "".join(text_data)
|
|
249
240
|
else:
|
|
250
241
|
text = text_data
|
|
251
|
-
|
|
242
|
+
|
|
252
243
|
# Process image output
|
|
253
244
|
image = None
|
|
254
245
|
if "data" in output:
|
|
255
246
|
if "image/png" in output["data"]:
|
|
256
247
|
image = NotebookOutputImage(
|
|
257
248
|
image_data=output["data"]["image/png"],
|
|
258
|
-
media_type="image/png"
|
|
249
|
+
media_type="image/png"
|
|
259
250
|
)
|
|
260
251
|
elif "image/jpeg" in output["data"]:
|
|
261
252
|
image = NotebookOutputImage(
|
|
262
253
|
image_data=output["data"]["image/jpeg"],
|
|
263
|
-
media_type="image/jpeg"
|
|
254
|
+
media_type="image/jpeg"
|
|
264
255
|
)
|
|
265
|
-
|
|
256
|
+
|
|
266
257
|
outputs.append(
|
|
267
258
|
NotebookCellOutput(
|
|
268
|
-
output_type=output_type,
|
|
259
|
+
output_type=output_type,
|
|
260
|
+
text=text,
|
|
261
|
+
image=image
|
|
269
262
|
)
|
|
270
263
|
)
|
|
271
|
-
|
|
264
|
+
|
|
272
265
|
elif output_type == "error":
|
|
273
266
|
# Format error traceback
|
|
274
267
|
ename = output.get("ename", "")
|
|
275
268
|
evalue = output.get("evalue", "")
|
|
276
269
|
traceback = output.get("traceback", [])
|
|
277
|
-
|
|
270
|
+
|
|
278
271
|
# Handle raw text strings and lists of strings
|
|
279
272
|
if isinstance(traceback, list):
|
|
280
273
|
# Clean ANSI escape codes and join the list but preserve the formatting
|
|
281
|
-
clean_traceback = [
|
|
282
|
-
clean_ansi_escapes(line) for line in traceback
|
|
283
|
-
]
|
|
274
|
+
clean_traceback = [clean_ansi_escapes(line) for line in traceback]
|
|
284
275
|
traceback_text = "\n".join(clean_traceback)
|
|
285
276
|
else:
|
|
286
277
|
traceback_text = clean_ansi_escapes(str(traceback))
|
|
287
|
-
|
|
278
|
+
|
|
288
279
|
error_text = f"{ename}: {evalue}\n{traceback_text}"
|
|
289
|
-
outputs.append(
|
|
290
|
-
|
|
291
|
-
output_type="error", text=error_text
|
|
292
|
-
)
|
|
293
|
-
)
|
|
294
|
-
|
|
280
|
+
outputs.append(NotebookCellOutput(output_type="error", text=error_text))
|
|
281
|
+
|
|
295
282
|
# Create cell object
|
|
296
283
|
processed_cell = NotebookCellSource(
|
|
297
284
|
cell_index=i,
|
|
@@ -299,9 +286,9 @@ class JupyterNotebookTools:
|
|
|
299
286
|
source=source,
|
|
300
287
|
language=language,
|
|
301
288
|
execution_count=execution_count,
|
|
302
|
-
outputs=outputs
|
|
289
|
+
outputs=outputs
|
|
303
290
|
)
|
|
304
|
-
|
|
291
|
+
|
|
305
292
|
processed_cells.append(processed_cell)
|
|
306
293
|
|
|
307
294
|
# Format the notebook content as a readable string
|
|
@@ -313,15 +300,13 @@ class JupyterNotebookTools:
|
|
|
313
300
|
cell_header += f" (execution_count: {cell.execution_count})"
|
|
314
301
|
if cell.cell_type == "code" and cell.language != "python":
|
|
315
302
|
cell_header += f" [{cell.language}]"
|
|
316
|
-
|
|
303
|
+
|
|
317
304
|
# Add cell to result
|
|
318
305
|
result.append(f"{cell_header}:")
|
|
319
|
-
result.append(
|
|
320
|
-
f"```{cell.language if cell.cell_type == 'code' else ''}"
|
|
321
|
-
)
|
|
306
|
+
result.append(f"```{cell.language if cell.cell_type == 'code' else ''}")
|
|
322
307
|
result.append(cell.source)
|
|
323
308
|
result.append("```")
|
|
324
|
-
|
|
309
|
+
|
|
325
310
|
# Add outputs if any
|
|
326
311
|
if cell.outputs:
|
|
327
312
|
result.append("Outputs:")
|
|
@@ -337,15 +322,11 @@ class JupyterNotebookTools:
|
|
|
337
322
|
result.append(output.text)
|
|
338
323
|
result.append("```")
|
|
339
324
|
if output.image:
|
|
340
|
-
result.append(
|
|
341
|
-
|
|
342
|
-
)
|
|
343
|
-
|
|
325
|
+
result.append(f"[Image output: {output.image.media_type}]")
|
|
326
|
+
|
|
344
327
|
result.append("") # Empty line between cells
|
|
345
328
|
|
|
346
|
-
await tool_ctx.info(
|
|
347
|
-
f"Successfully read notebook: {path} ({len(processed_cells)} cells)"
|
|
348
|
-
)
|
|
329
|
+
await tool_ctx.info(f"Successfully read notebook: {path} ({len(processed_cells)} cells)")
|
|
349
330
|
return "\n".join(result)
|
|
350
331
|
except Exception as e:
|
|
351
332
|
await tool_ctx.error(f"Error reading notebook: {str(e)}")
|
|
@@ -353,12 +334,12 @@ class JupyterNotebookTools:
|
|
|
353
334
|
|
|
354
335
|
@mcp_server.tool()
|
|
355
336
|
async def edit_notebook(
|
|
356
|
-
path: str,
|
|
357
|
-
cell_number: int,
|
|
358
|
-
new_source: str,
|
|
337
|
+
path: str,
|
|
338
|
+
cell_number: int,
|
|
339
|
+
new_source: str,
|
|
359
340
|
ctx: MCPContext,
|
|
360
341
|
cell_type: CellType | None = None,
|
|
361
|
-
edit_mode: EditMode = "replace"
|
|
342
|
+
edit_mode: EditMode = "replace"
|
|
362
343
|
) -> str:
|
|
363
344
|
"""Edit a specific cell in a Jupyter notebook.
|
|
364
345
|
|
|
@@ -403,23 +384,17 @@ class JupyterNotebookTools:
|
|
|
403
384
|
|
|
404
385
|
# Don't validate new_source for delete mode
|
|
405
386
|
if edit_mode != "delete" and not new_source:
|
|
406
|
-
await tool_ctx.error(
|
|
407
|
-
"New source is required for replace or insert operations"
|
|
408
|
-
)
|
|
387
|
+
await tool_ctx.error("New source is required for replace or insert operations")
|
|
409
388
|
return "Error: New source is required for replace or insert operations"
|
|
410
389
|
|
|
411
|
-
await tool_ctx.info(
|
|
412
|
-
f"Editing notebook: {path} (cell: {cell_number}, mode: {edit_mode})"
|
|
413
|
-
)
|
|
390
|
+
await tool_ctx.info(f"Editing notebook: {path} (cell: {cell_number}, mode: {edit_mode})")
|
|
414
391
|
|
|
415
392
|
# Check if path is allowed
|
|
416
393
|
if not self.permission_manager.is_path_allowed(path):
|
|
417
394
|
await tool_ctx.error(
|
|
418
395
|
f"Access denied - path outside allowed directories: {path}"
|
|
419
396
|
)
|
|
420
|
-
return
|
|
421
|
-
f"Error: Access denied - path outside allowed directories: {path}"
|
|
422
|
-
)
|
|
397
|
+
return f"Error: Access denied - path outside allowed directories: {path}"
|
|
423
398
|
|
|
424
399
|
try:
|
|
425
400
|
file_path = Path(path)
|
|
@@ -451,51 +426,44 @@ class JupyterNotebookTools:
|
|
|
451
426
|
|
|
452
427
|
# Check cell_number is valid
|
|
453
428
|
cells = notebook.get("cells", [])
|
|
454
|
-
|
|
429
|
+
|
|
455
430
|
if edit_mode == "insert":
|
|
456
431
|
if cell_number > len(cells):
|
|
457
|
-
await tool_ctx.error(
|
|
458
|
-
f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
|
|
459
|
-
)
|
|
432
|
+
await tool_ctx.error(f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})")
|
|
460
433
|
return f"Error: Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
|
|
461
434
|
else: # replace or delete
|
|
462
435
|
if cell_number >= len(cells):
|
|
463
|
-
await tool_ctx.error(
|
|
464
|
-
f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
|
|
465
|
-
)
|
|
436
|
+
await tool_ctx.error(f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})")
|
|
466
437
|
return f"Error: Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
|
|
467
438
|
|
|
468
|
-
language
|
|
469
|
-
|
|
470
|
-
.get("language_info", {})
|
|
471
|
-
.get("name", "python")
|
|
472
|
-
)
|
|
439
|
+
# Get notebook language (needed for context but not directly used in this block)
|
|
440
|
+
_ = notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
|
|
473
441
|
|
|
474
442
|
# Perform the requested operation
|
|
475
443
|
if edit_mode == "replace":
|
|
476
444
|
# Get the target cell
|
|
477
445
|
target_cell = cells[cell_number]
|
|
478
|
-
|
|
446
|
+
|
|
479
447
|
# Store previous contents for reporting
|
|
480
448
|
old_type = target_cell.get("cell_type", "code")
|
|
481
449
|
old_source = target_cell.get("source", "")
|
|
482
450
|
if isinstance(old_source, list):
|
|
483
451
|
old_source = "".join(old_source)
|
|
484
|
-
|
|
452
|
+
|
|
485
453
|
# Update source
|
|
486
454
|
target_cell["source"] = new_source
|
|
487
|
-
|
|
455
|
+
|
|
488
456
|
# Update type if specified
|
|
489
457
|
if cell_type is not None:
|
|
490
458
|
target_cell["cell_type"] = cell_type
|
|
491
|
-
|
|
459
|
+
|
|
492
460
|
# If changing to markdown, remove code-specific fields
|
|
493
461
|
if cell_type == "markdown":
|
|
494
462
|
if "outputs" in target_cell:
|
|
495
463
|
del target_cell["outputs"]
|
|
496
464
|
if "execution_count" in target_cell:
|
|
497
465
|
del target_cell["execution_count"]
|
|
498
|
-
|
|
466
|
+
|
|
499
467
|
# If code cell, reset execution
|
|
500
468
|
if target_cell["cell_type"] == "code":
|
|
501
469
|
target_cell["outputs"] = []
|
|
@@ -503,39 +471,33 @@ class JupyterNotebookTools:
|
|
|
503
471
|
|
|
504
472
|
change_description = f"Replaced cell {cell_number}"
|
|
505
473
|
if cell_type is not None and cell_type != old_type:
|
|
506
|
-
change_description += (
|
|
507
|
-
f" (changed type from {old_type} to {cell_type})"
|
|
508
|
-
)
|
|
474
|
+
change_description += f" (changed type from {old_type} to {cell_type})"
|
|
509
475
|
|
|
510
476
|
elif edit_mode == "insert":
|
|
511
477
|
# Create new cell
|
|
512
478
|
new_cell: dict[str, Any] = {
|
|
513
479
|
"cell_type": cell_type,
|
|
514
480
|
"source": new_source,
|
|
515
|
-
"metadata": {}
|
|
481
|
+
"metadata": {}
|
|
516
482
|
}
|
|
517
|
-
|
|
483
|
+
|
|
518
484
|
# Add code-specific fields
|
|
519
485
|
if cell_type == "code":
|
|
520
486
|
new_cell["outputs"] = []
|
|
521
487
|
new_cell["execution_count"] = None
|
|
522
|
-
|
|
488
|
+
|
|
523
489
|
# Insert the cell
|
|
524
490
|
cells.insert(cell_number, new_cell)
|
|
525
|
-
change_description =
|
|
526
|
-
f"Inserted new {cell_type} cell at position {cell_number}"
|
|
527
|
-
)
|
|
491
|
+
change_description = f"Inserted new {cell_type} cell at position {cell_number}"
|
|
528
492
|
|
|
529
493
|
else: # delete
|
|
530
494
|
# Store deleted cell info for reporting
|
|
531
495
|
deleted_cell = cells[cell_number]
|
|
532
496
|
deleted_type = deleted_cell.get("cell_type", "code")
|
|
533
|
-
|
|
497
|
+
|
|
534
498
|
# Remove the cell
|
|
535
499
|
del cells[cell_number]
|
|
536
|
-
change_description =
|
|
537
|
-
f"Deleted {deleted_type} cell at position {cell_number}"
|
|
538
|
-
)
|
|
500
|
+
change_description = f"Deleted {deleted_type} cell at position {cell_number}"
|
|
539
501
|
|
|
540
502
|
# Write the updated notebook back to file
|
|
541
503
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
@@ -545,9 +507,7 @@ class JupyterNotebookTools:
|
|
|
545
507
|
updated_content = json.dumps(notebook, indent=1)
|
|
546
508
|
self.document_context.update_document(path, updated_content)
|
|
547
509
|
|
|
548
|
-
await tool_ctx.info(
|
|
549
|
-
f"Successfully edited notebook: {path} - {change_description}"
|
|
550
|
-
)
|
|
510
|
+
await tool_ctx.info(f"Successfully edited notebook: {path} - {change_description}")
|
|
551
511
|
return f"Successfully edited notebook: {path} - {change_description}"
|
|
552
512
|
except Exception as e:
|
|
553
513
|
await tool_ctx.error(f"Error editing notebook: {str(e)}")
|
|
@@ -0,0 +1,165 @@
|
|
|
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)
|
|
@@ -1 +1,64 @@
|
|
|
1
|
-
"""Project analysis
|
|
1
|
+
"""Project analysis tools package for Hanzo MCP.
|
|
2
|
+
|
|
3
|
+
This package provides tools for analyzing project structure and dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
9
|
+
from hanzo_mcp.tools.common.context import DocumentContext
|
|
10
|
+
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
11
|
+
from hanzo_mcp.tools.project.analysis import ProjectAnalyzer, ProjectManager
|
|
12
|
+
from hanzo_mcp.tools.project.project_analyze import ProjectAnalyzeTool
|
|
13
|
+
from hanzo_mcp.tools.shell.command_executor import CommandExecutor
|
|
14
|
+
|
|
15
|
+
# Export all tool classes
|
|
16
|
+
__all__ = [
|
|
17
|
+
"ProjectAnalyzer",
|
|
18
|
+
"ProjectManager",
|
|
19
|
+
"ProjectAnalyzeTool",
|
|
20
|
+
"get_project_tools",
|
|
21
|
+
"register_project_tools",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_project_tools(
|
|
26
|
+
document_context: DocumentContext,
|
|
27
|
+
permission_manager: PermissionManager,
|
|
28
|
+
command_executor: CommandExecutor,
|
|
29
|
+
) -> list[BaseTool]:
|
|
30
|
+
"""Create instances of all project tools.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
permission_manager: Permission manager for access control
|
|
34
|
+
document_context: Document context for tracking file contents
|
|
35
|
+
command_executor: Command executor for running analysis scripts
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of project tool instances
|
|
39
|
+
"""
|
|
40
|
+
# Initialize project analyzer and manager
|
|
41
|
+
project_analyzer = ProjectAnalyzer(command_executor)
|
|
42
|
+
project_manager = ProjectManager(document_context, permission_manager, project_analyzer)
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
ProjectAnalyzeTool(permission_manager, project_manager, project_analyzer),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def register_project_tools(
|
|
50
|
+
mcp_server: FastMCP,
|
|
51
|
+
permission_manager: PermissionManager,
|
|
52
|
+
document_context: DocumentContext,
|
|
53
|
+
command_executor: CommandExecutor,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Register all project tools with the MCP server.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
mcp_server: The FastMCP server instance
|
|
59
|
+
permission_manager: Permission manager for access control
|
|
60
|
+
document_context: Document context for tracking file contents
|
|
61
|
+
command_executor: Command executor for running analysis scripts
|
|
62
|
+
"""
|
|
63
|
+
tools = get_project_tools(document_context, permission_manager, command_executor)
|
|
64
|
+
ToolRegistry.register_tools(mcp_server, tools)
|