Connecting an AI Agent (MCP)
Every Marshal install serves an MCP endpoint. Connect an agent to it — Claude Code on a laptop, Claude.ai in a browser, or a headless agent in CI — and that agent can start Marshal runs, follow them, read their logs and answer the questions they ask, as the person who signed in, with that person’s permissions.
The endpoint is on the same host as the web UI:
https://<your-marshal-host>/mcpIt is one endpoint per install. There is no central address that finds your install for you.
An agent connected over MCP can see only what the signed-in person can see in the web UI. A connection never raises someone’s role, and never reaches another org.
Step 1 — an admin turns MCP on
MCP access is off in every org until an admin turns it on, on every install. Until then, an agent that tries to connect is told to ask an org admin.
An owner or admin turns it on in Settings → Organization → MCP access:
| Switch | Default | What it does |
|---|---|---|
| Allow MCP access | Off | When off, no agent can connect, and every agent already connected in this org is refused on its next call. |
| Agents may answer approval gates | Off | When off, agents can answer ordinary questions only. Approval and takeover asks stay open for an answer in the web UI or Slack. |
The second switch is separate on purpose. An agent reads text that runs produced, so text inside a run can try to steer it. Leave approval gates for a human unless you have decided otherwise.
Step 2 — connect your agent
Claude Code
Add the server once:
claude mcp add --transport http marshal https://<your-marshal-host>/mcpThe first tool call opens your browser (/mcp inside Claude Code starts the
same flow). You sign in to Marshal as usual, then a consent page asks:
- which org this connection is for — one org per connection;
- which permissions to grant: Read, Launch, Answer.
The page names the client and the host its identity comes from. A client whose
host is not on the install’s verified list (claude.ai, anthropic.com and
cursor.com by default) is labelled unverified, and Launch and Answer are
unticked for it — tick them yourself if you want them.
Approve, and the client is connected. Nothing has to be registered on the Marshal side first.
Trying it on a laptop, with no cell
The whole flow runs against a local checkout — useful for seeing the consent page and the tool list before you point a client at a real install:
make dev-deps # postgres, redis, migrations
WEB_PUBLIC_URL=http://localhost:3000 make dev-api # second terminal
make dev-web # third terminal, serves :3000
claude mcp add --transport http marshal http://localhost:3000/mcpWEB_PUBLIC_URL must be the origin you give the client: it is what the Host
check, the advertised resource and the token audience are keyed on. The dev
server proxies /mcp and the OAuth paths to the API, so the client, the
consent page and the tokens all share one origin, as they do behind a real
ingress.
An org admin still has to turn MCP access on — a fresh local org has it off, and the consent page will say so.
Other MCP clients
Any client that speaks MCP over streamable HTTP and OAuth 2.1 with PKCE can
connect — nothing about the endpoint is Claude-specific. One rule decides
whether a given client works: its redirect URI must be https://, or http://
on a loopback host (127.0.0.1, [::1] or localhost, on any port — that is
what a desktop client uses). A client that registers a private-use scheme such
as myapp://callback is refused today; Cursor’s desktop registration is in
that group.
Installs with no inbound internet
Most self-hosted installs take no traffic from the internet at all. The MCP works there, with two things to set.
Claude.ai cannot be used with such an install. It is a service in Anthropic’s cloud: it has to reach your install from outside, and Marshal never tunnels out to meet it. Everything else on this page works — Claude Code, Cursor and any other client running inside your network, and headless agents with an API key.
How a laptop reaches the install
Two shapes, both of which the chart supports:
- A tailnet name. Set
tailscaleIngress.enabled=trueandtailscaleIngress.hostname, and the install also answers on<hostname>.<your tailnet>.ts.netfor anyone on the tailnet. SettailscaleIngress.tailnetto your tailnet’s domain as well — the chart then allows both the short name and the full MagicDNS name on/mcpby itself. - An internal DNS name. Any name that resolves to the install’s ingress
inside your network. List it in
ingress.extraHostsso it gets its own routing rule, or — if it reaches the API some other way — inmcp.extraHosts.
A name that is not in one of those lists gets 403 on /mcp, even though
the rest of the app works on it. That check is deliberate: it stops a name
someone else controls from pointing at your install and driving the MCP with a
person’s token.
Each name is its own sign-in
A person signs in per name. Connect on marshal.ts.net and you sign in
there; the same laptop connecting on marshal.internal signs in again. The
address you used decides the whole exchange — the sign-in page, the consent
page and the token — so a token minted on one name is refused on the other and
the client simply asks you to sign in again.
So pick one name per audience and put it in your install’s instructions. A person switching names is not broken, only asked to sign in once more.
Fully air-gapped: turn off client metadata documents
By default, Marshal identifies a client by fetching the https:// URL the
client uses as its ID — a Client ID Metadata Document, on the public internet.
An install with no outbound internet cannot do that: the fetch waits for
its timeout and then fails, and the person sees an error about the client’s
host.
Set mcp.cimdEnabled=false in the chart (MARSHAL_MCP_CIMD_ENABLED=false on
the API) on such an install. Then:
- the authorization server stops advertising that it supports those documents, so clients register themselves dynamically instead — Claude Code, Cursor and other standard clients all do this;
- a client that still sends a URL as its ID is refused at once, with a message telling it to register dynamically;
- every client is unverified on the consent page (only a document can carry a verified host), so Launch and Answer are unticked and the person ticks the ones they want.
Leave it on (the default) if the install can reach the internet outbound, even if nothing can reach it inbound: fetching those documents is what lets the consent page tell a real Claude or Cursor client from one that merely names itself that.
What the agent can do
Three permissions, twelve tools. Each tool needs exactly one permission, so a Read-only connection cannot start or stop anything.
| Tool | Permission | What it does |
|---|---|---|
runs_launch | Launch | Start a run from a deputy mission, or from a manifest with a task. |
runs_stop | Launch | Cancel a run that has not ended. |
inbox_answer | Answer | Answer one ask, in the person’s name. |
runs_wait | Read | Wait for news on a run and return the events after a cursor. |
runs_get | Read | Read one run: state, what started it, task, timing, result. |
runs_list | Read | List runs with the web UI’s filters. scope=org needs owner or admin. |
runs_logs | Read | Read what a run printed. |
inbox_list | Read | List the open asks put to this person. |
deputies_list | Read | List deputies with their active missions. |
manifests_list | Read | List manifests (run environments). |
spaces_get | Read | Read the workspace a run executed in — restarts, OOM kills, why it stopped. |
audit_search | Read | Search the audit log. Own events; the whole org for owners and admins. |
There is no tool that creates, changes or deletes credentials, secrets, connections, packages, members, deputies, manifests or webhooks. Read-only listing is all the MCP offers for them.
A few behaviours worth knowing:
- Launches are confirmed when the client can show a prompt. Claude Code asks you before the run starts, and declining starts nothing.
runs_waitholds at most 30 seconds per call and returns at most 200 events. It returns early when the run ends, asks a question, or produces events. The agent calls it again with the cursor it got back.runs_logsprefers the stored transcript. If your install keeps no transcripts, it falls back to the run events still kept (about an hour after the run ends), and sayslogs_unavailablewhen there is neither — never an empty success.- Run output is wrapped in an
<untrusted-content>block, with known secret shapes redacted, so the agent treats it as data rather than as instructions.
The run cap
One person may have 5 runs started through the MCP active at once, per org. The sixth is refused, with the runs that are in the way:
refused: you already have 5 active runs started through the MCP in this org,
and the cap is 5. Active runs: <ids>. Wait for one to end (runs_wait) or stop
one (runs_stop), then launch again.Runs started in the web UI, the CLI or by a schedule do not count. The cap is
the guard on clients that cannot ask you to confirm a launch, so keep it in
mind before raising it. Set MARSHAL_MCP_RUN_CAP on the API (chart value
api.extraEnv) to change it for the whole install.
Auditing and revoking
Every launch, stop and answer, and every sign-in, token and revoke, is written
to the audit log with the person, the client, the permissions and the outcome,
under channel mcp. Search it on the Audit page, or from an agent with
audit_search.
To end a connection:
- A person revokes their own: Personal settings → Connected apps → Revoke. The client is refused on its next call and must sign in again.
- An owner or admin sees every connection in the org on the same page, and can revoke any of them.
- Turning off “Allow MCP access” ends every connection in the org at once.
Access also ends by itself when the person leaves the org or their role changes — the same rules that end their web sessions.