Everything else the agent can do — shell, reading and writing files, starting
services, publishing a preview, committing to a repository — is always present
and is not configured here. You are choosing what it can reach outside the
workspace, not what it can do inside one.
Declare them in
agent.tools, on POST /v1/sessions. The agent has them
from its first turn, which is what every example below does.
PUT /v1/sessions/{session_id}/tools can change the configuration of a
session that already exists — see
Does PUT reach a running session? before
you rely on it for anything the current turn needs, because the answer is no.
At most 32 tools per session; each name is 1–64 characters.
Built-in tools
The sandbox already has these. Naming one is the whole request — they take no other field:
Naming any of them narrows the session to exactly those. Say nothing and the
session gets whatever this deployment allows, which is what every session got
before you could choose. Say
"tools": [] and it gets none of them — no way
out to the network at all, which is the shape for running code you have not
read.
You can only narrow. A tool the deployment has turned off does not come back
by being named in a request body. If it did, the operator switch would be
decoration, and the one thing it exists for — running untrusted work with no
route to the internet — is exactly what it could no longer guarantee.
So the list you get is the intersection, and reading the configuration back
tells you what you actually have rather than what you asked for.
Function tools
You declare the shape; the agent calls it; the turn stops until you answer.run-session.ts.
A turn waiting on you is still in flight. The workspace is not reclaimed
out from under it, however long you take. There is no deadline on the answer
unless you set one — timeout_seconds on the tool, see
required-actions.md — and the only ceiling either way is
the workspace’s two hours, in limits.md.
MCP servers
Exactly one ofurl or command:
url— a server already running, which the sandbox connects out to.command— a process started inside the sandbox, spoken to over stdio.
The connection is made from the sandbox, not from us. Your tool traffic does
not pass through our control plane — which is what lets an MCP server on your
private network be reachable at all, and keeps our egress out of your blast
radius. A server reachable only from your VPC works if the sandbox is in it;
one behind your office firewall does not.
What the sandbox can run
If you usecommand, this is what is already there to start:
So the two commonest MCP distribution shapes —
npx -y <package> and
uvx <package> — work with nothing installed first.
A stdio server has no way to receive a secret, yet
Every MCP client in the ecosystem hands a stdio server its credentials through the environment. This one cannot: the tool object has noenv field, and
there is nowhere else to put a value a command process would see as a
variable.
/v1 at all yet. Until env exists, a server of that
shape needs a remote HTTP endpoint that accepts a header instead, so its
secret goes in authorization or headers on a url entry rather than
command.
When a server cannot be reached
By default it is skipped, and the agent simply has fewer tools. Nothing fails. The agent will usually say it cannot do the thing you asked — which looks like the model being unhelpful, when the real cause is a server that never answered. That default is deliberate: one optional server being down should not destroy a session that could still do most of its work. But it is only right when the server really is optional. You will be told, though. A skipped server produces anmcp_unavailable
item in the session’s transcript and an mcp.unavailable event on the stream:
required: true the session fails loudly instead of running crippled, and
you get an error rather than a confused answer. It arrives on the first call
that needs the workspace — sending a message, writing a file — and it names the
server and what went wrong:
invalid_request rather than a retryable code, deliberately: waiting will not
make the server reachable. This is a configuration to change, not a state to
wait out.
Choose deliberately.required: falseand a broken URL is the one combination that produces no signal anywhere a caller can see it. If you are not sure,required: trueis the safer default for anything the task actually depends on.
required must be true or false. A string — "required": "yes" — is
refused rather than read as false, because false is a real choice here and
not one you would have made by writing the word required.
Confirm the configuration took
POST /v1/sessions answers 201, and its agent object does not include
tools at all — so a 201 is not evidence your server was configured, only
that the request was accepted. Read it back:
redacted means, is in
Reading the configuration back below.
Confirm it was actually used
A completed turn is not a tool call, and a model that cannot reach a server still produces a plausible-looking answer — it knows what that answer is supposed to look like, and says it anyway. Check rather than trust:tool_call entries naming your server — mcp_s_<name>_<tool> — and
then, the first time you wire a new server up, check one fact the agent
reported against the upstream system yourself, through a different path than
the agent used. Matching answers mean the agent actually reached it. An
agent’s own account of its own tool use is the weakest evidence available.
Does PUT reach a running session?
PUT /v1/sessions/{id}/tools answers 200, and GET /v1/sessions/{id}/tools
reads back exactly what you sent. Neither means the agent already running has
it.
Verified against production: a session was created with one function
tool, allowed to complete its first turn, then given a second tool with
PUT. GET read both tools back correctly. Asked on its next turn to call
the second one by name, the agent answered: “There’s no tool named tool_b
in my available set — only tool_a exists.” The new tool reached the
control plane’s record of the session and nothing else.
The tool list is fixed when the session’s workspace is built, the same as
instructions — a PUT changes what a new
workspace for this session will start with, not what the current one is
running. Paused-and-woken does not rebuild it; a session that is torn down
and recreated does.
If a session needs a new tool immediately, the reliable way is to give it one
from the start, or to accept that the change reaches the next workspace
rather than the current turn.
Reading the configuration back
tools array, each entry
carrying its type. The same shape goes back to PUT.
Secrets are not returned. authorization and every header value are held
back, and redacted names what was withheld — so a server with credentials
configured is distinguishable from one without, which an omission alone would
not tell you.
redacted is not a request field. If you read the configuration, change
something and PUT it back, you get a 400 naming redacted rather than a
success that quietly replaced your credentials with nothing. Re-send the
secrets, or build the body from your own source.
Demos
Every field above is real; these are the three shapes worth actually running, in order of how often you’ll reach for them. Each does the whole loop this page just walked through: create with the tool attached, read the configuration back, then confirm a real call happened rather than trusting the agent’s account of it.A remote server, bearer-authenticated — Linear
Verified end to end against production: a personal API key, sent straight asAuthorization: Bearer, no OAuth flow. Two endpoints —
https://mcp.linear.app/mcp/readonly for reads,
https://mcp.linear.app/mcp for reads and writes — which makes “the agent
cannot modify my tracker” a property of the URL you chose rather than a
sentence in a prompt.
required: true catches it — is in
Give an agent your tracker.
A local stdio server — npx, no network account needed
No vendor, no key — the server ships in the package, and the sandbox already
has npx. Good for trying the whole loop once before you point it at
anything real.
mcp_s_local_echo, with
args: {"message":"gobare-docs-check"} on the way in and
summary: "Echo: gobare-docs-check" on the way back — the same fact the
tool_call items page is checked against, independent of what the agent’s
own final message claims.
A hosted server from a second vendor — GitHub
The same shape as the Linear demo above, against a different provider, is worth seeing once precisely because the config barely changes:https://api.githubcopilot.com/mcp/ answers a plain bearer token with a
working MCP handshake — no OAuth dance, same as Linear. A token scoped to
read-only is enough for get_me; narrow allowed_tools to what the task
actually needs before you hand over anything broader.
Verified against production, with a token holding no scopes at all: the
tool_call item came back as mcp_s_github_get_me, and the login and id
in its result matched a direct, independent GET https://api.github.com/user
with the same token — not the agent’s own summary of what it saw.
Refusals
Every refusal carries
{ error: { code, message, request_id } } — see
errors.md.
Next
- required actions — answer a call the agent makes
- Give an agent your tracker — the Linear demo above, in full, with the numbers from the run
- extracting structured data — shape what comes back