Skip to content

Commit 66619b0

Browse files
Enhance: delete cell (#150)
* enhance delete cell * fix the missing index
1 parent 2358010 commit 66619b0

File tree

6 files changed

+62
-58
lines changed

6 files changed

+62
-58
lines changed

docs/docs/tools/index.mdx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,12 @@ The server currently offers 14 tools organized into 3 categories:
135135

136136
#### 13. `delete_cell`
137137

138-
- Delete a specific cell from the currently activated notebook and return the cell source of deleted cell.
138+
- Delete a specific cell or multiple cells from the currently activated notebook and return the cell source of deleted cells (if include_source=True).
139139
- When deleting many cells, MUST delete them in descending order of their index to avoid index shifting.
140140
- Input:
141-
- `cell_index`(int): Index of the cell to delete (0-based)
142-
- Returns: Success message with deletion confirmation and the source code of the deleted cell
141+
- `cell_indices`(list[int]): List of indices of the cells to delete (0-based)
142+
- `include_source`(bool, optional): Whether to include the source of deleted cells (default: true)
143+
- Returns: Success message with deletion confirmation and the source code of the deleted cells (if include_source=True)
143144

144145
#### 14. `execute_code`
145146

jupyter_mcp_server/server.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -462,18 +462,19 @@ async def read_cell(
462462

463463
@mcp.tool()
464464
async def delete_cell(
465-
cell_index: Annotated[int, Field(description="Index of the cell to delete (0-based)", ge=0)],
466-
) -> Annotated[str, Field(description="Success message and the cell source of deleted cell")]:
467-
"""Delete a specific cell from the currently activated notebook and return the cell source of deleted cell.
468-
When deleting many cells, MUST delete them in descending order of their index to avoid index shifting."""
465+
cell_indices: Annotated[list[int], Field(description="List of cell indices to delete (0-based)",min_items=1)],
466+
include_source: Annotated[bool, Field(description="Whether to include the source of deleted cells")] = True,
467+
) -> Annotated[str, Field(description="Success message with list of deleted cells and their source (if include_source=True)")]:
468+
"""Delete specific cells from the currently activated notebook and return the cell source of deleted cells (if include_source=True)."""
469469
return await safe_notebook_operation(
470470
lambda: DeleteCellTool().execute(
471471
mode=server_context.mode,
472472
server_client=server_context.server_client,
473473
contents_manager=server_context.contents_manager,
474474
kernel_manager=server_context.kernel_manager,
475475
notebook_manager=notebook_manager,
476-
cell_index=cell_index,
476+
cell_indices=cell_indices,
477+
include_source=include_source,
477478
)
478479
)
479480

jupyter_mcp_server/tools/delete_cell_tool.py

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
class DeleteCellTool(BaseTool):
17-
"""Tool to delete a specific cell from a notebook."""
17+
"""Tool to delete specific cells from a notebook."""
1818

1919
def _get_cell_source(self, cell: Any) -> str:
2020
"""Get the cell source from the cell"""
@@ -28,102 +28,98 @@ async def _delete_cell_ydoc(
2828
self,
2929
serverapp: Any,
3030
notebook_path: str,
31-
cell_index: int
32-
) -> dict:
31+
cell_indices: list[int]
32+
) -> list:
3333
"""Delete cell using YDoc (collaborative editing mode).
3434
3535
Args:
3636
serverapp: Jupyter ServerApp instance
3737
notebook_path: Path to the notebook
38-
cell_index: Index of cell to delete
38+
cell_indices: List of indices of cells to delete
3939
4040
Returns:
4141
NotebookNode
4242
"""
4343
nb = await get_notebook_model(serverapp, notebook_path)
4444
if nb:
45-
if cell_index >= len(nb):
45+
if max(cell_indices) >= len(nb):
4646
raise ValueError(
47-
f"Cell index {cell_index} is out of range. Notebook has {len(nb)} cells."
47+
f"Cell index {max(cell_indices)} is out of range. Notebook has {len(nb)} cells."
4848
)
4949

50-
cell = nb.delete_cell(cell_index)
51-
return {
52-
"index": cell_index,
53-
"cell_type": cell.cell_type,
54-
"source": self._get_cell_source(cell),
55-
}
50+
cells = nb.delete_many_cells(cell_indices)
51+
return cells
5652
else:
5753
# YDoc not available, use file operations
58-
return await self._delete_cell_file(notebook_path, cell_index)
54+
return await self._delete_cell_file(notebook_path, cell_indices)
5955

6056
async def _delete_cell_file(
6157
self,
6258
notebook_path: str,
63-
cell_index: int
64-
) -> dict:
59+
cell_indices: list[int]
60+
) -> list:
6561
"""Delete cell using file operations (non-collaborative mode).
6662
6763
Args:
6864
notebook_path: Absolute path to the notebook
69-
cell_index: Index of cell to delete
65+
cell_indices: List of indices of cells to delete
7066
7167
Returns:
72-
Success message
68+
List of deleted cells
7369
"""
7470
# Read notebook file as version 4 for consistency
7571
with open(notebook_path, "r", encoding="utf-8") as f:
7672
notebook = nbformat.read(f, as_version=4)
7773

7874
clean_notebook_outputs(notebook)
7975

80-
if cell_index >= len(notebook.cells):
76+
if max(cell_indices) >= len(notebook.cells):
8177
raise ValueError(
82-
f"Cell index {cell_index} is out of range. Notebook has {len(notebook.cells)} cells."
78+
f"Cell index {max(cell_indices)} is out of range. Notebook has {len(notebook.cells)} cells."
8379
)
8480

85-
cell = notebook.cells[cell_index]
86-
result = {
87-
"index": cell_index,
88-
"cell_type": cell.cell_type,
89-
"source": self._get_cell_source(cell),
90-
}
81+
deleted_cells = []
82+
for cell_index in cell_indices:
83+
cell = notebook.cells[cell_index]
84+
result = {
85+
"index": cell_index,
86+
"cell_type": cell.cell_type,
87+
"source": self._get_cell_source(cell),
88+
}
89+
deleted_cells.append(result)
9190

9291
# Delete the cell
93-
notebook.cells.pop(cell_index)
92+
for cell_index in sorted(cell_indices, reverse=True):
93+
notebook.cells.pop(cell_index)
9494

9595
# Write back to file
9696
with open(notebook_path, "w", encoding="utf-8") as f:
9797
nbformat.write(notebook, f)
9898

99-
return result
99+
return deleted_cells
100100

101101
async def _delete_cell_websocket(
102102
self,
103103
notebook_manager: NotebookManager,
104-
cell_index: int
105-
) -> dict:
104+
cell_indices: list[int]
105+
) -> list:
106106
"""Delete cell using WebSocket connection (MCP_SERVER mode).
107107
108108
Args:
109109
notebook_manager: Notebook manager instance
110-
cell_index: Index of cell to delete
110+
cell_indices: List of indices of cells to delete
111111
112112
Returns:
113-
Success message
113+
List of deleted cell information
114114
"""
115115
async with notebook_manager.get_current_connection() as notebook:
116-
if cell_index >= len(notebook):
116+
if max(cell_indices) >= len(notebook):
117117
raise ValueError(
118-
f"Cell index {cell_index} is out of range. Notebook has {len(notebook)} cells."
118+
f"Cell index {max(cell_indices)} is out of range. Notebook has {len(notebook)} cells."
119119
)
120120

121-
cell = notebook.delete_cell(cell_index)
122-
return {
123-
"index": cell_index,
124-
"cell_type": cell.cell_type,
125-
"source": self._get_cell_source(cell),
126-
}
121+
cells = notebook.delete_many_cells(cell_indices)
122+
return cells
127123

128124
async def execute(
129125
self,
@@ -135,7 +131,8 @@ async def execute(
135131
kernel_spec_manager: Optional[Any] = None,
136132
notebook_manager: Optional[NotebookManager] = None,
137133
# Tool-specific parameters
138-
cell_index: int = None,
134+
cell_indices: list[int] = None,
135+
include_source: bool = True,
139136
**kwargs
140137
) -> str:
141138
"""Execute the delete_cell tool.
@@ -181,17 +178,22 @@ async def execute(
181178

182179
if serverapp:
183180
# Try YDoc approach first
184-
cell_info = await self._delete_cell_ydoc(serverapp, notebook_path, cell_index)
181+
cells = await self._delete_cell_ydoc(serverapp, notebook_path, cell_indices)
185182
else:
186183
# Fall back to file operations
187-
cell_info = await self._delete_cell_file(notebook_path, cell_index)
184+
cells = await self._delete_cell_file(notebook_path, cell_indices)
188185

189186
elif mode == ServerMode.MCP_SERVER and notebook_manager is not None:
190187
# MCP_SERVER mode: Use WebSocket connection
191-
cell_info = await self._delete_cell_websocket(notebook_manager, cell_index)
188+
cells = await self._delete_cell_websocket(notebook_manager, cell_indices)
192189
else:
193190
raise ValueError(f"Invalid mode or missing required clients: mode={mode}")
194191

195-
info_list = [f"Cell {cell_info['index']} ({cell_info['cell_type']}) deleted successfully."]
196-
info_list.append(f"deleted cell source:\n{cell_info['source']}")
192+
info_list = []
193+
for cell_index, cell_info in zip(cell_indices, cells):
194+
info_list.append(f"Cell {cell_index} ({cell_info['cell_type']}) deleted successfully.")
195+
if include_source:
196+
info_list.append(f"deleted cell source:\n{cell_info['source']}")
197+
info_list.append("\n---\n")
198+
197199
return "\n".join(info_list)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323
dependencies = [
2424
"jupyter-kernel-client>=0.7.3",
2525
"jupyter-mcp-tools>=0.1.4",
26-
"jupyter-nbmodel-client>=0.14.2",
26+
"jupyter-nbmodel-client>=0.14.4",
2727
"jupyter-server-nbmodel",
2828
"jupyter-server-client",
2929
"jupyter_server>=1.6,<3",

tests/test_common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -336,8 +336,8 @@ async def list_kernels(self):
336336
return self._extract_text_content(result)
337337

338338
@requires_session
339-
async def delete_cell(self, cell_index):
340-
result = await self._call_tool_safe("delete_cell", {"cell_index": cell_index})
339+
async def delete_cell(self, cell_indices: list[int], include_source: bool = True):
340+
result = await self._call_tool_safe("delete_cell", {"cell_indices": cell_indices, "include_source": include_source})
341341
return self._get_structured_content_safe(result) if result else None
342342

343343
@requires_session

tests/test_tools.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ async def check_and_delete_cell(client: MCPClient, index, expected_type, content
8888
assert f"=====Cell {index} | type: {expected_type}" in cell_info['result'][0], "Cell metadata should be included"
8989
assert content in cell_info['result'][1], "Cell source should be included"
9090
# delete created cell
91-
result = await client.delete_cell(index)
91+
result = await client.delete_cell([index])
9292
assert result is not None, "delete_cell result should not be None"
9393
assert f"Cell {index} ({expected_type}) deleted successfully" in result["result"]
9494
assert f"deleted cell source:\n{content}" in result["result"]
@@ -158,7 +158,7 @@ async def test_multimodal_output(mcp_client_parametrized: MCPClient):
158158
assert isinstance(result['result'], list), "Result should be a list"
159159
assert isinstance(result['result'][0], dict)
160160
assert result['result'][0]['mimeType'] == "image/png", "Result should be a list of ImageContent"
161-
await mcp_client_parametrized.delete_cell(1)
161+
await mcp_client_parametrized.delete_cell([1])
162162

163163

164164
###############################################################################

0 commit comments

Comments
 (0)