hanzo-mcp 0.3.4__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.

Files changed (87) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +123 -160
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +388 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +120 -98
  14. hanzo_mcp/tools/__init__.py +107 -31
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/context.py +26 -292
  23. hanzo_mcp/tools/common/permissions.py +12 -12
  24. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  25. hanzo_mcp/tools/common/validation.py +1 -63
  26. hanzo_mcp/tools/filesystem/__init__.py +88 -41
  27. hanzo_mcp/tools/filesystem/base.py +32 -24
  28. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  29. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  30. hanzo_mcp/tools/filesystem/edit.py +279 -0
  31. hanzo_mcp/tools/filesystem/grep.py +458 -0
  32. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  33. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  34. hanzo_mcp/tools/filesystem/read.py +255 -0
  35. hanzo_mcp/tools/filesystem/write.py +156 -0
  36. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  37. hanzo_mcp/tools/jupyter/base.py +66 -57
  38. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  39. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  40. hanzo_mcp/tools/shell/__init__.py +29 -20
  41. hanzo_mcp/tools/shell/base.py +87 -45
  42. hanzo_mcp/tools/shell/bash_session.py +731 -0
  43. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  44. hanzo_mcp/tools/shell/command_executor.py +435 -384
  45. hanzo_mcp/tools/shell/run_command.py +284 -131
  46. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  47. hanzo_mcp/tools/shell/session_manager.py +196 -0
  48. hanzo_mcp/tools/shell/session_storage.py +325 -0
  49. hanzo_mcp/tools/todo/__init__.py +66 -0
  50. hanzo_mcp/tools/todo/base.py +319 -0
  51. hanzo_mcp/tools/todo/todo_read.py +148 -0
  52. hanzo_mcp/tools/todo/todo_write.py +378 -0
  53. hanzo_mcp/tools/vector/__init__.py +95 -0
  54. hanzo_mcp/tools/vector/infinity_store.py +365 -0
  55. hanzo_mcp/tools/vector/project_manager.py +361 -0
  56. hanzo_mcp/tools/vector/vector_index.py +115 -0
  57. hanzo_mcp/tools/vector/vector_search.py +215 -0
  58. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +35 -3
  59. hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
  60. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
  61. hanzo_mcp/tools/agent/base_provider.py +0 -73
  62. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  63. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  64. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  65. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  66. hanzo_mcp/tools/common/error_handling.py +0 -86
  67. hanzo_mcp/tools/common/logging_config.py +0 -115
  68. hanzo_mcp/tools/common/session.py +0 -91
  69. hanzo_mcp/tools/common/think_tool.py +0 -123
  70. hanzo_mcp/tools/common/version_tool.py +0 -120
  71. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  72. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  73. hanzo_mcp/tools/filesystem/read_files.py +0 -198
  74. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  75. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  76. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  77. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  78. hanzo_mcp/tools/project/__init__.py +0 -64
  79. hanzo_mcp/tools/project/analysis.py +0 -882
  80. hanzo_mcp/tools/project/base.py +0 -66
  81. hanzo_mcp/tools/project/project_analyze.py +0 -173
  82. hanzo_mcp/tools/shell/run_script.py +0 -215
  83. hanzo_mcp/tools/shell/script_tool.py +0 -244
  84. hanzo_mcp-0.3.4.dist-info/RECORD +0 -53
  85. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
  86. {hanzo_mcp-0.3.4.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
  87. {hanzo_mcp-0.3.4.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)