Skip to content

MCP server: design, and step 1 (run_python over local client tokens) - #7

Merged
thinkinginmath merged 3 commits into
mainfrom
docs/mcp-server-design
Sep 22, 2026
Merged

thinkinginmath merged 3 commits into
mainfrom
docs/mcp-server-design

Conversation

@thinkinginmath

Copy link
Copy Markdown
Collaborator

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 mcp is a stdio MCP server that holds no state. It is an HTTP client of a runtime that is already serving on loopback:

MCP client ──stdio──> agent-runtime mcp ──HTTP 127.0.0.1:9477──> agent-runtime serve

This is the load-bearing decision. kernel_manager and lab_manager are 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 Origin header, 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 in local_clients.json (0600), listed and revoked from the CLI, and answering local:<name> from the new GET /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 an Origin is rejected, because it is bound to no origin.

Tokens default to the actions scope. 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 code has 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.

  • ANSI stripped — IPython colors its tracebacks, and lab actions set FORCE_COLOR=1 on purpose for the page.
  • Figures passed through as images — display_data already carries base64 image/png, so a tutor can see the learner's plot.
  • Results capped head-and-tail.
  • A traceback is a successful call. It is the thing a tutor exists to read. Only an unreachable runtime, a refusal, or an impossible lab_id fail 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, print comes back as text, state persists across calls (x = 6 * 7 then x + 1 → 43), a ValueError returns as a readable traceback with is_error false and no escape codes, and an IPython.display PNG arrives as image content. Also checked live: issuing and revoking a token while serving, and an actions-scope token getting 403 on /cell/run while /labs stays open to it.

218 tests pass; ruff and mypy are clean.

Deviations from the design, both recorded in docs/mcp.md

  • The tool list does not vary with scope. A client asks for it once, at connect time, when the runtime may not be running — a list that depends on whether the daemon happened to be up is worse than a stable one.
  • /runtime/info advertises local_clients, not mcp. That list tells a page what the runtime offers, and the MCP server is a separate process.
  • list_labs waits for step 2, because environments are not exposed over HTTP at all yet.

Noticed in passing, not fixed here

/cell/run returns outputs shaped {"type": ..., "content": {...}}, while docs/protocol.md and 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

thinkinginmath and others added 3 commits September 21, 2026 03:32
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>
@thinkinginmath
thinkinginmath merged commit a6da41b into main Sep 22, 2026
5 checks passed
@thinkinginmath
thinkinginmath deleted the docs/mcp-server-design branch September 22, 2026 08:21
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