From 2c89627985ed4eb20e0950824385f4b53c3eae7e Mon Sep 17 00:00:00 2001 From: Kaden McKeen Date: Wed, 20 May 2026 21:50:59 -0400 Subject: [PATCH] fix: pin jupyter-mcp-server==0.23.0 in the .mcp.json jupyter entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_jupyter_mcp_entries()` emitted an unpinned `jupyter-mcp-server`, which `uvx` resolves to "latest". Since v1.0.0 (2026-04-03) that line makes server-startup auth mandatory: `jupyter-mcp-server` reads `JUPYTER_URL` / `JUPYTER_TOKEN` / `MCP_TOKEN` from the environment when the process starts. Claude Code spawns the server over stdio with no such env block, so on v1.0.x the process comes up but never completes the MCP handshake — the `jupyter` server hangs at "connecting" and exposes no tools. An unpinned entry therefore shipped broken-by-default. 0.23.0 is the last pre-auth release and supports the runtime `connect_to_jupyter(jupyter_url, jupyter_token)` call this integration is built on — the cluster JupyterLab URL/token rotate per compute-node session, so a static startup-env model is the wrong fit. It is the version verified against the electricrag deployment (2026-05-15). This became acute after #20: `jupyter` is now the only Jupyter MCP server, so the broken-by-default resolution has no fallback. - install.py: pin `jupyter-mcp-server==0.23.0`; full rationale in the `_jupyter_mcp_entries()` docstring plus an inline comment at the args. - docs/setup/jupyter-mcp.md: `.mcp.json` example, the `uvx` verify line, and the "Environment reference" updated to the pin; new troubleshooting row for the "hangs at connecting" symptom. - test_install.py: assert the exact pinned args. Existing consumer `.mcp.json` files keep whatever `jupyter` entry they already have (the merge is additive) — a pre-fix entry needs `==0.23.0` added by hand. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 19 ++++++++++++ src/aexp/install.py | 30 ++++++++++++++++++- .../vendor/limina/docs/setup/jupyter-mcp.md | 21 +++++++++++-- tests/test_install.py | 4 ++- 4 files changed, 69 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b7491..806aee9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 cleanup. The cluster-side `[jupyter]` extra and `aexp jupyter setup` extension recipe are unchanged. +### Fixed + +- **`aexp install --with-jupyter` now pins the `.mcp.json` `jupyter` + entry to `jupyter-mcp-server==0.23.0`.** The entry was unpinned, so + `uvx` resolved it to "latest" — currently the v1.0.x line, which + (since v1.0.0, 2026-04-03) makes server-startup auth mandatory: + `jupyter-mcp-server` reads `JUPYTER_URL` / `JUPYTER_TOKEN` / + `MCP_TOKEN` from the environment when the process starts. Claude Code + spawns the server over stdio with no such env block, so on v1.0.x the + process comes up but never completes the MCP handshake — the + `jupyter` server hangs at "connecting" and exposes no tools. 0.23.0 is + the last pre-auth release and supports the runtime + `connect_to_jupyter(jupyter_url, jupyter_token)` call this integration + is built on (the cluster URL/token rotate per session, so a + startup-env model does not fit). An unpinned entry therefore shipped + broken-by-default. Existing consumer `.mcp.json` files keep whatever + `jupyter` entry they already have (the merge is additive) — if yours + predates this fix, add `==0.23.0` to that entry's `args` by hand. + ## [0.4.0] - 2026-05-20 ### Added diff --git a/src/aexp/install.py b/src/aexp/install.py index 6dba766..8218ed0 100644 --- a/src/aexp/install.py +++ b/src/aexp/install.py @@ -599,11 +599,39 @@ def _jupyter_mcp_entries() -> dict[str, Any]: No ``.mcp.json`` edit, no MCP restart. That runtime retargeting is what makes the multi-node workflow (``/aexp-jupyter-connect`` / ``/aexp-jupyter-discover``) work. + + **Why ``jupyter-mcp-server`` is pinned to ``==0.23.0``.** + ``jupyter-mcp-server`` v1.0.0 (released 2026-04-03) made + server-startup auth mandatory: it reads ``JUPYTER_URL`` / + ``JUPYTER_TOKEN`` / ``MCP_TOKEN`` from the *environment when the + process starts*. Claude Code spawns this server over stdio with no + such env block, so on v1.0.x the process comes up but never + completes the MCP handshake — Claude Code shows the ``jupyter`` + server stuck "connecting" forever, exposing no tools. + + Moving *forward* to v1.0.x is not a fix here: the cluster JupyterLab + URL + token rotate every compute-node session, so baking them into + ``.mcp.json`` as static startup env vars is the wrong model. This + integration is built on the *runtime* ``connect_to_jupyter( + jupyter_url, jupyter_token)`` call, which the pre-auth 0.23.0 line + supports cleanly. 0.23.0 is the last release before the + mandatory-auth change and is the version verified against the + electricrag deployment (2026-05-15). + + The pin is load-bearing: an *unpinned* ``jupyter-mcp-server`` + resolves to "latest" via ``uvx`` — currently v1.0.x — so an unpinned + entry ships broken. Revisit the pin only when v1.0.x grows a + runtime-retarget path (or stdio-spawn stops requiring startup env); + if you bump it, also update the ``.mcp.json`` example and + "Environment reference" in ``docs/setup/jupyter-mcp.md``. """ return { "jupyter": { "command": "uvx", - "args": ["jupyter-mcp-server"], + # Pinned deliberately -- v1.0.x's mandatory startup-env auth + # hangs the MCP stdio handshake. Full rationale in the + # docstring above; do not unpin without re-verifying. + "args": ["jupyter-mcp-server==0.23.0"], }, } diff --git a/src/aexp/vendor/limina/docs/setup/jupyter-mcp.md b/src/aexp/vendor/limina/docs/setup/jupyter-mcp.md index f5cb9ab..5fb1683 100644 --- a/src/aexp/vendor/limina/docs/setup/jupyter-mcp.md +++ b/src/aexp/vendor/limina/docs/setup/jupyter-mcp.md @@ -174,7 +174,7 @@ pip install uv # Verify uvx can fetch jupyter-mcp-server (does NOT support --help cleanly, # but a non-error exit means it's installed) -uvx jupyter-mcp-server --help # may exit with usage; that's fine +uvx jupyter-mcp-server==0.23.0 --help # may exit with usage; that's fine ``` After `aexp install --with-jupyter` your `.mcp.json` will include the @@ -185,7 +185,7 @@ After `aexp install --with-jupyter` your `.mcp.json` will include the "mcpServers": { "jupyter": { "command": "uvx", - "args": ["jupyter-mcp-server"] + "args": ["jupyter-mcp-server==0.23.0"] } } } @@ -195,6 +195,20 @@ No token or URL is baked into the entry — the agent supplies them at runtime via `connect_to_jupyter` (see "Per-session: connect from the laptop" below). +> **Why the `==0.23.0` pin?** `jupyter-mcp-server` v1.0.0 (2026-04-03) +> made server-startup auth mandatory — the process reads `JUPYTER_URL` / +> `JUPYTER_TOKEN` / `MCP_TOKEN` from the environment *when it starts*. +> Claude Code spawns this server over stdio with no such env block, so +> on v1.0.x the process comes up but never completes the MCP handshake: +> the `jupyter` server hangs at "connecting" and exposes no tools. +> 0.23.0 is the last pre-auth release, and it supports the runtime +> `connect_to_jupyter(jupyter_url, jupyter_token)` call this whole +> integration is built around (the cluster URL/token rotate per +> session, so a startup-env model is the wrong fit). The pin is +> load-bearing — an *unpinned* entry resolves to latest = v1.0.x = +> broken. `aexp install --with-jupyter` writes the pin for you; don't +> drop it without re-verifying the handshake against a newer release. + ## Per-session: launch JupyterLab on the cluster Use whatever batch launcher you have for JupyterLab. The launcher should @@ -349,6 +363,7 @@ additive follow-up — open an issue. | Symptom | Likely cause | Fix | |---|---|---| +| The `jupyter` MCP server hangs at "connecting" in Claude Code and never exposes any tools | `jupyter-mcp-server` resolved to v1.0.x, which requires startup-env auth (`JUPYTER_URL`/`JUPYTER_TOKEN`/`MCP_TOKEN`) that a stdio spawn can't supply — the MCP handshake never completes | Pin the `.mcp.json` `jupyter` entry to `jupyter-mcp-server==0.23.0` (the last pre-auth release). `aexp install --with-jupyter` writes this pin by default — seeing this symptom means the pin was removed, or the `.mcp.json` predates it. | | **"File ID error: ... cannot be opened because its file ID could not be retrieved"** when opening any notebook in JupyterLab UI. Server log shows `404 POST /api/fileid/index` and `404 GET /jupyter-server-documents/get-example` | Frontend labextension `@jupyter-ai-contrib/server-documents` is calling Datalayer-private routes that no longer exist after we disabled the server-side `jupyter_server_documents` | `jupyter labextension disable @jupyter-ai-contrib/server-documents`, then restart the lab process | | `execute_cell` returns `jupyter_server_nbmodel extension not found. Please install it.` | `jupyter_server_nbmodel` is disabled (you may have over-corrected if you were following an older draft of this doc) | `jupyter server extension enable jupyter_server_nbmodel`, then restart the lab process | | `404 Not Found for url: http://.../api/collaboration/session/...` | `jupyter_server_ydoc` extension disabled | `jupyter server extension enable jupyter_server_ydoc`, then restart the lab process | @@ -536,7 +551,7 @@ snapshot. | Tool | Version | Notes | |---|---|---| | `uv` | latest | Used by `uvx jupyter-mcp-server` for the laptop-side MCP server | -| `jupyter-mcp-server` | 1.0.2+ | fetched ephemerally by `uvx`; not permanently installed | +| `jupyter-mcp-server` | 0.23.0 (pinned) | fetched ephemerally by `uvx`; pinned in `.mcp.json` — v1.0.x's mandatory startup-env auth hangs the MCP stdio handshake | ### Cluster server endpoint (when Jupyter is running) diff --git a/tests/test_install.py b/tests/test_install.py index 0333f52..2e73fe6 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -770,7 +770,9 @@ def test_install_with_jupyter_writes_mcp_entries(fresh_git_repo: Path) -> None: assert "aexp" in servers assert "jupyter" in servers assert servers["jupyter"]["command"] == "uvx" - assert "jupyter-mcp-server" in servers["jupyter"]["args"] + # jupyter-mcp-server is pinned: v1.0.x's mandatory startup-env auth + # hangs the MCP stdio handshake (see _jupyter_mcp_entries docstring). + assert servers["jupyter"]["args"] == ["jupyter-mcp-server==0.23.0"] assert "jupyter-compute" not in servers