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

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

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
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden. It discloses key behavioral traits: it's a mutation tool (replaces/inserts/deletes), requires absolute paths, uses 0-indexed cell numbers, and has default values. However, it does not mention permissions, error handling, or side effects like file locking, which are important for a mutation tool with no annotation coverage.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized and front-loaded with the core purpose. The second sentence about Jupyter notebooks adds useful context, and the subsequent sentences provide specific usage details. It could be slightly more concise by integrating some details, but overall it's efficient with minimal waste.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (mutation with multiple modes) and no annotations, the description does a good job covering purpose, usage, and key parameters. With an output schema present, it doesn't need to explain return values. However, it could improve by addressing potential errors or prerequisites more explicitly for a mutation tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema already documents all parameters thoroughly. The description adds some context (e.g., 'Jupyter notebooks are interactive documents...', 'must be an absolute path'), but does not provide significant additional meaning beyond what's in the schema descriptions. This meets the baseline for high schema coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description starts with 'Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source,' which provides a specific verb ('replaces'), resource ('cell in a Jupyter notebook'), and distinguishes it from sibling tools like notebook_read (read-only) and edit (generic). It clearly states what the tool does beyond just the name.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides clear context for when to use this tool (e.g., 'Use edit_mode=insert to add a new cell... Use edit_mode=delete to delete the cell...'), but it does not explicitly mention when not to use it or name alternatives like notebook_read for reading or edit for non-notebook files. This gives good guidance but lacks explicit exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other 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