Skip to content

Commit 3289e4c

Browse files
KadenMcclaude
andcommitted
refactor: drop jupyter-compute from the Jupyter MCP integration
`aexp install --with-jupyter` wired two near-duplicate Jupyter MCP servers into a consumer's `.mcp.json`: `jupyter` (uvx jupyter-mcp-server, MCP_SERVER mode, runtime-retargetable via `connect_to_jupyter`) and `jupyter-compute` (an npx mcp-remote proxy to a fixed cluster `/mcp` endpoint, JUPYTER_SERVER mode). `jupyter-compute` could not retarget to a different node without a config edit + MCP restart, which breaks the multi-node workflow the slash commands are built around. It was otherwise a near-duplicate of `jupyter` and a standing "which server do I use?" confusion surface. Reduce the integration to a single server, `jupyter`: - install.py: `_jupyter_mcp_entries()` emits only the `jupyter` entry; the additive merge no longer special-cases `jupyter-compute`. - Slash commands `/aexp-jupyter-iterate` and `/aexp-promote-nb`, `AGENTS.md`, and `docs/setup/jupyter-mcp.md` retargeted to the single-server `mcp__jupyter__*` tool family. - The two `jupyter-mcp-tools` UI tools are lost: `notebook_run-all-cells` was already 404-broken upstream; `notebook_get-selected-cell` is gone, so the affected commands now ask the user for the notebook/cell or use `aexp.jupyter.init().attached_notebooks`. The cluster-side `[jupyter]` pip extra and `aexp jupyter setup` extension recipe are intentionally unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b2de5bc commit 3289e4c

9 files changed

Lines changed: 206 additions & 293 deletions

File tree

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
12+
- **The Jupyter MCP integration is now a single MCP server.** `aexp
13+
install --with-jupyter` writes only the `jupyter` server entry to
14+
`.mcp.json` (laptop-side `uvx jupyter-mcp-server` in MCP_SERVER mode,
15+
runtime-retargetable to any node via `connect_to_jupyter`). The second
16+
`jupyter-compute` server — an `npx mcp-remote` proxy to a cluster
17+
`/mcp` endpoint (JUPYTER_SERVER mode) — is no longer emitted. It was a
18+
near-duplicate of `jupyter`, could not retarget to a different node
19+
without a `.mcp.json` edit + MCP restart, and was a standing
20+
"which server do I use?" confusion surface.
21+
- The `/aexp-jupyter-iterate` and `/aexp-promote-nb` slash commands,
22+
`AGENTS.md`, and `docs/setup/jupyter-mcp.md` are updated to the
23+
single-server tool family (`mcp__jupyter__*`).
24+
- **Lost capability:** the two `jupyter-mcp-tools` UI-delegated tools.
25+
`notebook_run-all-cells` was already 404-broken upstream;
26+
`notebook_get-selected-cell` ("which cell is the user looking at")
27+
is genuinely gone — the affected slash commands now ask the user for
28+
the notebook/cell or use `aexp.jupyter.init().attached_notebooks`.
29+
- A consumer `.mcp.json` written by an earlier `--with-jupyter`
30+
install keeps its `jupyter-compute` entry (the merge is
31+
additive-only and never deletes servers); remove it by hand for the
32+
cleanup. The cluster-side `[jupyter]` extra and `aexp jupyter setup`
33+
extension recipe are unchanged.
34+
1035
## [0.4.0] - 2026-05-20
1136

1237
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ The design bet: agents already know how to run experiments. What they need is a
124124
| **Slash commands** | Artifact creation: `/aexp-new-hypothesis`, `/aexp-new-experiment`, `/aexp-new-run`. Threads (forward-looking research concerns broader than a hypothesis): `/aexp-new-thread`, `/aexp-list-threads`, `/aexp-show-thread`, `/aexp-close-thread`. Finding creation (pick by what the finding cites): `/aexp-finding-from-run`, `/aexp-finding-from-batch`, `/aexp-finding-placeholder`. Read / inspect: `/aexp-show-run`, `/aexp-show-batch`, `/aexp-list-runs`, `/aexp-status`, `/aexp-validate`. Queue: `/aexp-queue-add`, `/aexp-queue-list`, `/aexp-queue-materialize`, `/aexp-queue-stop`. Notebook lifecycle (when `--with-jupyter` is configured): `/aexp-jupyter-iterate` (test loop), `/aexp-promote-nb` (promote working cells into a tracked-run script). Sandbox scaffolding: `/aexp-new-sandbox` (create an exploratory notebook subdir under `notebooks/_sandbox/`). 22 total. |
125125
| **CLI** | 22 verbs covering install, artifact creation (H/E/F/T + thread lifecycle), run lifecycle, batch queries, tracker binding, validation, offline sync, optional `jupyter-setup`, the `queue` subcommand group (add/list/remove/stop/clear/materialize/run) + `run-queued`, and sandbox scaffolding (`new-sandbox`). See `aexp --help` for the full list. Python API is a one-line `from aexp import ...`. |
126126
| **Typed JSON contracts** | Pydantic models (`RunLink`, `BatchSelector`, `Issue`, …) back the schema; MCP tools and CLI return the same shapes. |
127-
| **Jupyter MCP integration** (optional, `[jupyter]` extra) | `aexp install --with-jupyter` adds `jupyter` and `jupyter-compute` MCP servers to `.mcp.json` so Claude can read/edit/execute cells in a remote JupyterLab through an existing SSH tunnel — no agent SSH required. `aexp jupyter-setup` applies the verified Jupyter Server extension state on the cluster (disable Datalayer experiments that conflict with the mainstream stack). After install, see `docs/setup/jupyter-mcp.md` for cluster-side recipe + investigation log. The `/aexp-jupyter-iterate` slash command guides the read → propose → execute loop. |
127+
| **Jupyter MCP integration** (optional, `[jupyter]` extra) | `aexp install --with-jupyter` adds the `jupyter` MCP server to `.mcp.json` so Claude can read/edit/execute cells in a remote JupyterLab through an existing SSH tunnel — no agent SSH required. The target Jupyter is set per-session at runtime via `connect_to_jupyter`, so one entry retargets to any node. `aexp jupyter-setup` applies the verified Jupyter Server extension state on the cluster (disable Datalayer experiments that conflict with the mainstream stack). After install, see `docs/setup/jupyter-mcp.md` for cluster-side recipe + investigation log. The `/aexp-jupyter-iterate` slash command guides the read → propose → execute loop. |
128128

129129
### Exploratory surfaces
130130

src/aexp/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,8 +249,8 @@ def install(
249249
False,
250250
"--with-jupyter",
251251
help=(
252-
"Opt into the Jupyter MCP integration: writes `jupyter` and "
253-
"`jupyter-compute` server entries to `.mcp.json`, sets "
252+
"Opt into the Jupyter MCP integration: writes the `jupyter` "
253+
"server entry to `.mcp.json`, sets "
254254
"`jupyter_enabled: true` (sticky) in the install marker, and "
255255
"ensures `docs/setup/jupyter-mcp.md` is vendored. Requires "
256256
"`pip install agentic-experiments[jupyter]` for the Python "

src/aexp/install.py

Lines changed: 28 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -481,11 +481,11 @@ def _merge_or_write_mcp_json(
481481
current interpreter instead — lets editable installs take effect on
482482
the MCP side (at the cost of a machine-specific ``.mcp.json``).
483483
484-
When ``with_jupyter=True``, also writes the ``jupyter`` and
485-
``jupyter-compute`` entries used by the Jupyter MCP integration. The
486-
entries are *additive*: once written, subsequent installs without the
487-
flag leave them in place (matching the "never delete user-defined
488-
servers" pattern). To back out, the user edits ``.mcp.json`` by hand.
484+
When ``with_jupyter=True``, also writes the ``jupyter`` entry used by
485+
the Jupyter MCP integration. The entry is *additive*: once written,
486+
subsequent installs without the flag leave it in place (matching the
487+
"never delete user-defined servers" pattern). To back out, the user
488+
edits ``.mcp.json`` by hand.
489489
"""
490490
rel = _display_relpath(dst)
491491
our_entries: dict[str, Any] = {"aexp": _build_mcp_server_entry(repo_root, dev=dev)}
@@ -517,14 +517,12 @@ def _merge_or_write_mcp_json(
517517
merged.setdefault("mcpServers", {})
518518
# Always refresh our own ``aexp`` entry; preserve any user-defined servers.
519519
merged["mcpServers"]["aexp"] = our_entries["aexp"]
520-
# Jupyter entries: only ever ADD. If the user already has a `jupyter` /
521-
# `jupyter-compute` block (either from a prior --with-jupyter install or
522-
# from a manual setup) leave it alone — they may have hardcoded the
523-
# Windows-stable token there, which we must not clobber.
524-
if with_jupyter:
525-
for key in ("jupyter", "jupyter-compute"):
526-
if key not in merged["mcpServers"]:
527-
merged["mcpServers"][key] = our_entries[key]
520+
# Jupyter entry: only ever ADD. If the user already has a `jupyter`
521+
# block (from a prior --with-jupyter install or a manual setup) leave
522+
# it alone — they may have customized the URL/port or pinned a
523+
# version, which we must not clobber.
524+
if with_jupyter and "jupyter" not in merged["mcpServers"]:
525+
merged["mcpServers"]["jupyter"] = our_entries["jupyter"]
528526

529527
if merged == existing:
530528
return InstallAction("skipped_identical", rel)
@@ -588,49 +586,25 @@ def _build_mcp_server_entry(repo_root: Path, *, dev: bool = False) -> dict[str,
588586

589587

590588
def _jupyter_mcp_entries() -> dict[str, Any]:
591-
"""MCP server entries for the Jupyter MCP integration.
589+
"""MCP server entry for the Jupyter MCP integration.
592590
593-
Two side-by-side servers, both reaching the same JupyterLab through the
594-
user's existing SSH tunnel:
591+
A single laptop-side server:
595592
596593
- ``jupyter`` — laptop-side ``uvx jupyter-mcp-server`` running in
597-
MCP_SERVER mode (stdio to Claude, HTTP+WS to remote Jupyter). The
598-
token is provided per-session at runtime via the ``connect_to_jupyter``
599-
tool, so no token lives in this entry.
600-
- ``jupyter-compute`` — laptop-side ``npx mcp-remote`` proxy bridging
601-
Claude's stdio to the cluster's ``/mcp`` SSE endpoint, where
602-
``jupyter-mcp-server`` runs as a Jupyter Server extension
603-
(JUPYTER_SERVER mode). Token is interpolated from the
604-
``JUPYTER_TOKEN`` env var by default.
605-
606-
Default port is ``3618`` (matches the verified electricrag deployment).
607-
Consumers using a different port edit ``.mcp.json`` post-install.
608-
609-
On Windows, ``${JUPYTER_TOKEN}`` interpolation is fragile because
610-
``setx`` does not propagate to already-running processes (notably
611-
Explorer, which spawns Start-Menu apps including Claude Desktop). The
612-
documented fix is to hardcode the literal token in ``.mcp.json`` and
613-
set the matching value in ``~/.jupyter/jupyter_server_config.py`` on
614-
the cluster — see ``docs/setup/jupyter-mcp.md`` "Investigation log §4".
615-
The install never auto-rewrites the literal token: token management
616-
stays the consumer's responsibility.
594+
MCP_SERVER mode (stdio to Claude, HTTP+WS to the remote Jupyter).
595+
The target Jupyter URL + token are supplied per-session at runtime
596+
via the ``connect_to_jupyter`` tool, so no token lives in this
597+
entry and the *same* entry retargets to any node — open a tunnel on
598+
a new local port, call ``connect_to_jupyter`` at the new URL, done.
599+
No ``.mcp.json`` edit, no MCP restart. That runtime retargeting is
600+
what makes the multi-node workflow (``/aexp-jupyter-connect`` /
601+
``/aexp-jupyter-discover``) work.
617602
"""
618603
return {
619604
"jupyter": {
620605
"command": "uvx",
621606
"args": ["jupyter-mcp-server"],
622607
},
623-
"jupyter-compute": {
624-
"command": "npx",
625-
"args": [
626-
"-y",
627-
"mcp-remote",
628-
"http://127.0.0.1:3618/mcp",
629-
"--allow-http",
630-
"--header",
631-
"Authorization:token ${JUPYTER_TOKEN}",
632-
],
633-
},
634608
}
635609

636610

@@ -841,14 +815,13 @@ def install_limina(
841815
override (e.g. dogfooding the consumer scaffold against the
842816
dev repo on purpose).
843817
with_jupyter : bool, optional
844-
If ``True``, also write the ``jupyter`` and ``jupyter-compute``
845-
MCP server entries into ``.mcp.json``, vendor
846-
``docs/setup/jupyter-mcp.md`` into the consumer repo, and set
847-
``jupyter_enabled: true`` in the install marker. The marker bit
848-
is sticky — once set, subsequent installs preserve it even if
849-
``with_jupyter=False``. The ``.mcp.json`` entries are additive:
850-
existing user-defined ``jupyter`` / ``jupyter-compute`` blocks
851-
are preserved (so a hardcoded Windows-stable token survives).
818+
If ``True``, also write the ``jupyter`` MCP server entry into
819+
``.mcp.json``, vendor ``docs/setup/jupyter-mcp.md`` into the
820+
consumer repo, and set ``jupyter_enabled: true`` in the install
821+
marker. The marker bit is sticky — once set, subsequent installs
822+
preserve it even if ``with_jupyter=False``. The ``.mcp.json``
823+
entry is additive: an existing user-defined ``jupyter`` block is
824+
preserved (so a customized URL/port survives).
852825
See ``docs/setup/jupyter-mcp.md`` for the full setup recipe.
853826
854827
Returns

src/aexp/slash_commands/aexp-jupyter-iterate.md

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@
22
description: "Iterate on a JupyterLab cell with the user via the Jupyter MCP bridge (read → propose → execute)."
33
---
44

5-
Iterate on whatever cell the user is currently looking at in JupyterLab,
6-
through the Jupyter MCP bridge.
5+
Iterate on a notebook cell with the user, through the Jupyter MCP bridge.
76

8-
> **Prerequisite.** This command requires the `mcp__jupyter-compute__*`
9-
> tool family. Those tools come from `aexp install --with-jupyter` plus a
10-
> JupyterLab process reachable through the user's SSH tunnel. If the
11-
> tools are missing, run `/mcp` to inspect server status and consult
12-
> `docs/setup/jupyter-mcp.md` for the cluster-side setup recipe.
7+
> **Prerequisite.** This command requires the `mcp__jupyter__*` tool
8+
> family — in particular `connect_to_jupyter`, `execute_code`,
9+
> `read_cell`, and `execute_cell`. Those tools come from
10+
> `aexp install --with-jupyter` plus a JupyterLab process reachable
11+
> through the user's SSH tunnel. If the tools are missing, run `/mcp` to
12+
> inspect server status and consult `docs/setup/jupyter-mcp.md` for the
13+
> setup recipe.
1314
1415
Run through these steps:
1516

16-
0. **Confirm session identity.** Before touching any cells, dispatch:
17+
1. **Check tool availability.** Verify that `mcp__jupyter__execute_cell`
18+
and `mcp__jupyter__read_cell` are present in your tool list. If not,
19+
stop and report:
20+
"Jupyter MCP integration not available in this session. Run
21+
`aexp install --with-jupyter`, ensure the SSH tunnel to the cluster is
22+
open, connect with `/aexp-jupyter-connect`, and restart Claude. See
23+
`docs/setup/jupyter-mcp.md`."
24+
25+
2. **Confirm session identity.** Before touching any cells, dispatch:
1726
```
1827
execute_code(code="from aexp.jupyter import init; import json; print(json.dumps(init().model_dump(), default=str))")
1928
```
@@ -27,46 +36,43 @@ Run through these steps:
2736
shouldn't disturb.
2837

2938
If anything mismatches — wrong SLURM job, wrong host, unexpected GPU
30-
resident — STOP and ask. Do not proceed to step 1.
39+
resident — STOP and ask. Do not proceed to step 3. To switch to a
40+
different Jupyter, use `/aexp-jupyter-connect`.
3141

32-
1. **Check tool availability.** Verify that
33-
`mcp__jupyter-compute__notebook_get-selected-cell` and
34-
`mcp__jupyter-compute__execute_cell` are present in your tool list. If
35-
not, stop and report:
36-
"Jupyter MCP integration not available in this session. Run
37-
`aexp install --with-jupyter`, ensure the SSH tunnel to the cluster is
38-
open, and restart Claude Desktop. See `docs/setup/jupyter-mcp.md`."
42+
3. **Identify the target notebook and cell.** This single-server setup
43+
has no live "what is the user looking at" tool, so ask the user
44+
directly:
45+
"Which notebook should I work in, and which cell — give me the cell
46+
index, or describe it (e.g. 'the training loop')?"
47+
Cross-check the notebook name against the `attached_notebooks` list
48+
from step 2. Open it with `use_notebook` if it isn't already open.
3949

40-
2. **Identify what the user is looking at.** Call
41-
`mcp__jupyter-compute__notebook_get-selected-cell` to read the live UI
42-
selection. Report the notebook path, cell index, and cell type, and
43-
quote the source verbatim so the user can confirm you're targeting the
44-
right cell.
50+
4. **Locate and quote the cell.** Use `read_cell(cell_index=N)` for a
51+
single cell, or `read_notebook` (brief mode) to find the cell the
52+
user described. Report the notebook path, cell index, and cell type,
53+
and quote the source verbatim so the user can confirm you're
54+
targeting the right cell before any edit.
4555

46-
3. **Gather context if needed.** If the user's request references "the
47-
cells above" or relies on prior state, use
48-
`mcp__jupyter-compute__read_cell` on adjacent indices, or
49-
`mcp__jupyter-compute__read_notebook` (brief mode) for an overview.
50-
Don't dump the whole notebook unless asked.
56+
5. **Gather context if needed.** If the user's request references "the
57+
cells above" or relies on prior state, use `read_cell` on adjacent
58+
indices, or `read_notebook` (brief mode) for an overview. Don't dump
59+
the whole notebook unless asked.
5160

52-
4. **Propose a change.** Describe what you intend to modify and why.
61+
6. **Propose a change.** Describe what you intend to modify and why.
5362
Do NOT make the edit until the user confirms.
5463

55-
5. **On approval, apply the edit.** Use:
64+
7. **On approval, apply the edit.** Use:
5665
- `edit_cell_source` for surgical find/replace within one cell.
5766
- `overwrite_cell_source` for full replacement.
5867
- `insert_cell` to add a new cell.
5968

60-
6. **Execute the cell.** Call
61-
`mcp__jupyter-compute__execute_cell(cell_index=N)` with a reasonable
62-
timeout. Paste the actual stdout / errors verbatim — don't paraphrase.
69+
8. **Execute the cell.** Call `execute_cell(cell_index=N)` with a
70+
reasonable timeout. Paste the actual stdout / errors verbatim — don't
71+
paraphrase.
6372

64-
7. **Iterate or wrap up.** If the cell errored, propose the next fix and
65-
loop back to step 4. If it succeeded, ask the user whether to continue
66-
or stop.
73+
9. **Iterate or wrap up.** If the cell errored, propose the next fix and
74+
loop back to step 6. If it succeeded, ask the user whether to
75+
continue or stop.
6776

68-
> **Do NOT use** `notebook_run-all-cells` — it is exposed by the bridge
69-
> but currently returns 404 (asymmetric upstream bug, see
70-
> `docs/setup/jupyter-mcp.md` "Investigation log" §5). Loop
71-
> `execute_cell(cell_index=i)` over indices instead when a multi-cell run
72-
> is needed.
77+
> **Multi-cell runs.** To run a span of cells, loop
78+
> `execute_cell(cell_index=i)` over the indices in order.

src/aexp/slash_commands/aexp-promote-nb.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@ committed experiment.
1313
> The notebook stays as the smoke-test record — it's not edited, only
1414
> read. Outputs land at `<repo_root>/experiments/E<id>-<slug>.py`.
1515
16-
> **Prerequisite.** Best with `mcp__jupyter-compute__*` tools available
16+
> **Prerequisite.** Best with the `mcp__jupyter__*` tools available
1717
> (from `aexp install --with-jupyter`). If they're not, you can still
1818
> promote cells from a `.ipynb` file on disk via the standard Read tool;
19-
> you'll lose the live cell-selection convenience but everything else
20-
> works.
19+
> everything in this command works either way.
2120
2221
> **Invocation note.** The examples below use `python -m aexp` directly.
2322
> If running from a Claude Code session where `python` does not resolve
@@ -33,25 +32,26 @@ committed experiment.
3332
3433
Run through these steps:
3534

36-
1. **Tool availability check.** Verify whether
37-
`mcp__jupyter-compute__notebook_get-selected-cell` and
38-
`mcp__jupyter-compute__read_cell` are present. If yes, use them in
39-
the steps below. If not, ask the user for the path to the `.ipynb`
40-
file on disk and read it via the standard Read tool — JupyterLab
41-
notebooks are JSON; you can extract `cells[i].source` directly.
35+
1. **Tool availability check.** Verify whether the `mcp__jupyter__*`
36+
tools (`read_cell`, `read_notebook`, `use_notebook`) are present. If
37+
yes, use them in the steps below. If not, ask the user for the path
38+
to the `.ipynb` file on disk and read it via the standard Read tool —
39+
JupyterLab notebooks are JSON; you can extract `cells[i].source`
40+
directly.
4241

43-
2. **Identify the source notebook.** With the MCP bridge: call
44-
`mcp__jupyter-compute__notebook_get-selected-cell` to anchor on the
45-
user's current focus and report the notebook path back to them. Without
46-
the bridge: ask the user for the notebook path explicitly.
42+
2. **Identify the source notebook.** Ask the user for the notebook path
43+
explicitly. With the MCP bridge, cross-check it against the open
44+
notebooks (`aexp.jupyter.init().attached_notebooks`) and open it with
45+
`use_notebook` if it isn't already open. Without the bridge, take the
46+
path the user gives you.
4747

4848
3. **Identify the cell range to promote.** Ask:
49-
"promote just the currently-selected cell, or a range? if a range,
50-
give me indices (e.g., `4-12`) or describe the cells (e.g., 'from the
51-
model definition through the training loop')." Read each target cell
52-
(`read_cell(cell_index=N)` via MCP, or by indexing into
53-
`cells[]` from the on-disk JSON). Quote the source verbatim back to
54-
the user and confirm the selection before going further.
49+
"which cells should I promote — give me indices (e.g., `4-12`) or
50+
describe the cells (e.g., 'from the model definition through the
51+
training loop')." Read each target cell (`read_cell(cell_index=N)`
52+
via MCP, or by indexing into `cells[]` from the on-disk JSON). Quote
53+
the source verbatim back to the user and confirm the selection before
54+
going further.
5555

5656
4. **Identify the experiment.** Ask which `E###` this script is being
5757
promoted under. If the user is unsure, suggest checking

0 commit comments

Comments
 (0)