MCP Server¶
Strata Notebook can expose a running notebook session to an external coding
agent (Claude Code, and any other Model Context
Protocol client) over an HTTP endpoint at
/mcp. The agent gets the same operations the strata CLI drives -
read, run, author, manage dependencies - but against a warm session: its
populated artifact cache and current cell state, not an offline copy.
Because the tools reuse Strata's broadcasting execution paths, the browser UI and the terminal viewer double as a live view of the agent at work - cells flip status, outputs render, and new cells appear as the agent edits them.
Enable it¶
The endpoint is off by default. On a personal server there is one user and nothing to check, so a caller has full control of the session. Keep it behind loopback.
On a service-mode server it needs principal auth (auth_mode of
trusted_proxy or api_key). Service mode without principal auth is rejected
at startup when the flag is set. Each tool call then runs as the caller its HTTP
request names. Authorship, team-store attribution and the registry audit record
that caller, and the call is checked against the same notebook scopes as the
REST routes and the WebSocket:
| Scope | Tools |
|---|---|
notebook:read |
list_notebooks, get_notebook, get_cell, save_cell_output, get_variable, dag, status, list_workers, lineage, publish_preflight |
notebook:write |
add_cell, edit_cell, remove_cell, move_cell, note, add_worker, set_default_worker, set_variant, remove_worker, disconnect_ssh_worker, promote |
notebook:execute |
run_cell, cancel_cell, run_tests, run_snippet, set_widget_value, add_dependency, remove_dependency, connect_ssh_worker, and any tool not listed |
artifacts:publish |
publish |
Within a tenant, every open session is visible to any caller holding
notebook:read: the check is scopes, not ownership. A session records the
tenant of the caller who opened or created it, and a caller from another tenant
neither sees it in list_notebooks nor reaches it by session_id, as with
tenant-scoped artifacts. admin:* sees every tenant's sessions, and a caller
the proxy sends without a tenant is the default tenant (_default), so its
sessions are visible only to other tenantless callers and to admin:*. The
notebook REST routes and the WebSocket apply the same rule.
Then register it with your agent. For Claude Code:
An agent on another machine uses the server's name or address instead
(http://devbox.lan:8765/mcp). /mcp answers to the same Host names as
the rest of the server: localhost, any IP literal, STRATA_HOST and the
names in STRATA_ALLOWED_HOSTS (see
Deployment modes). Any other Host
is refused (in service mode, only once STRATA_ALLOWED_HOSTS is set). A request from a browser page must also be same-origin or come
from an origin in STRATA_CORS_ALLOW_ORIGINS.
The [mcp] extra needs mcp 2.2 or newer. If the flag is set but the extra is
not installed, the server logs a warning and starts normally without the
endpoint. With mcp 1 installed (another tool may pin it), /mcp stays off
the same way and the server logs that the installed mcp is too old.
One-command setup
strata agent <notebook-dir> does the enable-open-register-watch
steps below for you: it starts the server with this endpoint on, opens a
session, writes the .mcp.json an agent auto-connects to, and attaches the
TUI. Reach for it unless you want to wire the pieces up by hand.
Workflow¶
Sessions are opened by the notebook UI or the CLI; the MCP tools operate on sessions that are already open (they do not open notebooks from a path). The typical loop:
- You open a notebook in the browser (or with
strata). list_notebooks→ the agent gets thesession_id.- The agent inspects (
get_notebook/get_cell/dag/status), edits (add_cell/edit_cell/ …), runs (run_cell/run_tests, andcancel_cellto stop one), and manages dependencies (add_dependency/remove_dependency) - all against that session, while you watch it happen in the browser or the TUI.
A call that fails for a reason the agent can act on (an unknown session or
cell, a bad argument, a busy notebook) returns that reason in the tool error,
for example no open notebook session '…'; call list_notebooks first. An
unexpected server failure returns only Error executing tool <name>; the
details stay in the server log.
Tools¶
| Tool | Description |
|---|---|
list_notebooks |
The sessions currently open on the server: session_id, name, path. |
get_notebook(session_id) |
Every cell of a session, in order. |
get_cell(session_id, cell_id) |
One cell: source, status, outputs. For a widget cell, its controls and what each is set to. |
save_cell_output(session_id, cell_id, index=-1) |
Write a display output (a plot, an image) to the notebook's .strata/outputs/ and return the path, so the agent can open it. The path is on the server's machine: an agent on another host should use strata cell output --server … --session …, which downloads and writes the file locally. |
get_variable(session_id, name) |
The cell that defines a variable, "do I already have name?"; else the available names. For a swept variable, each variant's cell and the name lineage takes for it. |
set_variant(session_id, group, active?, mode?) |
Pick which variant of a group runs, or set the group to switch / sweep. |
dag(session_id) |
The dependency graph - edges, topological order, roots, leaves. |
status(session_id) |
Per-cell status + staleness summary. |
run_cell(session_id, cell_id, mode) |
Execute a cell (normal / rerun / force), broadcast live. |
cancel_cell(session_id, cell_id) |
Cancel the cell's run, whoever started it, as the browser's stop button does: the run stops and the cell is left idle. A run_cell waiting on it returns status: error with error_code: cancelled. Returns {cell_id, cancelled, status}; cancelled is false when the cell was not running. |
set_widget_value(session_id, cell_id, values) |
Set a widget cell's controls and re-run it at the new values, the same thing moving the slider does. |
run_tests(session_id, cell_id) |
Run a cell's cells/{id}.test.py. |
add_cell(session_id, source, after?, language?, author?) |
Add a cell (server mints the id). language is one of python, markdown, sql, r, prompt, widget. |
run_snippet(session_id, source, after?, language?, author?) |
Add a cell and run it in one call; returns the cell view with the run outcome nested under run. The scratchpad primitive. |
edit_cell(session_id, cell_id, source, author?) |
Replace a cell's source. |
remove_cell(session_id, cell_id) |
Delete a cell and its files. |
move_cell(session_id, cell_id, index) |
Reorder a cell. |
add_dependency(session_id, package) |
uv add a dependency. |
remove_dependency(session_id, package) |
uv remove a dependency. |
note(session_id, message) |
Post a line into the terminal viewer's Agent tab for the human watching. |
list_workers(session_id) |
The notebook's registered workers and which is the default. |
add_worker(session_id, name, url, transport?, token_env?, runtime_id?, set_default?) |
Register a remote executor worker so cells can run on it. |
set_default_worker(session_id, name?) |
Set the notebook's default worker (name omitted/local clears it). |
remove_worker(session_id, name) |
Remove a notebook-scoped worker. |
connect_ssh_worker(session_id, ssh_target, name?, set_default?, install?) |
Provision + tunnel + register a worker on a machine you reach over SSH (see Distributed Workers). |
disconnect_ssh_worker(session_id, name, stop_remote?) |
Close an SSH worker's tunnel and unregister it. |
lineage(session_id, cell_id, variable, max_depth?) |
The chain behind one of a cell's outputs: every step with the code it ran, the environment it ran in, who computed it and the digest of its bytes. For one instance of a # @per_variant cell, name it variable@variant=<name> (for example score@variant=triple); the error on a bare name lists the stored ones. |
promote(session_id, cell_id, variable, name, alias?, tags?) |
Copy the output and everything behind it into the team's store, under a name colleagues can ask for. Mints no public link. A protected alias comes back pending. Needs STRATA_NOTEBOOK_REMOTE_STORE_URL. |
publish_preflight(session_id, cell_id, variable) |
What publishing would expose - the whole chain, step by step. Read this to the user before publish. |
publish(session_id, cell_id, variable, title?) |
Mint a URL that needs no credentials. Copies the chain into the store the link resolves from first. |
Pass the same author on every authoring call: your own name or id. It is
recorded on the cell as created_by / updated_by and shown in the cell view,
so a person opening the notebook can tell which cells an agent wrote. On a
server that authenticates its callers the authenticated identity is used
instead and author is ignored; on a personal server there is nothing to check
it against, so it is a claim rather than a fact.
promote and publish are different acts. Promoting puts a result where
colleagues' cells already read from, inside a store that still needs
credentials; publishing mints a link that needs none and exposes every upstream
step's code and environment along with the result. publish_preflight returns
that exposure list, and an agent should put it in front of the user and get
their agreement before calling publish. publish returns the token only
when it mints the link; for a version already published it is empty, with the
publication's id. Withdrawing is strata artifact unpublish <token or id>.
A widget cell declares controls; what they are set to is runtime state,
not source. Editing the cell therefore cannot change what the notebook
computes, and reading the source cannot tell you what it is computing.
get_cell reports a widget's controls with each one's kind, declared default
and current value (the default itself, for a control nobody has moved, since
that is what the cell runs at), and set_widget_value(session_id, cell_id,
{"utilization": 0.9}) sets them and re-materializes the cell, marking
everything downstream stale. On a # @live widget it
runs the same cost-gated cascade a drag does, so the cheap downstream cells
come back computed rather than stale. Send only the controls you are changing.
A notebook that is mid-run refuses the call and leaves the stored values alone,
so retry once that run finishes.
run_cell modes match the UI and CLI: normal uses the cache and re-runs stale
upstreams first; rerun bypasses the target's cache but still refreshes
upstreams; force ("run this only") runs against whatever upstream artifacts
already exist.
set_variant drives variant groups:
set_variant(session_id, "policy", active="service") picks one variant,
set_variant(session_id, "policy", mode="sweep") runs them all, and either
argument can be sent alone. It makes the same two calls the UI's tab strip
does, so staleness recomputes against the new selection; the equivalent REST
route is PUT /v1/notebooks/{session_id}/variant-groups/{group}.
For a swept variable, get_variable names each instance and the spelling
lineage takes for it (score@variant=triple for a # @per_variant cell's
instances; the plain name on the member cell for a sweep group).
Watching the agent¶
An external agent's reasoning lives in its own client, so instead its tool
actions are narrated automatically into the terminal viewer's Agent tab -
"ran cell abc → ok", "added python cell def", "added dependency polars" - as it
works. It can also call the note
tool to post an explicit line of narration ("about to refactor featurize into
two cells"). Open the notebook in the terminal viewer and you can
follow along in real time; the browser UI does not show these notes.
Relationship to the CLI¶
The MCP tools and the strata CLI share one operation contract
(NotebookOps) and return the same curated views. The CLI
is the right tool for offline / headless authoring (write files, strata
run); the MCP server is for driving a live, warm session - rich outputs,
partial re-runs against a populated cache, and edits a human watches in real
time. See Authoring Programmatically for the file + CLI
loop that needs no server at all.