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,136 +1,131 @@
|
|
|
1
1
|
"""Edit notebook tool implementation.
|
|
2
2
|
|
|
3
|
-
This module provides the
|
|
3
|
+
This module provides the NoteBookEditTool for editing Jupyter notebook files.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
from pathlib import Path
|
|
8
|
-
from typing import Any, final, override
|
|
8
|
+
from typing import Annotated, Any, Literal, TypedDict, Unpack, final, override
|
|
9
9
|
|
|
10
|
-
from
|
|
11
|
-
from
|
|
10
|
+
from fastmcp import Context as MCPContext
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
from fastmcp.server.dependencies import get_context
|
|
13
|
+
from pydantic import Field
|
|
12
14
|
|
|
13
15
|
from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
|
|
14
16
|
|
|
17
|
+
NotebookPath = Annotated[
|
|
18
|
+
str,
|
|
19
|
+
Field(
|
|
20
|
+
description="The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
CellNumber = Annotated[
|
|
25
|
+
int,
|
|
26
|
+
Field(
|
|
27
|
+
description="The index of the cell to edit (0-based)",
|
|
28
|
+
ge=0,
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
NewSource = Annotated[
|
|
33
|
+
str,
|
|
34
|
+
Field(
|
|
35
|
+
description="The new source for the cell",
|
|
36
|
+
default="",
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
CellType = Annotated[
|
|
41
|
+
Literal["code", "markdown"],
|
|
42
|
+
Field(
|
|
43
|
+
description="The of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.",
|
|
44
|
+
default="code",
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
EditMode = Annotated[
|
|
49
|
+
Literal["replace", "insert", "delete"],
|
|
50
|
+
Field(
|
|
51
|
+
description="The of edit to make (replace, insert, delete). Defaults to replace.",
|
|
52
|
+
default="replace",
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NotebookEditToolParams(TypedDict):
|
|
58
|
+
"""Parameters for the NotebookEditTool.
|
|
59
|
+
|
|
60
|
+
Attributes:
|
|
61
|
+
notebook_path: The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)
|
|
62
|
+
cell_number: The index of the cell to edit (0-based)
|
|
63
|
+
new_source: The new source for the cell
|
|
64
|
+
cell_type: The of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.
|
|
65
|
+
edit_mode: The of edit to make (replace, insert, delete). Defaults to replace.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
notebook_path: NotebookPath
|
|
69
|
+
cell_number: CellNumber
|
|
70
|
+
new_source: NewSource
|
|
71
|
+
cell_type: CellType
|
|
72
|
+
edit_mode: EditMode
|
|
73
|
+
|
|
15
74
|
|
|
16
75
|
@final
|
|
17
|
-
class
|
|
76
|
+
class NoteBookEditTool(JupyterBaseTool):
|
|
18
77
|
"""Tool for editing Jupyter notebook files."""
|
|
19
|
-
|
|
78
|
+
|
|
20
79
|
@property
|
|
21
|
-
@override
|
|
80
|
+
@override
|
|
22
81
|
def name(self) -> str:
|
|
23
82
|
"""Get the tool name.
|
|
24
|
-
|
|
83
|
+
|
|
25
84
|
Returns:
|
|
26
85
|
Tool name
|
|
27
86
|
"""
|
|
28
|
-
return "
|
|
29
|
-
|
|
87
|
+
return "notebook_edit"
|
|
88
|
+
|
|
30
89
|
@property
|
|
31
|
-
@override
|
|
90
|
+
@override
|
|
32
91
|
def description(self) -> str:
|
|
33
92
|
"""Get the tool description.
|
|
34
|
-
|
|
93
|
+
|
|
35
94
|
Returns:
|
|
36
95
|
Tool description
|
|
37
96
|
"""
|
|
38
|
-
return "
|
|
97
|
+
return "Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number."
|
|
39
98
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
"cell_number": {
|
|
60
|
-
"type": "integer",
|
|
61
|
-
"description": "index of the cell to edit"
|
|
62
|
-
},
|
|
63
|
-
"new_source": {
|
|
64
|
-
"type": "string",
|
|
65
|
-
"description": "new source code or markdown content"
|
|
66
|
-
},
|
|
67
|
-
"cell_type": {
|
|
68
|
-
"anyOf": [
|
|
69
|
-
{"enum": ["code", "markdown"], "type": "string"},
|
|
70
|
-
{"type": "null"}
|
|
71
|
-
],
|
|
72
|
-
"default": None,
|
|
73
|
-
"description": "type of the new cell (code or markdown)"
|
|
74
|
-
},
|
|
75
|
-
"edit_mode": {
|
|
76
|
-
"default": "replace",
|
|
77
|
-
"enum": ["replace", "insert", "delete"],
|
|
78
|
-
"type": "string",
|
|
79
|
-
"description": "edit mode: replace, insert, or delete"
|
|
80
|
-
}
|
|
81
|
-
},
|
|
82
|
-
"required": ["path", "cell_number", "new_source"],
|
|
83
|
-
"title": "edit_notebookArguments",
|
|
84
|
-
"type": "object"
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
@property
|
|
88
|
-
@override
|
|
89
|
-
def required(self) -> list[str]:
|
|
90
|
-
"""Get the list of required parameter names.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
List of required parameter names
|
|
94
|
-
"""
|
|
95
|
-
return ["path", "cell_number", "new_source"]
|
|
96
|
-
|
|
97
|
-
@override
|
|
98
|
-
async def call(self, ctx: MCPContext, **params: Any) -> str:
|
|
99
|
+
@override
|
|
100
|
+
async def call(
|
|
101
|
+
self,
|
|
102
|
+
ctx: MCPContext,
|
|
103
|
+
**params: Unpack[NotebookEditToolParams],
|
|
104
|
+
) -> str:
|
|
99
105
|
"""Execute the tool with the given parameters.
|
|
100
|
-
|
|
106
|
+
|
|
101
107
|
Args:
|
|
102
108
|
ctx: MCP context
|
|
103
109
|
**params: Tool parameters
|
|
104
|
-
|
|
110
|
+
|
|
105
111
|
Returns:
|
|
106
112
|
Tool result
|
|
107
113
|
"""
|
|
108
114
|
tool_ctx = self.create_tool_context(ctx)
|
|
109
115
|
self.set_tool_context_info(tool_ctx)
|
|
110
|
-
|
|
116
|
+
|
|
111
117
|
# Extract parameters
|
|
112
|
-
|
|
118
|
+
notebook_path = params.get("notebook_path")
|
|
113
119
|
cell_number = params.get("cell_number")
|
|
114
120
|
new_source = params.get("new_source")
|
|
115
121
|
cell_type = params.get("cell_type")
|
|
116
122
|
edit_mode = params.get("edit_mode", "replace")
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if path is None:
|
|
120
|
-
await tool_ctx.error("Path parameter is required")
|
|
121
|
-
return "Error: Path parameter is required"
|
|
122
|
-
|
|
123
|
-
path_str = str(path)
|
|
124
|
-
path_validation = self.validate_path(path_str)
|
|
123
|
+
|
|
124
|
+
path_validation = self.validate_path(notebook_path)
|
|
125
125
|
if path_validation.is_error:
|
|
126
126
|
await tool_ctx.error(path_validation.error_message)
|
|
127
127
|
return f"Error: {path_validation.error_message}"
|
|
128
128
|
|
|
129
|
-
# Validate cell_number
|
|
130
|
-
if cell_number is None or cell_number < 0:
|
|
131
|
-
await tool_ctx.error("Cell number must be non-negative")
|
|
132
|
-
return "Error: Cell number must be non-negative"
|
|
133
|
-
|
|
134
129
|
# Validate edit_mode
|
|
135
130
|
if edit_mode not in ["replace", "insert", "delete"]:
|
|
136
131
|
await tool_ctx.error("Edit mode must be replace, insert, or delete")
|
|
@@ -143,33 +138,37 @@ In delete mode, the specified cell is removed."""
|
|
|
143
138
|
|
|
144
139
|
# Don't validate new_source for delete mode
|
|
145
140
|
if edit_mode != "delete" and not new_source:
|
|
146
|
-
await tool_ctx.error(
|
|
141
|
+
await tool_ctx.error(
|
|
142
|
+
"New source is required for replace or insert operations"
|
|
143
|
+
)
|
|
147
144
|
return "Error: New source is required for replace or insert operations"
|
|
148
145
|
|
|
149
|
-
await tool_ctx.info(
|
|
146
|
+
await tool_ctx.info(
|
|
147
|
+
f"Editing notebook: {notebook_path} (cell: {cell_number}, mode: {edit_mode})"
|
|
148
|
+
)
|
|
150
149
|
|
|
151
150
|
# Check if path is allowed
|
|
152
|
-
if not self.is_path_allowed(
|
|
151
|
+
if not self.is_path_allowed(notebook_path):
|
|
153
152
|
await tool_ctx.error(
|
|
154
|
-
f"Access denied - path outside allowed directories: {
|
|
153
|
+
f"Access denied - path outside allowed directories: {notebook_path}"
|
|
155
154
|
)
|
|
156
|
-
return f"Error: Access denied - path outside allowed directories: {
|
|
155
|
+
return f"Error: Access denied - path outside allowed directories: {notebook_path}"
|
|
157
156
|
|
|
158
157
|
try:
|
|
159
|
-
file_path = Path(
|
|
158
|
+
file_path = Path(notebook_path)
|
|
160
159
|
|
|
161
160
|
if not file_path.exists():
|
|
162
|
-
await tool_ctx.error(f"File does not exist: {
|
|
163
|
-
return f"Error: File does not exist: {
|
|
161
|
+
await tool_ctx.error(f"File does not exist: {notebook_path}")
|
|
162
|
+
return f"Error: File does not exist: {notebook_path}"
|
|
164
163
|
|
|
165
164
|
if not file_path.is_file():
|
|
166
|
-
await tool_ctx.error(f"Path is not a file: {
|
|
167
|
-
return f"Error: Path is not a file: {
|
|
165
|
+
await tool_ctx.error(f"Path is not a file: {notebook_path}")
|
|
166
|
+
return f"Error: Path is not a file: {notebook_path}"
|
|
168
167
|
|
|
169
168
|
# Check file extension
|
|
170
169
|
if file_path.suffix.lower() != ".ipynb":
|
|
171
|
-
await tool_ctx.error(f"File is not a Jupyter notebook: {
|
|
172
|
-
return f"Error: File is not a Jupyter notebook: {
|
|
170
|
+
await tool_ctx.error(f"File is not a Jupyter notebook: {notebook_path}")
|
|
171
|
+
return f"Error: File is not a Jupyter notebook: {notebook_path}"
|
|
173
172
|
|
|
174
173
|
# Read and parse the notebook
|
|
175
174
|
try:
|
|
@@ -177,54 +176,62 @@ In delete mode, the specified cell is removed."""
|
|
|
177
176
|
content = f.read()
|
|
178
177
|
notebook = json.loads(content)
|
|
179
178
|
except json.JSONDecodeError:
|
|
180
|
-
await tool_ctx.error(f"Invalid notebook format: {
|
|
181
|
-
return f"Error: Invalid notebook format: {
|
|
179
|
+
await tool_ctx.error(f"Invalid notebook format: {notebook_path}")
|
|
180
|
+
return f"Error: Invalid notebook format: {notebook_path}"
|
|
182
181
|
except UnicodeDecodeError:
|
|
183
|
-
await tool_ctx.error(f"Cannot read notebook file: {
|
|
184
|
-
return f"Error: Cannot read notebook file: {
|
|
182
|
+
await tool_ctx.error(f"Cannot read notebook file: {notebook_path}")
|
|
183
|
+
return f"Error: Cannot read notebook file: {notebook_path}"
|
|
185
184
|
|
|
186
185
|
# Check cell_number is valid
|
|
187
186
|
cells = notebook.get("cells", [])
|
|
188
|
-
|
|
187
|
+
|
|
189
188
|
if edit_mode == "insert":
|
|
190
189
|
if cell_number > len(cells):
|
|
191
|
-
await tool_ctx.error(
|
|
190
|
+
await tool_ctx.error(
|
|
191
|
+
f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
|
|
192
|
+
)
|
|
192
193
|
return f"Error: Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
|
|
193
194
|
else: # replace or delete
|
|
194
195
|
if cell_number >= len(cells):
|
|
195
|
-
await tool_ctx.error(
|
|
196
|
+
await tool_ctx.error(
|
|
197
|
+
f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
|
|
198
|
+
)
|
|
196
199
|
return f"Error: Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
|
|
197
200
|
|
|
198
201
|
# Get notebook language (needed for context but not directly used in this block)
|
|
199
|
-
_ =
|
|
202
|
+
_ = (
|
|
203
|
+
notebook.get("metadata", {})
|
|
204
|
+
.get("language_info", {})
|
|
205
|
+
.get("name", "python")
|
|
206
|
+
)
|
|
200
207
|
|
|
201
208
|
# Perform the requested operation
|
|
202
209
|
if edit_mode == "replace":
|
|
203
210
|
# Get the target cell
|
|
204
211
|
target_cell = cells[cell_number]
|
|
205
|
-
|
|
212
|
+
|
|
206
213
|
# Store previous contents for reporting
|
|
207
214
|
old_type = target_cell.get("cell_type", "code")
|
|
208
215
|
old_source = target_cell.get("source", "")
|
|
209
|
-
|
|
216
|
+
|
|
210
217
|
# Fix for old_source which might be a list of strings
|
|
211
218
|
if isinstance(old_source, list):
|
|
212
219
|
old_source = "".join([str(item) for item in old_source])
|
|
213
|
-
|
|
220
|
+
|
|
214
221
|
# Update source
|
|
215
222
|
target_cell["source"] = new_source
|
|
216
|
-
|
|
223
|
+
|
|
217
224
|
# Update type if specified
|
|
218
225
|
if cell_type is not None:
|
|
219
226
|
target_cell["cell_type"] = cell_type
|
|
220
|
-
|
|
227
|
+
|
|
221
228
|
# If changing to markdown, remove code-specific fields
|
|
222
229
|
if cell_type == "markdown":
|
|
223
230
|
if "outputs" in target_cell:
|
|
224
231
|
del target_cell["outputs"]
|
|
225
232
|
if "execution_count" in target_cell:
|
|
226
233
|
del target_cell["execution_count"]
|
|
227
|
-
|
|
234
|
+
|
|
228
235
|
# If code cell, reset execution
|
|
229
236
|
if target_cell["cell_type"] == "code":
|
|
230
237
|
target_cell["outputs"] = []
|
|
@@ -232,64 +239,80 @@ In delete mode, the specified cell is removed."""
|
|
|
232
239
|
|
|
233
240
|
change_description = f"Replaced cell {cell_number}"
|
|
234
241
|
if cell_type is not None and cell_type != old_type:
|
|
235
|
-
change_description +=
|
|
242
|
+
change_description += (
|
|
243
|
+
f" (changed type from {old_type} to {cell_type})"
|
|
244
|
+
)
|
|
236
245
|
|
|
237
246
|
elif edit_mode == "insert":
|
|
238
247
|
# Create new cell
|
|
239
248
|
new_cell: dict[str, Any] = {
|
|
240
249
|
"cell_type": cell_type,
|
|
241
250
|
"source": new_source,
|
|
242
|
-
"metadata": {}
|
|
251
|
+
"metadata": {},
|
|
243
252
|
}
|
|
244
|
-
|
|
253
|
+
|
|
245
254
|
# Add code-specific fields
|
|
246
255
|
if cell_type == "code":
|
|
247
256
|
new_cell["outputs"] = []
|
|
248
257
|
new_cell["execution_count"] = None
|
|
249
|
-
|
|
258
|
+
|
|
250
259
|
# Insert the cell
|
|
251
260
|
cells.insert(cell_number, new_cell)
|
|
252
|
-
change_description =
|
|
261
|
+
change_description = (
|
|
262
|
+
f"Inserted new {cell_type} cell at position {cell_number}"
|
|
263
|
+
)
|
|
253
264
|
|
|
254
265
|
else: # delete
|
|
255
266
|
# Store deleted cell info for reporting
|
|
256
267
|
deleted_cell = cells[cell_number]
|
|
257
268
|
deleted_type = deleted_cell.get("cell_type", "code")
|
|
258
|
-
|
|
269
|
+
|
|
259
270
|
# Remove the cell
|
|
260
271
|
del cells[cell_number]
|
|
261
|
-
change_description =
|
|
272
|
+
change_description = (
|
|
273
|
+
f"Deleted {deleted_type} cell at position {cell_number}"
|
|
274
|
+
)
|
|
262
275
|
|
|
263
276
|
# Write the updated notebook back to file
|
|
264
277
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
265
278
|
json.dump(notebook, f, indent=1)
|
|
266
279
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
await tool_ctx.info(
|
|
281
|
+
f"Successfully edited notebook: {notebook_path} - {change_description}"
|
|
282
|
+
)
|
|
283
|
+
return (
|
|
284
|
+
f"Successfully edited notebook: {notebook_path} - {change_description}"
|
|
285
|
+
)
|
|
273
286
|
except Exception as e:
|
|
274
287
|
await tool_ctx.error(f"Error editing notebook: {str(e)}")
|
|
275
288
|
return f"Error editing notebook: {str(e)}"
|
|
276
|
-
|
|
289
|
+
|
|
277
290
|
@override
|
|
278
291
|
def register(self, mcp_server: FastMCP) -> None:
|
|
279
292
|
"""Register this edit notebook tool with the MCP server.
|
|
280
|
-
|
|
293
|
+
|
|
281
294
|
Creates a wrapper function with explicitly defined parameters that match
|
|
282
295
|
the tool's parameter schema and registers it with the MCP server.
|
|
283
|
-
|
|
296
|
+
|
|
284
297
|
Args:
|
|
285
298
|
mcp_server: The FastMCP server instance
|
|
286
299
|
"""
|
|
287
300
|
tool_self = self # Create a reference to self for use in the closure
|
|
288
|
-
|
|
289
|
-
@mcp_server.tool(name=self.name, description=self.
|
|
290
|
-
async def
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
|
|
302
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
303
|
+
async def notebook_edit(
|
|
304
|
+
notebook_path: NotebookPath,
|
|
305
|
+
cell_number: CellNumber,
|
|
306
|
+
new_source: NewSource,
|
|
307
|
+
cell_type: CellType,
|
|
308
|
+
edit_mode: EditMode,
|
|
309
|
+
) -> str:
|
|
310
|
+
ctx = get_context()
|
|
311
|
+
return await tool_self.call(
|
|
312
|
+
ctx,
|
|
313
|
+
notebook_path=notebook_path,
|
|
314
|
+
cell_number=cell_number,
|
|
315
|
+
new_source=new_source,
|
|
316
|
+
cell_type=cell_type,
|
|
317
|
+
edit_mode=edit_mode,
|
|
318
|
+
)
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Read notebook tool implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the NotebookReadTool for reading Jupyter notebook files.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, TypedDict, Unpack, final, override
|
|
9
|
+
|
|
10
|
+
from fastmcp import Context as MCPContext
|
|
11
|
+
from fastmcp import FastMCP
|
|
12
|
+
from fastmcp.server.dependencies import get_context
|
|
13
|
+
from pydantic import Field
|
|
14
|
+
|
|
15
|
+
from hanzo_mcp.tools.jupyter.base import JupyterBaseTool
|
|
16
|
+
|
|
17
|
+
NotebookPath = Annotated[
|
|
18
|
+
str,
|
|
19
|
+
Field(
|
|
20
|
+
description="The absolute path to the Jupyter notebook file to read (must be absolute, not relative)",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class NotebookReadToolParams(TypedDict):
|
|
26
|
+
"""Parameters for the NotebookReadTool.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
notebook_path: The absolute path to the Jupyter notebook file to read (must be absolute, not relative)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
notebook_path: NotebookPath
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class NotebookReadTool(JupyterBaseTool):
|
|
37
|
+
"""Tool for reading Jupyter notebook files."""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@override
|
|
41
|
+
def name(self) -> str:
|
|
42
|
+
"""Get the tool name.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tool name
|
|
46
|
+
"""
|
|
47
|
+
return "notebook_read"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@override
|
|
51
|
+
def description(self) -> str:
|
|
52
|
+
"""Get the tool description.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Tool description
|
|
56
|
+
"""
|
|
57
|
+
return "Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path."
|
|
58
|
+
|
|
59
|
+
@override
|
|
60
|
+
async def call(
|
|
61
|
+
self,
|
|
62
|
+
ctx: MCPContext,
|
|
63
|
+
**params: Unpack[NotebookReadToolParams],
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Execute the tool with the given parameters.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
ctx: MCP context
|
|
69
|
+
**params: Tool parameters
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Tool result
|
|
73
|
+
"""
|
|
74
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
75
|
+
self.set_tool_context_info(tool_ctx)
|
|
76
|
+
|
|
77
|
+
# Extract parameters
|
|
78
|
+
notebook_path: NotebookPath = params["notebook_path"]
|
|
79
|
+
|
|
80
|
+
# Validate path parameter
|
|
81
|
+
path_validation = self.validate_path(notebook_path)
|
|
82
|
+
if path_validation.is_error:
|
|
83
|
+
await tool_ctx.error(path_validation.error_message)
|
|
84
|
+
return f"Error: {path_validation.error_message}"
|
|
85
|
+
|
|
86
|
+
await tool_ctx.info(f"Reading notebook: {notebook_path}")
|
|
87
|
+
|
|
88
|
+
# Check if path is allowed
|
|
89
|
+
if not self.is_path_allowed(notebook_path):
|
|
90
|
+
await tool_ctx.error(
|
|
91
|
+
f"Access denied - path outside allowed directories: {notebook_path}"
|
|
92
|
+
)
|
|
93
|
+
return f"Error: Access denied - path outside allowed directories: {notebook_path}"
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
file_path = Path(notebook_path)
|
|
97
|
+
|
|
98
|
+
if not file_path.exists():
|
|
99
|
+
await tool_ctx.error(f"File does not exist: {notebook_path}")
|
|
100
|
+
return f"Error: File does not exist: {notebook_path}"
|
|
101
|
+
|
|
102
|
+
if not file_path.is_file():
|
|
103
|
+
await tool_ctx.error(f"Path is not a file: {notebook_path}")
|
|
104
|
+
return f"Error: Path is not a file: {notebook_path}"
|
|
105
|
+
|
|
106
|
+
# Check file extension
|
|
107
|
+
if file_path.suffix.lower() != ".ipynb":
|
|
108
|
+
await tool_ctx.error(f"File is not a Jupyter notebook: {notebook_path}")
|
|
109
|
+
return f"Error: File is not a Jupyter notebook: {notebook_path}"
|
|
110
|
+
|
|
111
|
+
# Read and parse the notebook
|
|
112
|
+
try:
|
|
113
|
+
# This will read the file, so we don't need to read it separately
|
|
114
|
+
_, processed_cells = await self.parse_notebook(file_path)
|
|
115
|
+
|
|
116
|
+
# Format the notebook content as a readable string
|
|
117
|
+
result = self.format_notebook_cells(processed_cells)
|
|
118
|
+
|
|
119
|
+
await tool_ctx.info(
|
|
120
|
+
f"Successfully read notebook: {notebook_path} ({len(processed_cells)} cells)"
|
|
121
|
+
)
|
|
122
|
+
return result
|
|
123
|
+
except json.JSONDecodeError:
|
|
124
|
+
await tool_ctx.error(f"Invalid notebook format: {notebook_path}")
|
|
125
|
+
return f"Error: Invalid notebook format: {notebook_path}"
|
|
126
|
+
except UnicodeDecodeError:
|
|
127
|
+
await tool_ctx.error(f"Cannot read notebook file: {notebook_path}")
|
|
128
|
+
return f"Error: Cannot read notebook file: {notebook_path}"
|
|
129
|
+
except Exception as e:
|
|
130
|
+
await tool_ctx.error(f"Error reading notebook: {str(e)}")
|
|
131
|
+
return f"Error reading notebook: {str(e)}"
|
|
132
|
+
|
|
133
|
+
@override
|
|
134
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
135
|
+
"""Register this read notebook tool with the MCP server.
|
|
136
|
+
|
|
137
|
+
Creates a wrapper function with explicitly defined parameters that match
|
|
138
|
+
the tool's parameter schema and registers it with the MCP server.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
mcp_server: The FastMCP server instance
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
tool_self = self # Create a reference to self for use in the closure
|
|
145
|
+
|
|
146
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
147
|
+
async def notebook_read(
|
|
148
|
+
ctx: MCPContext,
|
|
149
|
+
notebook_path: NotebookPath,
|
|
150
|
+
) -> str:
|
|
151
|
+
ctx = get_context()
|
|
152
|
+
return await tool_self.call(ctx, notebook_path=notebook_path)
|