jupyter-mcp
Recipe card from the charly-jupyter plugin (Images — the deployable catalog).
jupyter-mcp – JupyterLab CRDT MCP server extension
Section titled “jupyter-mcp – JupyterLab CRDT MCP server extension”Candy Properties
Section titled “Candy Properties”| Property | Value |
|---|---|
| Dependencies | (none) |
| Packages | (none – pip-only Tier 1 candy) |
| Services | (none) |
| Volumes | (none) |
| Install files | charly.yml, task:, jupyter_mcp/ (Python package) |
Architecture: Tier 1 Post-Install Layer
Section titled “Architecture: Tier 1 Post-Install Layer”This is a Tier 1 “post-install” layer — it has no pixi.toml and installs into whatever pixi environment exists from the parent Tier 2 candy. It follows the same pattern as llama-cpp and unsloth.
Single source of truth: The jupyter_mcp Python package lives only in this candy. Both jupyter (lightweight) and jupyter-ml (GPU ML) compose it via their candy: field. This prevents code duplication and ensures bug fixes propagate to all boxes.
Usage philosophy and caveats
Section titled “Usage philosophy and caveats”The MCP server manages CRDT rooms invisibly. Clients see notebooks and cells; the server takes care of room lifecycle. Reading or mutating any notebook just works — call cell_get, cell_update, notebook_get directly.
- MCP works WITH the user. Every
notebook_*andcell_*tool auto-attaches to whichever CRDT room exists for the path (JupyterLab UI tab, another MCP session, or this one), or creates a fresh room if none exists. Callingcell_updateon a notebook the user has open in JupyterLab edits THAT EXACT Y.Doc — the user sees changes in real time. There is no scenario where MCP and UI work in parallel rooms. - No client-side room management. Clients do not open or close CRDT rooms. Idle rooms (no clients, no MCP activity for
MCP_ROOM_IDLE_TIMEOUT_SEC, default 600s) are flushed and closed by a server-side sweeper. Configure the timeout via env var on the candy for fast tests. cell_updateis atomic bycell_id. The cell’s stable identifier is preserved across the update, so a sequence ofcell_updatecalls never duplicates cells even when the room has concurrent state.- Path canonicalization. Any path you pass —
"foo.ipynb","./foo.ipynb","/workspace/foo.ipynb"— is normalized to the workspace-relative form before reachingfile_id_manager.index(). All three converge on the same room. Host paths and..escapes are rejected with a clear error. - Single room per notebook is an INVARIANT. Two MCP sessions plus the JupyterLab UI editing the same notebook ALL share one Y.Doc. If
room_listever shows two entries for the same logical file, that’s a regression — open a bug. - Don’t mix MCP cell-mutations with direct disk writes on the same notebook concurrently. The upstream
jupyter_server_ydocfile watcher CRDT-merges external writes against the in-memory state, producing hybrid cells. Pick one writer per session — either MCP or direct file edit, not both. - After bulk edits,
notebook_getcell-count check is cheap insurance. Defensive verification catches upstream regressions early. notebook_watchreturns on the FIRST change after the call started. Pair withnotebook_getif you need to confirm a specific change landed.- Idle rooms auto-flush + close. A notebook with no connected clients and no MCP activity for
MCP_ROOM_IDLE_TIMEOUT_SECis flushed to disk and removed from memory. The next access auto-creates the room again from disk. No client-driven flush is needed. file_id_manager.dbis auto-cleaned on adapter init. Rows whose path resolves outside the workspace (host-path leaks) and rows whose underlying file no longer exists (with no active room referencing them) are pruned. Idempotent and safe.
How It Works
Section titled “How It Works”The task: performs three operations:
- Install FastMCP —
pip install "fastmcp>=3.2.0"(not via pixi because pixi’s cross-platform resolver conflicts with opentelemetry-api on aarch64) - Install jupyter-mcp extension —
pip install --no-deps /ctx/jupyter_mcp(from the candy’s build context) - Enable Jupyter Server extension — writes
jupyter_mcp.jsonto the Jupyter server config directory
MCP Server
Section titled “MCP Server”The extension registers a Streamable HTTP MCP server at http://localhost:8888/mcp (MCP spec 2025-11-25). It provides 11 tools, all named in <noun>_<verb> form.
| Category | Tools |
|---|---|
| Notebook management | notebook_list, notebook_create, notebook_get, notebook_watch, notebook_list_users |
| Cell operations | cell_get, cell_update, cell_insert, cell_delete, cell_execute |
| Read-only diagnostic | room_list |
| Tool | Description |
|---|---|
notebook_list |
List all notebooks in the workspace (filesystem) |
notebook_create |
Create a new empty notebook on disk (filesystem; the room auto-attaches on the first cell op) |
notebook_get |
Get full notebook content from the live CRDT document; auto-attaches to existing room or creates one |
notebook_watch |
Block until a CRDT change is observed; auto-attaches |
notebook_list_users |
List awareness users currently connected to one notebook’s room (read-only diagnostic; returns [] if no room exists) |
cell_get |
Get a specific cell’s content; auto-attaches |
cell_update |
Replace a cell’s source IN PLACE — preserves the cell’s id, leaves position in _ycells structurally untouched. Auto-attaches |
cell_insert |
Insert a new cell at a position; auto-attaches |
cell_delete |
Delete a cell; auto-attaches |
cell_execute |
Execute a cell, return outputs, AND write outputs + execution_count back via the in-place set_cell path. Auto-attaches |
room_list |
Read-only diagnostic: list every active CRDT room with rich metadata (room_id, path, file_id, users, user_count, has_kernel). Use to verify the single-room invariant |
CRDT Collaboration
Section titled “CRDT Collaboration”Cell operations mutate the live CRDT document — changes appear instantly in all connected JupyterLab clients. Multiple MCP clients and browser users can edit the same notebook simultaneously. The server uses jupyter-server-ydoc for CRDT document management. The single-room invariant means MCP and UI clients ALWAYS share one Y.Doc per logical file — calls converge through deterministic room_id = json:notebook:<file_id_manager.index(canonical_path)>.
Key implementation details:
-
Auto-attach in
_resolve_notebook_doc. Every path-accepting adapter method routes through this single resolve point. It computes the canonical path, looks up the deterministic room_id, and either attaches to the existingserver.rooms[room_id](UI-created or MCP-created) or constructs a new room mirroringYDocWebSocketHandler.prepare(). Same code path → same room → no parallel state. -
In-place
set_cell. Cell mutations operate on the existing Y.Map’ssource(Y.Text),metadata(Y.Map), andoutputs(Y.Array) IN PLACE. The cell’sidand its position in_ycellsare structurally untouched (no fresh UUID minted, no delete-then-insert at the CRDT level). A post-condition verify ensuresidstability after every mutation. -
Path canonicalization at the boundary.
_canonical_notebook_path(path)resolves host paths and..escapes BEFORE any call tofile_id_manager.index(). Prevents host-path leaks where an absolute host path could be minted into the container’s file_id manager from a stray client call. -
Server-side idle-room sweeper. A background asyncio task runs every
MCP_ROOM_SWEEP_INTERVAL_SEC(default 60), flushes and closes rooms with zero connected clients and idle for >MCP_ROOM_IDLE_TIMEOUT_SEC(default 600). Activity tracking via_room_last_active: every CRDT mutation through the adapter bumps the timestamp, so an actively-mutated MCP-only room (no WS clients) is not reaped. -
cell_executepersists outputs. After collecting iopub messages, the adapter translates them vianbformat.v4.output_from_msg, capturesexecution_countfrom the shell-channelexecute_reply(works forprint()/display()-only cells that don’t emitexecute_result), and writescell["outputs"]+cell["execution_count"]back viaset_cell()— the same in-place CRDT-aware path used bycell_update. Eventually flushed to disk by the upstream save_delay or the idle-room sweeper. -
file_id_manager.dbcleanup on init. A one-shot pass deletes rows whose path is outside the notebook root (host-path leaks) or whose underlying file no longer exists (with no active room referencing them). Idempotent — safe to run on every server start.
Architecture Stack
Section titled “Architecture Stack”Claude Code / MCP Client ↓ Streamable HTTP (POST /mcp)FastMCP Server (mcp_server.py) — 11 tools, no room_* management ↓JupyterLab RTC Adapter (rtc_adapter.py) — auto-attach + canonicalization ↓ CRDT operations (in-place Y.Map mutation)jupyter-server-ydoc DocumentRoom — single room per file_id ↓ Y.js document syncJupyterLab Kernel Manager (execute_cell)Key Files
Section titled “Key Files”candy/jupyter-mcp/ charly.yml # Description only (Tier 1, no deps) # tasks: block in charly.yml — fastmcp + pip install + extension enable jupyter_mcp/ # Python package pyproject.toml jupyter_mcp/ __init__.py app.py # Jupyter server extension entry point mcp_server.py # FastMCP tool definitions (11 tools) rtc_adapter.py # CRDT room management, kernel execution tornado_asgi.py # Tornado-to-ASGI bridge for FastMCPIntegration with mcp_provide
Section titled “Integration with mcp_provide”The parent jupyter candy declares mcp_provide to make this MCP server discoverable to other services at deploy time. The hermes service auto-discovers this server via the CHARLY_MCP_SERVERS env var and registers all 11 tools as mcp_jupyter_<tool_name>.
MCP Name Decoupling (design principle)
Section titled “MCP Name Decoupling (design principle)”The jupyter MCP server name is deliberately decoupled from the candy name, the Python package name, and the box name. Three places anchor it:
| File | Field | Value | Purpose |
|---|---|---|---|
candy/jupyter/charly.yml |
env.MCP_SERVER_NAME |
"jupyter" |
Runtime advertisement |
candy/jupyter/charly.yml |
mcp_provide[0].name |
jupyter |
Cross-container discovery (hermes, openwebui) |
plugins/charly-jupyter/.mcp.json |
mcpServers.jupyter |
— | Claude Code static registration |
Package/candy/box names describe the artifact; the MCP name describes the service contract. Rename the artifact freely; the contract is stable.
Used In Candies
Section titled “Used In Candies”/charly-jupyter:jupyter— lightweight multi-arch JupyterLab (candy: [jupyter-mcp])/charly-jupyter:jupyter-ml— GPU ML JupyterLab (candy: [llama-cpp, unsloth, jupyter-mcp])
Used In Boxes
Section titled “Used In Boxes”Related Skills
Section titled “Related Skills”/charly-jupyter:jupyter— lightweight Tier 2 parent candy/charly-jupyter:jupyter-ml— GPU ML Tier 2 parent candy/charly-image:layer— candy authoring rules (Tier 1 pattern)/charly-build:charly-mcp-cmd— client-side verb for probing this server’s tool catalog (ping, list-tools, call); usecharly check live jupyter --filter mcpto see all 11 tools this candy registers/charly-selkies:chrome-devtools-mcp— sibling MCP-server-provider candy for Chrome DevTools (different domain, samemcp_providepattern)/charly-hermes:hermes— downstream MCP consumer (auto-discoversjupyterviaCHARLY_MCP_SERVERS; uses the 11 tools to read/edit/execute notebook cells programmatically)/charly-openwebui:openwebui— downstream MCP consumer (setsCODE_EXECUTION_ENGINE=jupyterwhen this server is discovered, routing Open WebUI’s in-chat code blocks to the Jupyter kernel)
When to Use This Skill
Section titled “When to Use This Skill”Use when the user asks about:
- The jupyter-mcp candy or its MCP server implementation
- How the CRDT collaboration works in JupyterLab
- The 11 MCP tools for notebook manipulation
- The auto-attach single-room invariant
- The path canonicalization rules
- The idle-room sweeper / cleanup behavior
- How the MCP extension is installed and enabled
- The Tier 1 extraction pattern (avoiding code duplication across Tier 2 candies)
Related
Section titled “Related”/charly-check:check— declarative testing (check:block,charly check box,charly check live)