notebook-tools-mcp
README.md
# notebook-tools-mcp
MCP server for Jupyter notebook navigation and editing. Reads `.ipynb` files directly as JSON — no Jupyter server, no kernel, no overhead.
## Problem
AI CLI agents (Claude Code, Gemini CLI, Codex) working inside VS Code Dev Containers don't have a good way of interacting with Jupyter notebooks. Notebooks are JSON files where 90%+ of the bytes are base64-encoded image outputs and execution metadata. Reading a 1MB notebook with a standard file-read tool wastes ~294K tokens on noise. Agents re-invent notebook parsing logic every session, burning tokens on the same problem repeatedly.
## Architecture
6 files, ~700 lines, zero dependencies beyond `mcp>=1.10.1`:
```
notebook_tools_mcp/
__init__.py (35L) FastMCP instance + server instructions
_helpers.py (182L) Shared utilities: load/save, cell formatting, parsing
read_tools.py (251L) 6 read tools
search_tools.py (126L) 2 search tools
write_tools.py (104L) 3 write tools
server.py (16L) Entry point
```
Each file < 260 lines. Single responsibility. Shared helpers avoid duplication. No circular imports.
## Design decisions
**No Jupyter server.** Works in remote VS Code Dev Containers (Docker) over SSH. Adding a Jupyter server means extra memory, extra complexity, zero value when VS Code already renders notebooks natively.
**No dependencies beyond `mcp`.** Notebooks are JSON. Python's stdlib `json` module reads them perfectly. `nbformat` (and its 50+ transitive Jupyter ecosystem deps) is unnecessary weight.
**No execution.** Running code requires a kernel, which requires a server. That's the complexity this tool exists to avoid.
**stdio transport only.** Launched on demand by the MCP client, communicates via stdin/stdout, exits when done. No HTTP endpoints, no WebSocket connections, no persistent processes.
**Consistent index-based addressing.** All 11 tools use integer cell indices. `nb_overview` shows indices → `nb_read_cell(15)` reads → `nb_write_cell(15, ...)` edits. One addressing scheme throughout.
**`sort_keys=True` on save.** Matches Jupyter/nbformat convention for deterministic output. Prevents noisy git diffs from key reordering between load/save cycles.
### Write tools vs NotebookEdit
Claude Code has a built-in `NotebookEdit` tool (addresses cells by `cell_id` or `cell_number`). This server also provides write tools. Both are kept intentionally:
| | MCP write tools | NotebookEdit (built-in) |
|---|---|---|
| Addressing | Integer index (matches `nb_overview`) | `cell_id` string or `cell_number` (0-indexed) |
| Best for | Within the `nb_overview` → read → edit flow | When VS Code or another tool provides the cell_id |
| Footprint | 102 lines | Built-in |
The MCP write tools exist for **workflow cohesion** — an agent using `nb_overview` to find a cell already has its index. Requiring a lookup to get the `cell_id` for NotebookEdit would add a pointless extra step.
### Tools we deliberately did NOT build
These were considered and rejected because `nb_search` / `nb_search_dir` already cover them:
| Rejected tool | Why `nb_search` is sufficient |
|---|---|
| `nb_dependencies(var)` | `nb_search(path, "var_name")` returns definitions and usages; LLMs trivially distinguish `x = ...` from `...x...` |
| `nb_function_map` | `nb_search_dir(dir, "^def\\s+\\w+", cell_type="code")` |
| `nb_imports_all` | `nb_search_dir(dir, "^import\|^from\\s+\\w+\\s+import")` |
| `nb_compare_cells` | Agent calls `nb_read_cell` twice |
Building AST-based dependency tracking would add ~200+ lines of fragile code that breaks across Python/R/Julia/bash cells. Not worth it when regex search + LLM reasoning achieves the same result.
### How agents discover when to use these tools
Claude Code (and similar agents) decide which tool to use based on three channels, in order of priority:
1. **MCP server `instructions`** — set via `FastMCP(instructions=...)` in `__init__.py`. Injected into the system prompt of every conversation where the server is connected. This is where the "use nb_search INSTEAD OF Grep for .ipynb" guidance lives. Agents see this before any tool is called.
2. **Tool docstrings** — the `"""..."""` on each `@mcp.tool()` function. Shown when the agent discovers tools (e.g. via ToolSearch). Each docstring says what the tool does AND when to prefer it over alternatives.
3. **CLAUDE.md** — project-level instructions always loaded into context. Contains a decision table mapping tasks to tools (e.g. "Read notebook → nb_overview, NOT Read tool").
All three channels reinforce the same message: use `nb_*` tools for `.ipynb`, never `Read`/`Grep`.
## Install
```bash
# pip
pip install git+https://github.com/tony-zhelonkin/notebook-tools-mcp.git
# uv
uv pip install git+https://github.com/tony-zhelonkin/notebook-tools-mcp.git
```
For development (editable install from a local clone):
```bash
git clone https://github.com/tony-zhelonkin/notebook-tools-mcp.git
pip install -e notebook-tools-mcp/
```
## Configure
Add to `.mcp.json`:
```json
{
"mcpServers": {
"notebook-tools": {
"type": "stdio",
"command": "python",
"args": ["-m", "notebook_tools_mcp.server"]
}
}
}
```
Or toggle via [SciAgent-toolkit](https://github.com/tony-zhelonkin/SciAgent-toolkit) addon system:
```bash
./scripts/manage-addon.sh enable notebook-tools --project-dir /path/to/project
./scripts/manage-addon.sh disable notebook-tools --project-dir /path/to/project
```
## Tools
All tools take `notebook_path` (absolute path) as first parameter unless noted.
### Read tools
| Tool | Parameters | What it does |
|------|-----------|-------------|
| `nb_metadata` | `notebook_path` | Kernel info, format version, cell counts, file size, output size |
| `nb_overview` | `notebook_path`, `include_output_sizes=true` | Cell index, type, line/char count, first-line preview, output sizes |
| `nb_read_cell` | `notebook_path`, `cell_index`, `include_outputs=false`, `max_output_chars=2000` | Full source of one cell by index, optional truncated outputs |
| `nb_read_cells` | `notebook_path`, `cell_indices` (e.g. `"0,1,5-8"`), `cell_type=null`, `include_outputs=false` | Batch read with range syntax, optional type filter |
| `nb_read_section` | `notebook_path`, `header`, `max_cells=50`, `include_outputs=false` | All cells from a markdown heading to the next same-or-higher-level heading |
| `nb_headings` | `notebook_path` | All markdown headings with cell indices and levels |
### Search tools
| Tool | Parameters | What it does |
|------|-----------|-------------|
| `nb_search` | `notebook_path`, `pattern` (regex), `cell_type=null`, `context_lines=1` | Regex search across cells with context, optional type filter |
| `nb_search_dir` | `directory`, `pattern` (regex), `cell_type=null`, `context_lines=0` | Search all `.ipynb` files in a directory, grouped by notebook |
### Write tools
| Tool | Parameters | What it does |
|------|-----------|-------------|
| `nb_write_cell` | `notebook_path`, `cell_index`, `source` | Overwrite source content of an existing cell |
| `nb_insert_cell` | `notebook_path`, `cell_index` (`-1` to append), `cell_type`, `source` | Insert a new cell at position |
| `nb_delete_cell` | `notebook_path`, `cell_index` | Delete a cell |
## Typical agent workflow
```
1. nb_metadata(path) → Is this notebook big? What kernel?
2. nb_overview(path) → See all cells at a glance (index, type, preview)
3. nb_headings(path) → Understand section structure
4. nb_search(path, "pattern") → Find cells containing a variable/function/import
5. nb_read_cell(path, 15) → Read the specific cell you need
6. nb_read_cells(path, "15-20") → Read a range of related cells
7. nb_read_section(path, "Results") → Read everything under a heading
8. nb_write_cell(path, 15, src) → Edit the cell in-place
```
The key insight: **start with `nb_overview`, then drill down**. Never read the full notebook.
## Token economics
Tested against real project notebooks:
| Notebook | File size | Cells | Full `Read` tool | `nb_overview` | `nb_read_cell` | `nb_search` |
|---|---|---|---|---|---|---|
| NB00 (smallest) | 51 KB | 30 | ~13K tokens | ~500 tokens | ~50-200 tokens | ~100 tokens |
| NB01 (biggest) | 1.15 MB | 41 | ~294K tokens | ~690 tokens | ~100-400 tokens | ~50-200 tokens |
For the 1.15 MB notebook, `nb_overview` achieves **~425x token reduction** vs reading the full file. Individual cell reads achieve **~700-2900x reduction**.
## CLAUDE.md snippet
Add this to your project's `CLAUDE.md` to steer Claude Code toward using notebook-tools instead of built-in tools for `.ipynb` files. The server also ships its own `instructions` (injected into the system prompt automatically), but `CLAUDE.md` reinforcement ensures consistent behavior, especially when agents are choosing between several tools.
```markdown
### Working with .ipynb files
**ALWAYS use `notebook-tools` MCP tools instead of built-in tools for `.ipynb` files:**
| Task | Use this | NOT this | Why |
|------|----------|----------|-----|
| Read notebook | `nb_overview` then `nb_read_cell` | `Read` tool | Read loads raw JSON with base64 images, wastes 100K+ tokens |
| Search notebook | `nb_search` / `nb_search_dir` | `Grep` tool | Grep sees JSON structure, nb_search sees cell source code |
| Edit notebook cell | `nb_write_cell` (by index from nb_overview) | — | Consistent index-based workflow |
| Edit notebook cell | `NotebookEdit` (by cell_id) | — | Use when cell_id is known from another source |
| Insert/delete cells | `nb_insert_cell` / `nb_delete_cell` | — | Index-based, consistent with nb_overview |
**Workflow:** `nb_overview` (get cell indices) → `nb_read_cell` or `nb_search` → `nb_write_cell`.
All 11 tools are in the `notebook-tools` MCP server. Start with `nb_overview` for any notebook interaction.
```
### When NotebookEdit is the better choice
The MCP write tools and Claude Code's built-in `NotebookEdit` solve the same problem (editing notebook cells) with different addressing:
- **MCP `nb_write_cell`**: addresses by integer index. Best when you're already in the `nb_overview` → `nb_read_cell` → edit flow, because you already have the index.
- **NotebookEdit**: addresses by `cell_id` (a string like `"abc123"`) or `cell_number` (0-indexed integer). Best when something else gives you the cell_id — for example, VS Code's notebook renderer, or the `id` field shown in `nb_read_cell` output.
In practice, MCP write tools are used more often because the typical agent workflow starts with `nb_overview`, which shows indices. `NotebookEdit` is better when the agent already has a `cell_id` from a non-MCP source, or when it needs `NotebookEdit`'s `edit_mode: "insert"` semantics (insert *after* a specific cell_id rather than *at* an index position).
Both tools can coexist safely. The server's `instructions` field tells agents to prefer MCP write tools during the `nb_overview` workflow. There is no conflict as long as the agent doesn't use both on the same cell in the same turn.
## Changelog
### v0.3.0 (2026-02-25)
- **Fix:** Added `sort_keys=True` to `save_notebook` for deterministic JSON output matching Jupyter/nbformat convention
- **Agent guidance:** Added `FastMCP(instructions=...)` with tool selection decision tree (injected into agent system prompt)
- **Agent guidance:** Rewrote all 11 tool docstrings to specify when to use each tool vs built-in alternatives
- **Doc:** Documented write tools relationship to Claude Code's built-in NotebookEdit
- **Doc:** Added "How agents discover when to use these tools" section explaining the 3-channel guidance pattern
### v0.2.0 (2026-02-25)
- Added write tools: `nb_write_cell`, `nb_insert_cell`, `nb_delete_cell`
- Modularized into 6-file architecture
- Added `nb_headings`, `nb_read_section`, `nb_search_dir`
### v0.1.0
- Initial release: `nb_metadata`, `nb_overview`, `nb_read_cell`, `nb_read_cells`, `nb_search`
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues