Skip to main content
Glama
Liu-Eroteme
by Liu-Eroteme
README.md
# jupyter-mcp

Kernel-attached MCP server for Jupyter EDA workflows. Built for coding
agents that iterate on notebooks: named cells, a dependency DAG, minimal
re-execution, condensed outputs, and cheap LLM summaries for navigation.

## Why

Editing notebooks through generic file tools is painful for agents:

- cells have no stable, human-meaningful addresses
- every change means re-executing the whole notebook from scratch
- raw outputs (box-drawn tables, base64 charts, ANSI noise) flood context
- nothing tells you which cells a change invalidates

This server fixes each of those with an opinionated data model.

## Concepts

| Concept | What it means |
|---|---|
| **Cell names** | Every cell has a unique kebab-case name stored in `cell.metadata.jupyter_mcp.name`. Unnamed cells get auto-names from their first comment/heading. |
| **Revisions** | Each cell has a short content hash (`rev`). Every mutation requires the `expected_rev` from your latest read — optimistic locking that makes wrong-target and stale edits structurally impossible. No confirmation round-trips. |
| **Dependency DAG** | A static AST pass (not runtime tracing) extracts per-cell defines/uses/mutations and builds last-writer-wins edges. Works on unexecuted cells, which is the whole point: edit first, then `run`. |
| **Staleness** | A cell is fresh only if its *current source* ran on the *currently live kernel* (freshness is stamped with a per-kernel epoch). Source edits make a cell and its dependents stale; a new or restarted kernel makes everything stale — persisted metadata can never claim freshness against empty kernel state. `run` executes exactly the stale set, in document order. |
| **Background execution** | Every run goes through a per-notebook executor thread. `run` waits a bounded time (default 60 s); anything longer keeps executing while outputs accumulate in a live buffer — the overview shows RUNNING/QUEUED, `read_cells` returns output-so-far, `interrupt` stops it. Oversized output keeps head+tail with explicit dropped markers. |
| **Condensed outputs** | Streams merged, ANSI stripped, long text truncated head+tail with explicit markers. Duplicate table reprs collapse to one: uniform → CSV, ragged → JSON. Charts return as real MCP images (downscaled), so the agent *sees* them. |
| **Summaries** | Lazy, batched `claude-haiku-4-5` summaries (tldr + description + output summary) cached in cell metadata keyed by content hash. Unchanged cells are never re-summarized. Degrades to deterministic fallbacks (marked `*`) without API credentials. |
| **Snapshots / undo** | Every mutation snapshots the file first (under `~/.cache/jupyter_mcp/`); `undo_last` restores it. |

## Tools

| Tool | Purpose |
|---|---|
| `create_notebook` | New empty notebook |
| `notebook_overview` | Index: names, revs, staleness, tldrs, edges, lint |
| `read_cells` | Full cells (code + condensed outputs + images), by names/slice |
| `add_cell` / `update_cell` / `remove_cell` / `move_cell` | Mutations; all take `expected_rev`; add/update accept `run="stale"` to fold the edit→run loop into one call |
| `run` | Default: all stale cells (minimal recompute). With `cells`: exactly those, even if fresh, after freshening their stale ancestors (`fresh_deps`). Bounded wait; continues in background |
| `interrupt` | Stop the running cell (KeyboardInterrupt), cancel the queue |
| `restart_kernel` | Fresh kernel; marks everything stale |
| `inspect_variable` | Type/shape/schema of a live variable plus its richest repr: dataframes as CSV, figures as images, else pretty repr |
| `undo_last` | Restore pre-mutation snapshot |
| `summarize_cells` | Detailed LLM summaries incl. outputs |
| `search_cells` | Search source + names + summaries |

## Setup

```sh
uv sync
```

Register with Claude Code (`.mcp.json` in any project, or globally):

```json
{
  "mcpServers": {
    "jupyter": {
      "command": "uv",
      "args": ["run", "--project", "/path/to/jupyter_mcp", "jupyter-mcp"]
    }
  }
}
```

The server is multi-notebook: every tool takes a notebook `path`, one kernel
per notebook, started lazily in the notebook's directory (so relative data
paths behave like in your editor). Kernelspec comes from the notebook's
metadata, falling back to `python3`. Kernels idle longer than 30 minutes are
shut down lazily (`JUPYTER_MCP_KERNEL_TTL_SECONDS` overrides); the next
execution restarts them, and epoch-scoped staleness handles the rest. On
POSIX, kernels connect over IPC sockets (no open TCP ports).

### Summaries & credentials

Summaries use the plain `anthropic` SDK: credentials resolve from
`ANTHROPIC_API_KEY` or an `ant auth login` profile automatically. Cost is
negligible (Haiku, batched, hash-cached). To disable entirely set
`JUPYTER_MCP_DISABLE_SUMMARIES=1` — everything else works unchanged.

## Development

```sh
uv run pytest              # full suite
uv run pytest -m "not kernel"   # skip real-kernel integration tests
```

Layout: `src/jupyter_mcp/` is a plain library (model, dag, condense, kernel,
summaries, session) with the MCP surface isolated in `server.py`; everything
below the server is unit-testable without MCP.

## Known limitations (v1)

- The DAG is static: dynamic patterns (`globals()[name] = ...`, `exec`,
  attribute mutation through aliases) are invisible. Method calls only count
  as mutations for a known allowlist (`append`, `fit`, ...) — pure-functional
  chains (polars) intentionally create no false forward edges.
- `%%bash` / `%%sql` style cells are treated as opaque (no dependencies).
- Concurrent edits from a live Jupyter editor are detected (the server
  reloads and rejects the mutation) but not merged.

See [docs/ROADMAP.md](docs/ROADMAP.md) for what's deliberately deferred —
including the phase-2 OKF knowledge base.

TDQS

A3.8/5.0

Scored across 14 tools

Disambiguation5/5

Each tool has a clear, distinct purpose with minimal overlap. The only potential ambiguity is between add_cell's run option and the run tool, but their primary functions are sufficiently different.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in snake_case (e.g., add_cell, create_notebook, inspect_variable). The only minor deviation is undo_last (verb_adverb), but it remains consistent with the overall style.

Tool Count5/5

With 14 tools, the set is well-scoped for a Jupyter notebook server. It covers creation, editing, execution, inspection, and undo operations without being excessive or insufficient.

Completeness4/5

The tool surface covers the core notebook lifecycle (CRUD, execution, inspection, undo). However, there is no explicit save or export tool, which is a minor gap assuming automatic persistence.

Maintenance

ActivitySlowing
ResponsivenessNo issues