Skip to main content
Glama

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