Repository navigation
MCP server: design, and step 1 (run_python over local client tokens) - #7
Merged
Merged
Conversation
An adapter process over the localhost protocol rather than a second runtime: the kernels, prepared labs and runs are process state, so importing them twice would give a tutor its own kernels and hide the cell the learner just ran. Records the parts that are not obvious from the code: local client tokens as a third principal (a process sends no Origin, so today it cannot authenticate at all), why lab runs must not block a tool call to completion, why ANSI stripping is the adapter's job and not a change to what the browser gets, and that a code-scope token is a revocation and audit story rather than a new boundary against someone who can already run Python as the user. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Step 1 of docs/mcp.md: `agent-runtime mcp` runs a stdio MCP server exposing one tool, `run_python`. It holds no state - it is an HTTP client of a runtime that is already serving, because kernels and runs are process state and a second copy would give a tutor its own kernels, invisible to the learner's page. A process is not a browser origin, so it could not authenticate at all: no Origin header means no pairing to bind a token to. Local client tokens are that third principal, issued by `agent-runtime token create`, hashed at rest, and answering `local:<name>` from the new `GET /runtime/whoami`. They default to the `actions` scope: an agent client is a model reading untrusted course pages and tracebacks, so `--scope code` is typed deliberately. The store is re-read whenever the file changes, which a live test caught: tokens are issued by the CLI while the server runs, so without it a new token would not work until a restart - and a revoked one would keep working. What the adapter converts, because the runtime serves a browser and a model is not one: ANSI stripped from IPython's colored tracebacks, figures passed through as images, results capped head-and-tail. A traceback is a successful call - it is the thing a tutor exists to read - so only an unreachable runtime, a refusal or an impossible lab id fail the tool. The tool list does not vary with the token's scope, though the design said it would: a client asks for it once, at connect time, when the runtime may not be up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three files the formatter joins onto one line. No behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two commits: the design for an MCP server, and step 1 of it.
Why
An MCP client — Claude Code, Claude Desktop, Cursor — becomes a tutor with hands in the learner's own environment: it reads the real traceback, with the real package versions, instead of whatever a page chose to report. One adapter reaches every such client, rather than one integration each.
The shape
agent-runtime mcpis a stdio MCP server that holds no state. It is an HTTP client of a runtime that is already serving on loopback:This is the load-bearing decision.
kernel_managerandlab_managerare process globals and runs live in memory, so a second process that imported them would get its own kernels: the tutor would not see the cell the learner just ran in the browser, and two processes would race to build the same virtual environment.A third principal
A process is not a browser origin. It sends no
Originheader, so pairing has nothing to bind a token to — which meant a local process could not authenticate at all, short of--no-pairing.agent-runtime token create "Claude Desktop"issues a local client token: hashed at rest inlocal_clients.json(0600), listed and revoked from the CLI, and answeringlocal:<name>from the newGET /runtime/whoami. Two invariants keep the browser model intact — a paired site's token is not a local client token (separate stores, separate branches), and a local token sent with anOriginis rejected, because it is bound to no origin.Tokens default to the
actionsscope. The new exposure here is prompt injection, not token theft: an agent client is a model reading course pages, lab output and third-party tracebacks, and then deciding what to run.--scope codehas to be typed.A live test caught a real bug: tokens are issued by the CLI while the server runs, but the store was read once at import — so a new token did not work until a restart, and a revoked one kept working. The store now re-reads the file whenever it changes, which is why the check is on every lookup rather than only on a miss.
What the adapter converts
The runtime serves a browser; a model is not one.
FORCE_COLOR=1on purpose for the page.display_dataalready carries base64image/png, so a tutor can see the learner's plot.lab_idfail the tool, each with the command that fixes it.Verified end to end
Against a real runtime and a real kernel, driven by an MCP client over stdio: the tool lists,
printcomes back as text, state persists across calls (x = 6 * 7thenx + 1→43), aValueErrorreturns as a readable traceback withis_errorfalse and no escape codes, and anIPython.displayPNG arrives as image content. Also checked live: issuing and revoking a token while serving, and anactions-scope token getting403on/cell/runwhile/labsstays open to it.218 tests pass; ruff and mypy are clean.
Deviations from the design, both recorded in
docs/mcp.md/runtime/infoadvertiseslocal_clients, notmcp. That list tells a page what the runtime offers, and the MCP server is a separate process.list_labswaits for step 2, because environments are not exposed over HTTP at all yet.Noticed in passing, not fixed here
/cell/runreturns outputs shaped{"type": ..., "content": {...}}, whiledocs/protocol.mdand the streaming endpoint document the flatter nbformat-like{"output_type": ..., "text": ...}. The renderer accepts both; the two should be reconciled.Next
Steps 2–4 of
docs/mcp.md: the lab tools and their rendering, session history (the buffer that turns "can run code" into "knows what the learner has been doing"), and streamable HTTP if clients ask for it.🤖 Generated with Claude Code