Skip to Content
Connecting an AI Agent (MCP)

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>/mcp

It 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:

SwitchDefaultWhat it does
Allow MCP accessOffWhen off, no agent can connect, and every agent already connected in this org is refused on its next call.
Agents may answer approval gatesOffWhen 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

Add the server once:

claude mcp add --transport http marshal https://<your-marshal-host>/mcp

The 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/mcp

WEB_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=true and tailscaleIngress.hostname, and the install also answers on <hostname>.<your tailnet>.ts.net for anyone on the tailnet. Set tailscaleIngress.tailnet to your tailnet’s domain as well — the chart then allows both the short name and the full MagicDNS name on /mcp by itself.
  • An internal DNS name. Any name that resolves to the install’s ingress inside your network. List it in ingress.extraHosts so it gets its own routing rule, or — if it reaches the API some other way — in mcp.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.

ToolPermissionWhat it does
runs_launchLaunchStart a run from a deputy mission, or from a manifest with a task.
runs_stopLaunchCancel a run that has not ended.
inbox_answerAnswerAnswer one ask, in the person’s name.
runs_waitReadWait for news on a run and return the events after a cursor.
runs_getReadRead one run: state, what started it, task, timing, result.
runs_listReadList runs with the web UI’s filters. scope=org needs owner or admin.
runs_logsReadRead what a run printed.
inbox_listReadList the open asks put to this person.
deputies_listReadList deputies with their active missions.
manifests_listReadList manifests (run environments).
spaces_getReadRead the workspace a run executed in — restarts, OOM kills, why it stopped.
audit_searchReadSearch 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_wait holds 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_logs prefers 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 says logs_unavailable when 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.