Skip to content

jupyter

Version 2026.144.1443
Repo superproject

JupyterLab notebook server with a CRDT MCP server for agent-driven notebook editing. Installs JupyterLab into the pixi default environment (the jupyter-lab binary), adds the spaCy en_core_web_sm NLP model and the jupyterlab-quarto extension, and serves notebooks on port 8888 from the /workspace volume under supervisord. A bundled MCP server (composed from jupyter-mcp) exposes notebook_/cell_ tools at /mcp so an agent can create and edit notebooks over the Model Context Protocol.

  • jupyter

This candy’s plan: — the runnable spec charly check executes against a live deployment. check: steps are idempotent probes; run: steps change state.

Intent Step
check JupyterLab is installed as a pixi-managed binary in the default env
check the spaCy en_core_web_sm model loads for NLP notebooks
check the workspace volume is mounted for notebooks
check the JupyterLab service is running under supervisord
check the JupyterLab REST API answers on the published port with a server version
check the Jupyter MCP server responds to a protocol ping
check the MCP tool catalog exposes the notebook and cell manipulation tools
agent-check an agent can create a notebook and edit its cells through the MCP server end to end
check command=${HOME}/.pixi/envs/default/bin/python -c “import spacy; spacy.load(‘en_core_web_sm’)”
check command=${HOME}/.pixi/envs/default/bin/jupyter labextension list 2>&1
check addr=127.0.0.1:${HOST_PORT:8888}
check mcp=list-tools
check mcp=call
check command=${HOME}/.pixi/envs/default/bin/python - <<‘PYEOF’ import asyncio, json, sys, uuid from fastmcp import Client async def main(): async with Client(“http://localhost:8888/mcp”) as c: async def call(t, a=None): r = await c.call_tool(t, a or {}) return r.data if r.data is not None else json.loads( “”.join(b.text for b in r.content if hasattr(b, “text”)) ) p = f“check_id{uuid.uuid4().hex[:8]}.ipynb“ await call(“notebook_create”, {“path”: p}) await call(“cell_insert”, {“path”: p, “index”: 0, “source”: “alpha”, “cell_type”: “markdown”}) before = (await call(“cell_get”, {“path”: p, “index”: 0}))[“id”] await call(“cell_update”, {“path”: p, “index”: 0, “source”: “beta”}) after = (await call(“cell_get”, {“path”: p, “index”: 0}))[“id”] assert before == after, f“id drift {before!r} -> {after!r}” print(“OK”) asyncio.run(main()) PYEOF
check command=${HOME}/.pixi/envs/default/bin/python - <<‘PYEOF’ import asyncio, json, uuid from fastmcp import Client async def main(): async with Client(“http://localhost:8888/mcp”) as c: async def call(t, a=None): r = await c.call_tool(t, a or {}) return r.data if r.data is not None else json.loads( “”.join(b.text for b in r.content if hasattr(b, “text”)) ) p = f“check_count{uuid.uuid4().hex[:8]}.ipynb“ await call(“notebook_create”, {“path”: p}) for i, src in enumerate([“a”,“b”,“c”,“d”,“e”]): await call(“cell_insert”, {“path”: p, “index”: i, “source”: src, “cell_type”: “markdown”}) # Upstream YNotebook auto-injects a default empty code cell # on first room init, so baseline = N_inserts + 1. The # invariant we care about: cell_update preserves count. before = len((await call(“notebook_get”, {“path”: p}))[“cells”]) for idx, src in [(0,“A”),(2,“C”),(4,“E”),(0,“AA”),(2,“CC”)]: await call(“cell_update”, {“path”: p, “index”: idx, “source”: src}) after = len((await call(“notebook_get”, {“path”: p}))[“cells”]) assert before == after, f“count drift {before} -> {after}” print(“OK”) asyncio.run(main()) PYEOF
check command=${HOME}/.pixi/envs/default/bin/python - <<‘PYEOF’ import asyncio, json, uuid from fastmcp import Client async def main(): async with Client(“http://localhost:8888/mcp”) as c: async def call(t, a=None): r = await c.call_tool(t, a or {}) return r.data if r.data is not None else json.loads( “”.join(b.text for b in r.content if hasattr(b, “text”)) ) stem = f“check_canon{uuid.uuid4().hex[:8]}“ p = f“{stem}.ipynb“ await call(“notebook_create”, {“path”: p}) # Three different path forms — all must converge on ONE room. await call(“cell_insert”, {“path”: p, “index”: 0, “source”: “x”}) await call(“cell_insert”, {“path”: f“./{p}“, “index”: 1, “source”: “y”}) await call(“cell_insert”, {“path”: f“/workspace/{p}“, “index”: 2, “source”: “z”}) rl = await call(“room_list”) matching = [r for r in rl if stem in r.get(“path”,“”)] assert len(matching) == 1, f“expected 1 room, got {len(matching)}: {matching}” # All three inserts landed in the SAME notebook. Upstream # YNotebook auto-injects a default empty code cell on first # room init, so 3 inserts + 1 default = 4. cells = (await call(“notebook_get”, {“path”: p}))[“cells”] assert len(cells) == 4, f“expected 4 cells (3 inserts + 1 default), got {len(cells)}” print(“OK”) asyncio.run(main()) PYEOF
check command=${HOME}/.pixi/envs/default/bin/python - <<‘PYEOF’ import asyncio, json from fastmcp import Client async def main(): async with Client(“http://localhost:8888/mcp”) as c: # Host path (outside container’s /workspace) MUST be rejected. try: r = await c.call_tool(“cell_get”, { “path”: “/home/user/workspace/anything.ipynb”, “index”: 0, }) text = “”.join(b.text for b in r.content if hasattr(b, “text”)) is_error = bool(getattr(r, “is_error”, False)) if not is_error and “outside” not in text and “ValueError” not in text: raise SystemExit(f“host path accepted: {text!r}”) except Exception as e: if “outside” in str(e) or “resolves outside” in str(e): print(“OK”) return if “ValueError” in str(e): print(“OK”) return raise print(“OK”) asyncio.run(main()) PYEOF
check command=${HOME}/.pixi/envs/default/bin/python - <<‘PYEOF’ import asyncio, json, uuid from fastmcp import Client async def main(): async with Client(“http://localhost:8888/mcp”) as c: async def call(t, a=None): r = await c.call_tool(t, a or {}) return r.data if r.data is not None else json.loads( “”.join(b.text for b in r.content if hasattr(b, “text”)) ) p = f“check_cold{uuid.uuid4().hex[:8]}.ipynb“ # No prior tool to “open” the room — cell_insert auto-attaches. # Upstream YNotebook auto-injects a default empty code cell, so # baseline = 1, after 1 insert = 2. await call(“notebook_create”, {“path”: p}) await call(“cell_insert”, {“path”: p, “index”: 0, “source”: “auto”}) cells = (await call(“notebook_get”, {“path”: p}))[“cells”] sources = [c.get(“source”) for c in cells] assert any(“auto” in str(s) for s in sources), \ f“inserted cell missing: {cells}” rl = await call(“room_list”) assert any(p in r.get(“path”,“”) for r in rl), \ f“room not auto-created for {p}” print(“OK”) asyncio.run(main()) PYEOF