Your assistant can write Python. It cannot run it. Point it at one MCP endpoint and it can — in a container with no network, a memory limit and a clock, and what comes back is structured JSON rather than a wall of console output.
Most ways of running code hand back a string and leave you to parse it. That is fine for a person at a terminal and useless to a program — and an assistant that has to regex its way out of stdout will get it wrong eventually, quietly, on the number that mattered. Crucible answers in fields.
A snippet ending in an expression needs no print. Its value comes back as answer,
typed — a number is a number, a dict is an object, a DataFrame is rows and columns.
emit(name, value) for anything else worth namingshow(x) to put a table or a figure in displayartifact(name, data) for a file to carry outThe exception comes back with your line numbers — not the harness’s — the source line that raised, the frames between, and a hint that names the actual problem.
next carries the call that fixes it, arguments filled inAny matplotlib figure left open is rendered and carried out as a PNG. Nobody remembers to call
savefig into a container that is about to be destroyed, so it is done for them.
Anything written to the workspace is collected before the sandbox dies. Fetch one at a time with
artifact.get, so a listing of fifty runs is not fifty megabytes of base64.
An assistant runs code somebody else wrote from a prompt somebody else typed. The interesting question is not whether it works but what it can reach when it doesn’t.
--network none, by default, always. A runtime can be given egress deliberately, and then only
to the hosts it names. Data gets in by being sent with the call.
Read-only root filesystem. Two tmpfs mounts — the workspace and /tmp — and both
vanish with the container.
Every capability dropped, no-new-privileges, running as nobody. Memory, swap, CPU, PIDs and
file descriptors all capped, so a fork bomb is a failed run rather than an incident.
The sandbox stops itself just before the outer one kills it — which is why a run that overruns still tells you what it printed and where it got to, instead of just disappearing.
Every run records its own isolation. If a deployment has no engine and has been told to fall back to a plain process, every reply says so, in the record and in the words. It is never a silent downgrade.
The server’s own environment — its database URL, its API keys — is scrubbed before the code starts. What a run gets is its runtime’s variables and the secrets it is entitled to, by name.
A run always has an owner, so “who ran this, in what, and what did it cost” is a question with an answer. Everything else exists to shape a run or to explain one.
One execution: what was sent, what it got, what it printed, what it returned, what it cost, how it ended.
A named environment: image, packages, network policy, limits. Building bakes the packages in, because a run has no network to install from.
runtime.probe says what is really installedCode worth keeping, with declared inputs checked before anything starts and a revision history behind it.
A sandbox that stays alive, so variables, imports and files survive between calls.
defined lists what is in there, by name and typeA value a run needs and nobody can read back. Encrypted, injected as an environment variable, and returned by no operation at all.
How much anyone may spend, per day or per month. They stack, and the tightest one decides.
No REST surface, no SDK, no client library to keep in step. An assistant learns the API at runtime and then calls it — and there is nothing to relearn when it changes, because it describes itself.
discover — learn the APITables, fields, allowed values, the filter language, and all 274 operations with their exact signatures and a runnable example each. Nine scopes, including a fuzzy search across the lot.
execute — do everythingRun Python. Build a runtime. Save a program. Open a session. Set a quota. Read a report. Every operation
takes the same envelope, and dry_run, idempotency_key and actor
work on all of them.
There is no cell you scroll back to and no hidden state you forgot you set — except in a session, where that is the point and the state is listed for you.
A run is synchronous and bounded by a clock. If the work takes an hour, it is not this shape of work.
Nothing here serves traffic or listens on a port. Code runs, answers, and the container is destroyed.
Packages come from PyPI at build time. A run never installs anything, because it has no network to install from.
There is a records browser and a run log for when you want to look. Nobody is meant to open it daily.
/mcp is the only door into the data. The web pages sign you in, show you your own records, and manage keys.
Real timestamptz, jsonb with GIN indexes, and partial indexes that match each
ORDER BY exactly. The schema is generated from one file; there is no migration directory to
fall out of step with it.
A local socket, or a remote one over DOCKER_HOST. Nothing is mounted and no file is staged on
the host — the job goes in on stdin and the answer comes out on stdout, which is what makes a remote
engine work at all.
Dynamic client registration, PKCE, rotating refresh tokens. Or issue an API key from your account page and paste it into a header.
Invitations and password resets, in one house style. Not configured? The link comes back in the reply so you can send it yourself.
assist.write turns a request into code that will actually run here; assist.explain
and assist.fix read a failed run and answer from it. All optional — nothing else needs a model.
The organisation fence, row visibility and secret redaction all live in the one path every read and write goes through. A query that forgets one is impossible rather than merely unlikely.
claude mcp add --transport http crucible https://your-deployment/mcp
Nothing needs setting up before the first run. With no runtime configured it runs plain Python and tells you so — and tells you what to call when you want pandas.