Skip to content

notebook_run_cell — read-run-publish in one MCP call#15

Merged
junjihashimoto merged 2 commits into
mainfrom
feature/notebook-run-cell
Jun 15, 2026
Merged

notebook_run_cell — read-run-publish in one MCP call#15
junjihashimoto merged 2 commits into
mainfrom
feature/notebook-run-cell

Conversation

@junjihashimoto

Copy link
Copy Markdown
Contributor

Summary

Adds notebook_run_cell(ipynb_path, cell_index) to xlean-mcp. It collapses the four-step recipe an MCP host needs to make a kernel-side computation visible in the user's open JupyterLab tab WITHOUT a page reload, all in one call:

  1. Read the cell source from the .ipynb at (path, index).
  2. Execute it on the live xeus-lean kernel via the existing KernelBridge.
  3. Embed outputs back into the .ipynb via Jupyter Server's PUT /api/contents/<path> so jupyter-collaboration's Y.js doc store hears the update and pushes the diff to every connected browser client live.
  4. jupyter trust the file so the browser's per-session trust gate doesn't quarantine image/svg+xml / text/html payloads as untrusted (the cause of "the cell ran but my SVG only shows after F5" pain).

MIME-bundle splitting

A display_data whose data carries BOTH text/plain and image/svg+xml (the shape Display.waveform emits — log + render in one bundle) gets rendered by JupyterLab as the SVG ONLY: the renderer picks the highest-priority MIME and treats the others as fallbacks. The agent sees the SVG appear but the build log silently disappears.

outputToCells splits multi-key bundles into one display_data per MIME type, so both render. Enumerated against the MIME types xeus-lean's Display.* actually emits (text/plain, text/html, image/svg+xml, image/png, image/jpeg, text/markdown, text/latex, application/json, jupyter-widget-view) rather than iterating Json.obj's underlying TreeMap.Raw (whose API moves between Lean versions).

Drive-by fix in Net/HTTP.lean

Content-Length was being computed from body.length — Lean's character count, which under-reports when the body contains the multi-byte UTF-8 characters Lean writes (, , , λ, …). Jupyter Server then read a truncated body and rejected the PUT with 400: Invalid JSON in body of request. Switching to body.toUTF8.size gives byte-correct framing.

Test plan

  • tools/list shows notebook_run_cell
  • tools/call notebook_run_cell against a cell whose source draws a Sparkle JIT waveform:
    • returns outputs_written ≥ 2 (split worked)
    • browser tab shows BOTH the build-log text/plain AND the SVG, live, no reload
  • After the call, the .ipynb file on disk has metadata.signature set (jupyter trust ran)
  • PUT body with multi-byte UTF-8 chars ( in let count ← Signal.reg 0#4) survives Content-Length framing

Adds a `notebook_run_cell` MCP tool that combines the four-step
recipe an agent needs to make a kernel-side computation visible
in the user's open JupyterLab tab without a reload:

  1. Read the cell at (path, index) from the .ipynb.
  2. Send its source through `KernelBridge.execute` to the live
     xeus-lean kernel.
  3. Embed the resulting outputs back into the cell via Jupyter
     Server's `PUT /api/contents/<path>` (so
     jupyter-collaboration's Y.js doc store hears the update
     and pushes the diff to every connected browser live).
  4. Shell out to `jupyter trust` so the browser's per-session
     trust gate doesn't quarantine `image/svg+xml` / `text/html`
     payloads as untrusted.

Multi-key MIME bundles (a single `display_data` carrying both
`text/plain` and `image/svg+xml`, as `Display.waveform` does)
get split into one output per MIME type, because JupyterLab
picks the highest-priority renderer within a bundle and hides
the others.  Without the split, an agent that prints a build
log AND draws a waveform only sees the waveform appear; with
the split both render.

Drive-by: `Net/HTTP.lean`'s `Content-Length` was computed from
`body.length` (character count), which under-reports when the
body contains the multi-byte UTF-8 characters Lean writes
(`←`, `→`, `≥`, `λ`, …).  Jupyter Server then read a truncated
body and rejected the PUT as invalid JSON.  Switch to
`body.toUTF8.size` for byte-correct framing.
Add a `#load_file "/path/to/foo.lean"` command magic that reads a
Lean source file from disk and elaborates its commands into the
running kernel session's current environment.  Behaves like
pasting the file's contents into a cell:

  - definitions, theorems, instances, `open`s, `namespace`s,
    `@[extern]` declarations etc. all land in the current env
  - one error in the loaded file no longer aborts the cell — every
    `MessageLog` diagnostic is forwarded individually via
    `logInfo` / `logWarning` / `logError` so the user sees what
    landed and what didn't
  - module-header `import`s inside the loaded file are silently
    ignored because xeus-lean's REPL locks the import list to
    what was loaded at session start; we can't add new modules
    on the fly here

Internals: pipes the file content through `Lean.Elab.process`
(which is the same `IO.processCommands` loop the compiler uses
for an offline build), then merges the resulting environment
back via `setEnv`.

Pair with `file_write` (or the MCP `file_write` tool) to get a
"draft a snippet → save it → splice it into the live env" loop
without restarting the session.
@junjihashimoto junjihashimoto merged commit 4123081 into main Jun 15, 2026
6 checks passed
@junjihashimoto junjihashimoto deleted the feature/notebook-run-cell branch June 15, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant