Skip to main content
Glama
SDGLBL
by SDGLBL

notebook_read

Extract code, text, and outputs from Jupyter notebook files to analyze data science projects and computational workflows.

Instructions

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.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
notebook_pathYesThe absolute path to the Jupyter notebook file to read (must be absolute, not relative)

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • Core handler implementation that performs path validation, reads the .ipynb file, parses it using JSON, processes cells and outputs via base class methods, formats the content, and returns the formatted string.
    @override
    async def call(
        self,
        ctx: MCPContext,
        **params: Unpack[NotebookReadToolParams],
    ) -> 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: NotebookPath = params["notebook_path"]
    
        # Validate path parameter
        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}"
    
        await tool_ctx.info(f"Reading notebook: {notebook_path}")
    
        # 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:
                # This will read the file, so we don't need to read it separately
                _, processed_cells = await self.parse_notebook(file_path)
    
                # Format the notebook content as a readable string
                result = self.format_notebook_cells(processed_cells)
    
                await tool_ctx.info(
                    f"Successfully read notebook: {notebook_path} ({len(processed_cells)} cells)"
                )
                return result
            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}"
        except Exception as e:
            await tool_ctx.error(f"Error reading notebook: {str(e)}")
            return f"Error reading notebook: {str(e)}"
  • Input schema definition: Annotated NotebookPath type and TypedDict for tool parameters.
    NotebookPath = Annotated[
        str,
        Field(
            description="The absolute path to the Jupyter notebook file to read (must be absolute, not relative)",
        ),
    ]
    
    
    class NotebookReadToolParams(TypedDict):
        """Parameters for the NotebookReadTool.
    
        Attributes:
            notebook_path: The absolute path to the Jupyter notebook file to read (must be absolute, not relative)
        """
    
        notebook_path: NotebookPath
  • Tool registration method that defines the 'notebook_read' wrapper function with input schema and decorates it with @mcp_server.tool using the tool's name and description.
    def register(self, mcp_server: FastMCP) -> None:
        """Register this read 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_read(
            ctx: MCPContext,
            notebook_path: NotebookPath,
        ) -> str:
            ctx = get_context()
            return await tool_self.call(ctx, notebook_path=notebook_path)
  • Instantiation of NotebookReadTool in get_jupyter_tools function, which is called during registration.
    return [
        NotebookReadTool(permission_manager),
        NoteBookEditTool(permission_manager),
    ]
  • Top-level call to register_jupyter_tools during register_all_tools, which instantiates and registers the notebook_read tool.
    # Register all jupyter tools
    jupyter_tools = register_jupyter_tools(mcp_server, permission_manager)
    for tool in jupyter_tools:
        all_tools[tool.name] = tool
  • Key helper method in base class that parses .ipynb JSON, extracts cells, processes outputs (text, images, errors, streams), cleans ANSI codes, and returns processed cells used by the handler.
    async def parse_notebook(
        self, file_path: Path
    ) -> tuple[dict[str, Any], list[NotebookCellSource]]:
        """Parse a Jupyter notebook file.
    
        Args:
            file_path: Path to the notebook file
    
        Returns:
            Tuple of (notebook_data, processed_cells)
        """
        with open(file_path, "r", encoding="utf-8") as f:
            content = f.read()
            notebook = json.loads(content)
    
        # Get notebook language
        language = (
            notebook.get("metadata", {}).get("language_info", {}).get("name", "python")
        )
        cells = notebook.get("cells", [])
        processed_cells = []
    
        for i, cell in enumerate(cells):
            cell_type = cell.get("cell_type", "code")
    
            # Skip if not code or markdown
            if cell_type not in ["code", "markdown"]:
                continue
    
            # Get source
            source = cell.get("source", "")
            if isinstance(source, list):
                source = "".join(source)
    
            # Get execution count for code cells
            execution_count = None
            if cell_type == "code":
                execution_count = cell.get("execution_count")
    
            # Process outputs for code cells
            outputs = []
            if cell_type == "code" and "outputs" in cell:
                for output in cell["outputs"]:
                    output_type = output.get("output_type", "")
    
                    # Process different output types
                    if output_type == "stream":
                        text = output.get("text", "")
                        if isinstance(text, list):
                            text = "".join(text)
                        outputs.append(
                            NotebookCellOutput(output_type="stream", text=text)
                        )
    
                    elif output_type in ["execute_result", "display_data"]:
                        # Process text output
                        text = None
                        if "data" in output and "text/plain" in output["data"]:
                            text_data = output["data"]["text/plain"]
                            if isinstance(text_data, list):
                                text = "".join(text_data)
                            else:
                                text = text_data
    
                        # Process image output
                        image = None
                        if "data" in output:
                            if "image/png" in output["data"]:
                                image = NotebookOutputImage(
                                    image_data=output["data"]["image/png"],
                                    media_type="image/png",
                                )
                            elif "image/jpeg" in output["data"]:
                                image = NotebookOutputImage(
                                    image_data=output["data"]["image/jpeg"],
                                    media_type="image/jpeg",
                                )
    
                        outputs.append(
                            NotebookCellOutput(
                                output_type=output_type, text=text, image=image
                            )
                        )
    
                    elif output_type == "error":
                        # Format error traceback
                        ename = output.get("ename", "")
                        evalue = output.get("evalue", "")
                        traceback = output.get("traceback", [])
    
                        # Handle raw text strings and lists of strings
                        if isinstance(traceback, list):
                            # Clean ANSI escape codes and join the list but preserve the formatting
                            clean_traceback = [
                                clean_ansi_escapes(line) for line in traceback
                            ]
                            traceback_text = "\n".join(clean_traceback)
                        else:
                            traceback_text = clean_ansi_escapes(str(traceback))
    
                        error_text = f"{ename}: {evalue}\n{traceback_text}"
                        outputs.append(
                            NotebookCellOutput(output_type="error", text=error_text)
                        )
    
            # Create cell object
            processed_cell = NotebookCellSource(
                cell_index=i,
                cell_type=cell_type,
                source=source,
                language=language,
                execution_count=execution_count,
                outputs=outputs,
            )
    
            processed_cells.append(processed_cell)
    
        return notebook, processed_cells
Behavior3/5

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

No annotations are provided, so the description carries the full burden of behavioral disclosure. It states that the tool 'returns all of the cells with their outputs,' which clarifies the return behavior beyond just reading the file. However, it doesn't address potential issues like file permissions, error handling for invalid paths or formats, or whether the operation is idempotent. The description adds some value but leaves gaps in behavioral context.

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

Conciseness5/5

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

The description is appropriately sized and front-loaded: the first sentence clearly states the tool's purpose and output, the second provides useful background context about Jupyter notebooks, and the third adds a crucial parameter constraint. Every sentence earns its place without redundancy or fluff.

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 low complexity (1 parameter, no nested objects) and the presence of an output schema (which handles return values), the description is mostly complete. It covers the purpose, output format, and a key parameter constraint. However, it could improve by addressing behavioral aspects like error handling or permissions, which are not covered by annotations or the output schema.

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

Parameters4/5

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

The input schema has 100% description coverage, with the parameter 'notebook_path' fully documented in the schema. The description adds semantic context by reinforcing that the path 'must be an absolute path, not a relative path,' which emphasizes a critical constraint. Since schema coverage is high, the baseline is 3, but the extra emphasis on the absolute path requirement justifies a higher score.

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

Purpose4/5

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

The description clearly states the tool's purpose: 'Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs.' It specifies the verb ('Reads'), resource ('Jupyter notebook'), and output format ('cells with their outputs'). However, it doesn't explicitly differentiate from sibling tools like 'read' or 'notebook_edit', which would be needed for a perfect score.

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

Usage Guidelines3/5

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

The description provides implied usage guidance by mentioning that Jupyter notebooks are 'commonly used for data analysis and scientific computing,' suggesting a context for when this tool might be appropriate. However, it lacks explicit guidance on when to use this tool versus alternatives like 'read' (for general files) or 'notebook_edit' (for modifying notebooks), and doesn't mention any prerequisites or exclusions beyond the path requirement.

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