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