Skip to main content
Glama
SDGLBL
by SDGLBL

notebook_edit

Modify Jupyter notebook cells by replacing, inserting, or deleting content to update code and documentation in data analysis workflows.

Instructions

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.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
notebook_pathYesThe absolute path to the Jupyter notebook file to edit (must be absolute, not relative)
cell_numberYesThe index of the cell to edit (0-based)
new_sourceNoThe new source for the cell
cell_typeNoThe of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.code
edit_modeNoThe of edit to make (replace, insert, delete). Defaults to replace.replace

Implementation Reference

  • The main handler function that implements the notebook editing logic: validates inputs, parses .ipynb JSON, modifies the cells list based on edit_mode (replace, insert, delete), writes back the updated notebook, and returns success/error message.
    async def call(
        self,
        ctx: MCPContext,
        **params: Unpack[NotebookEditToolParams],
    ) -> str:
        """Execute the tool with the given parameters.
    
        Args:
            ctx: MCP context
            **params: Tool parameters
    
        Returns:
            Tool result
        """
        tool_ctx = self.create_tool_context(ctx)
        self.set_tool_context_info(tool_ctx)
    
        # Extract parameters
        notebook_path = params.get("notebook_path")
        cell_number = params.get("cell_number")
        new_source = params.get("new_source")
        cell_type = params.get("cell_type")
        edit_mode = params.get("edit_mode", "replace")
    
        path_validation = self.validate_path(notebook_path)
        if path_validation.is_error:
            await tool_ctx.error(path_validation.error_message)
            return f"Error: {path_validation.error_message}"
    
        # Validate edit_mode
        if edit_mode not in ["replace", "insert", "delete"]:
            await tool_ctx.error("Edit mode must be replace, insert, or delete")
            return "Error: Edit mode must be replace, insert, or delete"
    
        # In insert mode, cell_type is required
        if edit_mode == "insert" and cell_type is None:
            await tool_ctx.error("Cell type is required when using insert mode")
            return "Error: Cell type is required when using insert mode"
    
        # Don't validate new_source for delete mode
        if edit_mode != "delete" and not new_source:
            await tool_ctx.error(
                "New source is required for replace or insert operations"
            )
            return "Error: New source is required for replace or insert operations"
    
        await tool_ctx.info(
            f"Editing notebook: {notebook_path} (cell: {cell_number}, mode: {edit_mode})"
        )
    
        # Check if path is allowed
        if not self.is_path_allowed(notebook_path):
            await tool_ctx.error(
                f"Access denied - path outside allowed directories: {notebook_path}"
            )
            return f"Error: Access denied - path outside allowed directories: {notebook_path}"
    
        try:
            file_path = Path(notebook_path)
    
            if not file_path.exists():
                await tool_ctx.error(f"File does not exist: {notebook_path}")
                return f"Error: File does not exist: {notebook_path}"
    
            if not file_path.is_file():
                await tool_ctx.error(f"Path is not a file: {notebook_path}")
                return f"Error: Path is not a file: {notebook_path}"
    
            # Check file extension
            if file_path.suffix.lower() != ".ipynb":
                await tool_ctx.error(f"File is not a Jupyter notebook: {notebook_path}")
                return f"Error: File is not a Jupyter notebook: {notebook_path}"
    
            # Read and parse the notebook
            try:
                with open(file_path, "r", encoding="utf-8") as f:
                    content = f.read()
                    notebook = json.loads(content)
            except json.JSONDecodeError:
                await tool_ctx.error(f"Invalid notebook format: {notebook_path}")
                return f"Error: Invalid notebook format: {notebook_path}"
            except UnicodeDecodeError:
                await tool_ctx.error(f"Cannot read notebook file: {notebook_path}")
                return f"Error: Cannot read notebook file: {notebook_path}"
    
            # Check cell_number is valid
            cells = notebook.get("cells", [])
    
            if edit_mode == "insert":
                if cell_number > len(cells):
                    await tool_ctx.error(
                        f"Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
                    )
                    return f"Error: Cell number {cell_number} is out of bounds for insert (max: {len(cells)})"
            else:  # replace or delete
                if cell_number >= len(cells):
                    await tool_ctx.error(
                        f"Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
                    )
                    return f"Error: Cell number {cell_number} is out of bounds (max: {len(cells) - 1})"
    
            # Get notebook language (needed for context but not directly used in this block)
            _ = (
                notebook.get("metadata", {})
                .get("language_info", {})
                .get("name", "python")
            )
    
            # Perform the requested operation
            if edit_mode == "replace":
                # Get the target cell
                target_cell = cells[cell_number]
    
                # Store previous contents for reporting
                old_type = target_cell.get("cell_type", "code")
                old_source = target_cell.get("source", "")
    
                # Fix for old_source which might be a list of strings
                if isinstance(old_source, list):
                    old_source = "".join([str(item) for item in old_source])
    
                # Update source
                target_cell["source"] = new_source
    
                # Update type if specified
                if cell_type is not None:
                    target_cell["cell_type"] = cell_type
    
                # If changing to markdown, remove code-specific fields
                if cell_type == "markdown":
                    if "outputs" in target_cell:
                        del target_cell["outputs"]
                    if "execution_count" in target_cell:
                        del target_cell["execution_count"]
    
                # If code cell, reset execution
                if target_cell["cell_type"] == "code":
                    target_cell["outputs"] = []
                    target_cell["execution_count"] = None
    
                change_description = f"Replaced cell {cell_number}"
                if cell_type is not None and cell_type != old_type:
                    change_description += (
                        f" (changed type from {old_type} to {cell_type})"
                    )
    
            elif edit_mode == "insert":
                # Create new cell
                new_cell: dict[str, Any] = {
                    "cell_type": cell_type,
                    "source": new_source,
                    "metadata": {},
                }
    
                # Add code-specific fields
                if cell_type == "code":
                    new_cell["outputs"] = []
                    new_cell["execution_count"] = None
    
                # Insert the cell
                cells.insert(cell_number, new_cell)
                change_description = (
                    f"Inserted new {cell_type} cell at position {cell_number}"
                )
    
            else:  # delete
                # Store deleted cell info for reporting
                deleted_cell = cells[cell_number]
                deleted_type = deleted_cell.get("cell_type", "code")
    
                # Remove the cell
                del cells[cell_number]
                change_description = (
                    f"Deleted {deleted_type} cell at position {cell_number}"
                )
    
            # Write the updated notebook back to file
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(notebook, f, indent=1)
    
            await tool_ctx.info(
                f"Successfully edited notebook: {notebook_path} - {change_description}"
            )
            return (
                f"Successfully edited notebook: {notebook_path} - {change_description}"
            )
        except Exception as e:
            await tool_ctx.error(f"Error editing notebook: {str(e)}")
            return f"Error editing notebook: {str(e)}"
  • TypedDict defining the input schema for the tool parameters, using Annotated types for validation and descriptions (defined lines 17-55).
    class NotebookEditToolParams(TypedDict):
        """Parameters for the NotebookEditTool.
    
        Attributes:
            notebook_path: The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)
            cell_number: The index of the cell to edit (0-based)
            new_source: The new source for the cell
            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.
            edit_mode: The of edit to make (replace, insert, delete). Defaults to replace.
        """
    
        notebook_path: NotebookPath
        cell_number: CellNumber
        new_source: NewSource
        cell_type: CellType
        edit_mode: EditMode
  • Tool name property returning 'notebook_edit' and description property.
    @property
    @override
    def name(self) -> str:
        """Get the tool name.
    
        Returns:
            Tool name
        """
        return "notebook_edit"
  • The register method that creates the MCP tool wrapper function 'notebook_edit' with typed parameters and registers it using @mcp_server.tool, delegating to the class call method.
    def register(self, mcp_server: FastMCP) -> None:
        """Register this edit notebook tool with the MCP server.
    
        Creates a wrapper function with explicitly defined parameters that match
        the tool's parameter schema and registers it with the MCP server.
    
        Args:
            mcp_server: The FastMCP server instance
        """
        tool_self = self  # Create a reference to self for use in the closure
    
        @mcp_server.tool(name=self.name, description=self.description)
        async def notebook_edit(
            notebook_path: NotebookPath,
            cell_number: CellNumber,
            new_source: NewSource,
            cell_type: CellType,
            edit_mode: EditMode,
        ) -> str:
            ctx = get_context()
            return await tool_self.call(
                ctx,
                notebook_path=notebook_path,
                cell_number=cell_number,
                new_source=new_source,
                cell_type=cell_type,
                edit_mode=edit_mode,
            )
  • Instantiates NoteBookEditTool with permission_manager and registers all Jupyter tools via ToolRegistry (which calls each tool's register method). This function is called from the top-level tools registry.
    def get_jupyter_tools(permission_manager: PermissionManager) -> list[BaseTool]:
        """Create instances of all Jupyter notebook tools.
    
        Args:
            permission_manager: Permission manager for access control
    
        Returns:
            List of Jupyter notebook tool instances
        """
        return [
            NotebookReadTool(permission_manager),
            NoteBookEditTool(permission_manager),
        ]
    
    
    def register_jupyter_tools(
        mcp_server: FastMCP,
        permission_manager: PermissionManager,
    ) -> list[BaseTool]:
        """Register all Jupyter notebook tools with the MCP server.
    
        Args:
            mcp_server: The FastMCP server instance
            permission_manager: Permission manager for access control
    
        Returns:
            List of registered tools
        """
        tools = get_jupyter_tools(permission_manager)
        ToolRegistry.register_tools(mcp_server, tools)
        return tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SDGLBL/mcp-claude-code'

If you have feedback or need assistance with the MCP directory API, please join our Discord server