# MCP gateway

This page describes the local MCP gateway. Cloud sandboxes use MCP servers
configured in Docker Agentic Platform: see
[Load an MCP server](cloud/usage.md#load-an-mcp-server).

Docker Sandboxes includes an MCP gateway for connecting agents to Model Context
Protocol servers. The gateway gives the agent inside the sandbox one MCP
endpoint, while `sbx` manages the registered servers, OAuth credentials, and
sandbox lifecycle on the host.

This is different from configuring an MCP server directly in an agent such as
Claude Code. Direct MCP setup configures that agent's own MCP client. With
Docker Sandboxes, you register MCP servers once on the host, and the sandbox
gateway exposes them to supported agents inside isolated sandboxes. That
host-managed gateway provides a single path for credentials, explicit server
loading, live updates, and organization governance.

> [!NOTE]
> The Docker Sandboxes MCP gateway is separate from the Docker Desktop MCP
> Toolkit. You don't need the Docker Desktop MCP Toolkit to use `sbx mcp`, and
> MCP Toolkit server settings aren't shared with Docker Sandboxes.

## Prerequisites

- Sign in with `sbx login`.
- Use an agent integration that configures MCP at startup: Claude Code, Codex,
  Devin, Gemini, Kiro, or OpenCode.
- For remote servers that require OAuth without Dynamic Client Registration,
  register an OAuth client with the server provider.
- For `--local --url` registrations that resolve to OCI packages, use a host
  with Docker installed and running. Docker is also required for explicit
  `--command docker ...` registrations.

## Quick start

Start by registering one MCP server on the host:

```console
$ sbx mcp add notion --url https://mcp.notion.com/mcp
```

If the server requires OAuth, `sbx` opens an authorization flow before it stores
the registration. After registration, verify that the server is registered:

```console
$ sbx mcp ls
```

The listing shows each server's transport, status, gateway, and details.

Then start a sandbox and expose the registered server:

```console
$ sbx run claude --name mcp-demo --static-mcp notion
```

The sandbox starts with an MCP gateway and pre-loads the `notion` server. The
registration remains on the host and can be reused by other sandboxes.

## Register an MCP server

`sbx mcp add` registers an MCP server by name. The registration records the
server definition on the host. It doesn't attach the server to a sandbox by
itself. To expose a registered server to a sandbox, pass it with
[`--static-mcp`](#use-static-mode) when you create the
sandbox, or use [`sbx mcp load`](#add-a-server-to-a-running-sandbox) for a
sandbox that's already running.

Server names can contain letters, numbers, dots, hyphens, and underscores.

The `--url` flag can point to different kinds of input. The execution location
depends on what you register:

- A remote endpoint URL identifies a running MCP server. The server runs
  remotely, and the sandbox gateway connects to it.
- A metadata URL with `--local` returns a registry entry, `server.json`, or
  `server.yaml` that describes an OCI-packaged stdio server. `sbx` resolves the
  image and runs it on the host with Docker.
- An explicit command runs on the host as a stdio MCP server.

Local stdio servers run on the host, not inside the sandbox. The agent inside
the sandbox connects only to the MCP gateway.

If a `--url` hostname resolves to a private, loopback, link-local, or cloud
metadata address, `sbx` registers the server but warns you about the resolved
address. Register only URLs you trust. Fetching a manifest from an untrusted
URL can expose internal services or cloud metadata, and DNS rebinding can
redirect a hostname after it has been checked.

OAuth metadata discovery also warns and continues when it encounters these
addresses, including redirect destinations.
For a trusted internal server, pass `--skip-ssrf-check` to skip both the MCP
URL check and the OAuth metadata discovery checks and suppress their warnings.
Use the flag only when you trust the MCP host, OAuth provider, and all metadata
redirect destinations.

### Remote endpoint URL

For a remote MCP endpoint, pass the server URL:

```console
$ sbx mcp add notion --url https://mcp.notion.com/mcp
$ sbx mcp add linear --url https://mcp.linear.app/mcp
```

If requests to a remote server stall, see
[MCP server streams stall](troubleshooting.md#mcp-server-streams-stall).

#### Custom request headers

Use `--header 'Name: value'` to send custom HTTP headers to a remote MCP
endpoint, for example to authenticate with an API key. Repeat the flag for
each header, using each header name once:

```console
$ sbx mcp add acme --url https://mcp.acme.com/mcp \
  --header 'Authorization: Bearer ${api-key}' \
  --header 'Accept: application/json, text/event-stream'
$ sbx secret set mcp:acme:api-key
```

Replace the example URL with your MCP endpoint. The `sbx secret set` command
prompts for the API key and stores it in the
[host credential store](configuration/credentials.md#where-secrets-are-stored).
The `${api-key}` placeholder stays in the registration. When a sandbox
connects, the gateway reads the secret and substitutes its value in the header.
Use single quotes around header values so your shell preserves placeholders.
Placeholders name stored secrets, not environment variables: `${api-key}`
reads `mcp:acme:api-key`, regardless of your shell environment.

Store each placeholder with `sbx secret set mcp:<server>:<placeholder>`.
Credential headers such as `Authorization` must use a secret placeholder.
An explicit `Authorization` header takes precedence over an OAuth access token.

After storing the secret, expose the server to a sandbox:

```console
$ sbx run claude --name acme-demo --static-mcp acme
```

Custom headers require a remote HTTP endpoint and can't be used with
`--command` or `--local`. They also require the host to connect to the server.
The hosted gateway rejects these registrations unless you supply
`--oauth-authorization-server`, which routes the connection through the host.

#### Manage header secrets

Header secrets use the global scope on the host. Set them with
`sbx secret set mcp:<server>:<placeholder>` without `--sandbox`.
Header secrets require a stored value and don't support `--ref` or `--command`
dynamic sources.
To check the header templates and whether their secrets are set, run
`sbx mcp inspect acme`. The command doesn't display resolved secret values.

The secret store also contains an automatically managed `:endpoint` record,
such as `mcp:acme:api-key:endpoint`. This metadata binds the secret to the
registered server's URLs so the gateway can detect an endpoint change before
sending the secret. You don't need to set this record yourself. If you change
the server's endpoint, follow the CLI guidance to set the secret again for
that endpoint.

To rotate a header secret, run `sbx secret set` with the same name. After
setting, changing, or removing a header secret, stop and restart the sandbox
or restart `sandboxd` to apply the change to an existing gateway. An existing
connection keeps its previous value, and a server skipped because its secret
was missing isn't retried automatically.

Removing a registration with `sbx mcp rm` keeps its header secrets and prints
commands to remove them. To remove the example secret:

```console
$ sbx secret rm mcp:acme:api-key
```

### Local stdio server

Some MCP servers communicate over stdio instead of exposing a remote HTTP
endpoint. Use a local stdio server when `sbx` should launch the MCP server on
the host. You can provide a metadata URL or an explicit command.

#### From registry or manifest metadata

Use `--local --url` when you have an MCP community registry URL or a URL that
returns a `server.json` or `server.yaml` document. The registry entry or
manifest must describe an OCI package that uses stdio transport. `sbx` doesn't
launch non-OCI package types, such as `npm`, from metadata. To use those
servers, register an explicit command.

This path resolves the image from the metadata and starts it on the host with
Docker, so Docker must be installed and running on the host.

```console
$ sbx mcp add fetch --local \
  --url https://registry.modelcontextprotocol.io/v0/servers/fetch-mcp/versions/latest
```

If the entry doesn't publish an OCI stdio package, `sbx` rejects the
registration instead of starting it locally.

A server manifest describes the MCP server package and how to start it. It can
be hosted on a GitHub raw URL, internal HTTP server, or CDN.

```console
$ sbx mcp add opine --local --url https://example.com/mcp/opine/server.yaml
```

#### From an explicit command

Use `--command` when you already know the executable and arguments, or when you
need custom Docker flags. The command can be a package runner such as `npx` or
a Docker container command:

```console
$ sbx mcp add playwright --command npx --args @playwright/mcp@latest
$ sbx mcp add local-image-server --command docker \
  --args "run,-i,--rm,your/image"
```

To set the working directory for the host process, pass `--dir`. This flag is
only valid with `--command`:

```console
$ sbx mcp add local-fs --command node --args server.js --dir /srv/data
```

Use registry or manifest metadata when you have a published server definition
and don't need to customize `docker run`. Use `--command` for local
development, private servers, or custom container flags.

> [!WARNING]
> Local stdio servers run on the host, outside sandbox isolation. If the command
> starts a Docker container, that container uses host Docker isolation, not
> sandbox isolation. The process or container can access host files, host
> network resources, and credentials made available to it. Use trusted commands
> and images, and avoid mounting host paths or passing credentials unless the
> server needs them.

### Docker Hardened Image

For an MCP server packaged as a Docker Hardened Image, pass its `dhi.io`
reference instead of a registry or manifest URL:

```console
$ sbx mcp add fetch --url dhi.io/fetch-mcp:latest
```

Use a tag or digest: `dhi.io/<IMAGE>:<TAG>` or
`dhi.io/<IMAGE>@sha256:<DIGEST>`. For images from other registries, use a
server-manifest URL or an [explicit command](#from-an-explicit-command).

## Authorize OAuth-backed servers

If a registered remote server requires OAuth, `sbx mcp add` starts the
authorization flow by default:

```console
$ sbx mcp add notion --url https://mcp.notion.com/mcp
```

OAuth credentials stay on the host. In local gateway mode, `sbx` stores tokens
in the host operating system's credential store.

To register an OAuth-backed server without authorizing it, pass `--skip-auth`:

```console
$ sbx mcp add notion --url https://mcp.notion.com/mcp --skip-auth
```

### Use a pre-registered OAuth client

In local gateway mode, you can register a remote OAuth server that doesn't
support Dynamic Client Registration. If the server publishes OAuth metadata,
pass the client ID that you registered with the server provider:

```console
$ sbx mcp add slack --url https://slack.example.com/mcp \
  --client-id <CLIENT_ID>
```

If the server doesn't publish OAuth metadata, pass
`--oauth-authorization-server` with the client ID. The flag accepts a local
file path or an HTTP or HTTPS URL to an RFC 8414 authorization server metadata
document. The document must define `authorization_endpoint` and
`token_endpoint`:

```console
$ sbx mcp add serverx --url https://mcp.serverx.example/mcp \
  --oauth-authorization-server ./serverx-authorization-server.json \
  --client-id <CLIENT_ID>
```

These flags are only valid with `--url`.

For a confidential OAuth client, store the client secret before registering
the server. There is no `--client-secret` flag:

```console
$ sbx secret set mcp:slack:client_secret
$ sbx mcp add slack --url https://slack.example.com/mcp \
  --client-id <CLIENT_ID>
```

The client secret stays in the host credential store and isn't
written to the MCP registration. If the server requires a confidential client
and no secret is stored, registration succeeds but authorization is skipped.
Store the secret, then run `sbx mcp auth <server>`.

#### Change the OAuth client

If you re-register a server name with a different client ID, issuer, or token
endpoint, `sbx` refuses to use the secret bound to the previous client.
After verifying that the registration points to the intended client and
provider, store that client's secret, clear the old binding, and authorize:

```console
$ sbx secret set mcp:slack:client_secret
$ sbx secret rm mcp:slack:client_secret:identity
$ sbx mcp auth slack
```

Setting a secret alone doesn't clear the old binding. The `:identity` record
is metadata that binds the stored secret to its OAuth client. Authorization
records the binding for the new client.

For secrets stored under the legacy name `mcp:<server>.client_secret`, set
the secret again using `mcp:<server>:client_secret`.

### Set OAuth scopes

Use the repeatable `--scope` flag to record the default scopes requested during
authorization:

```console
$ sbx mcp add serverx --url https://mcp.serverx.example/mcp \
  --scope read --scope write
```

The `sbx mcp auth` command accepts `--scope` to override the recorded defaults
for one authorization:

```console
$ sbx mcp auth serverx --scope read
```

Unless you pass `--no-scope`, `sbx` requests the first available scope set in
the following order:

1. Scopes passed to `sbx mcp auth --scope`
2. Default scopes recorded by `sbx mcp add --scope`
3. Scopes that the protected resource says it requires
4. Whichever of `openid`, `email`, `profile`, and `offline_access` the
   authorization server advertises

If none of these provide a scope set, `sbx` omits the OAuth `scope` parameter so
the authorization server applies its default grant. Other advertised scopes
aren't included in the fallback.

When `sbx` uses recorded defaults or resource-required scopes, it also
requests `offline_access` if the authorization server advertises it. The
server can then issue a refresh token, so you don't have to authorize again
when the access token expires. Scopes passed to `sbx mcp auth --scope` are
requested exactly as given, without `offline_access`. Existing credentials
issued without a refresh token aren't upgraded; run `sbx mcp auth` again to
request a new grant.

Pass `--no-scope` to suppress the recorded defaults, resource-required scopes,
and advertised scope fallback for one authorization, without changing the
stored defaults:

```console
$ sbx mcp auth serverx --no-scope
```

You can't combine `--no-scope` with `--scope`. Scopes you choose are checked
against the authorization server's advertised scopes and the resource's
required scopes. If either source publishes scopes, a scope present in neither
produces a warning but is still requested. The authorization server can still
refuse an advertised scope for a particular
client. For a local authorization flow that requested scopes, `sbx` lists the
requested, advertised, and refused scopes and suggests a retry command. If the
server identifies the refused scopes, the command removes them. Otherwise, it
uses `--no-scope`.
`sbx` never retries automatically.

For each OAuth-backed remote server exposed to a sandbox, the gateway exposes a
helper tool named `<server>-authorize`, such as `notion-authorize`. The agent can
call the tool to authorize or reauthorize the server. If the server isn't
authorized, the helper is the only tool exposed for that server.

You can manage OAuth credentials from the host:

```console
$ sbx mcp auth status notion
$ sbx mcp auth notion
$ sbx mcp auth rm notion
```

Use `--all` to apply `auth`, `auth status`, or `auth rm` to all registered
OAuth-backed servers. The `auth status` output reports the scopes granted by the
authorization server, the defaults recorded with `sbx mcp add --scope`, and the
scopes the server supports. It collapses duplicate scope names and highlights
granted scopes that weren't requested or are no longer in the supported set.
Use `--json` for machine-readable output:

```console
$ sbx mcp auth status notion --json
```

### Override the OAuth resource indicator

Use `--resource` when your OAuth provider requires a token audience that
differs from the resource identifier discovered by `sbx`. By default, `sbx`
uses the server's protected-resource metadata, or the endpoint URL if the
metadata is absent. Check the value with `sbx mcp inspect <SERVER>` before
setting an override:

```console
$ sbx mcp add serverx --url https://mcp.serverx.example/mcp \
  --resource https://api.serverx.example/mcp
```

Use the resource URI supplied by your provider. It must have a scheme and host
and no fragment. To change an existing registration's value, remove the
registration and add it again.

This override requires OAuth authorization on the host. The hosted gateway
doesn't use it unless you also supply `--oauth-authorization-server` or
`--client-id` to route authorization through the host. Without those flags,
`sbx mcp add` warns that the hosted gateway won't use the override.

## Choose an MCP mode

Every sandbox starts an MCP gateway. When the sandbox starts, supported agent
integrations read the gateway URL and register it with the agent.

Whether you pass `--static-mcp` when you create the sandbox determines its MCP
mode:

- Static mode pre-loads the specified servers and doesn't expose dynamic
  discovery tools to the agent.
- Dynamic mode pre-loads no servers and lets the agent find and attach
  registered servers.

This choice persists across sandbox restarts unless you
[unload every server in the static set](#remove-a-server-from-a-running-sandbox).

### Use static mode

Pass `--static-mcp` to pre-load registered MCP servers:

```console
$ sbx mcp add notion --url https://mcp.notion.com/mcp
$ sbx mcp add linear --url https://mcp.linear.app/mcp
$ sbx run claude --name my-session --static-mcp notion,linear
```

You can pass `--static-mcp` as a comma-separated list or repeat the flag:

```console
$ sbx run claude --name my-session \
  --static-mcp notion --static-mcp linear
```

Every name in the static set must already be registered with `sbx mcp add`. The
gateway doesn't expose `mcp-find`, `mcp-add`, or `mcp-config-set` to the agent.

You can't replace the initial set by passing `--static-mcp` when reconnecting to
an existing sandbox. To attach another server from the host, use
[`sbx mcp load`](#add-a-server-to-a-running-sandbox).

### Use dynamic mode

Omit `--static-mcp` to use dynamic mode. The gateway pre-loads no servers and
exposes `mcp-find`, `mcp-add`, and `mcp-config-set` to the agent. The agent can
search the registered server catalog and attach servers during the session.

If you run `sbx mcp add` after a dynamic sandbox starts, its gateway refreshes
the searchable catalog. The agent can then find and attach the new registration
without restarting. The `sbx mcp add` command doesn't attach the server by
itself.

## Add a server to a running sandbox

To attach an already-registered server to a running sandbox, use
`sbx mcp load`. This works in both static and dynamic modes:

```console
$ sbx mcp add linear --url https://mcp.linear.app/mcp
$ sbx mcp load linear --sandbox my-session
```

Connected agent sessions receive a tool-list update, so the added tools become
visible without reconnecting. The loaded server remains attached across sandbox
restarts.

## Remove a server from a running sandbox

In Docker Sandboxes v0.48.0 and later, use `sbx mcp unload` to detach a server
from a running sandbox's local MCP gateway. This works in both static and
dynamic modes. Cloud sandboxes and hosted MCP gateways don't support unloading
servers.

```console
$ sbx mcp unload linear --sandbox my-session
```

Connected agent sessions receive a tool-list update, so the server's tools are
removed without restarting the agent. The command also removes the saved
attachment so the server isn't restored when the sandbox or daemon restarts.
The server's host registration and credentials remain available, and other
sandboxes that have loaded the server are unaffected.

To attach the server again:

```console
$ sbx mcp load linear --sandbox my-session
```

In dynamic mode, the agent can also find and attach the registered server
again. Unloading a server doesn't prevent the agent from reloading it.

If you unload all servers from a static sandbox, it stays in static mode for
the current session. After restarting the Docker Sandboxes daemon and
reconnecting, the agent can find and attach registered servers.

## Built-in gateway tools

The local MCP gateway exposes a small set of built-in tools. These tools belong
to the gateway itself, not to a registered MCP server. Agents can see and call
them on the same MCP connection as server tools, so they can appear in agent
tool lists, logs, policy decisions, audit logs, or approval prompts.

You don't need to call these tools directly for normal setup. Use `sbx mcp`
commands to register servers and manage credentials from the host. The tools
matter because agents can call them during a session, and admins can govern
them separately from tools provided by registered MCP servers.

| Tool                 | Description                                                                                |
| -------------------- | ------------------------------------------------------------------------------------------ |
| `mcp-exec`           | Executes a tool by name through the gateway.                                               |
| `code-mode`          | Creates an ephemeral JavaScript tool that can call selected tools through the MCP gateway. |
| `mcp-find`           | Searches the registered server catalog without changing sandbox state. Dynamic mode only.  |
| `mcp-add`            | Attaches a registered server to the sandbox. Dynamic mode only.                            |
| `mcp-config-set`     | Sets per-session configuration overrides for an attached server. Dynamic mode only.        |
| `<server>-authorize` | Starts or restarts OAuth authorization for an exposed OAuth-backed remote server.          |

Servers attached with `mcp-add` remain attached across sandbox restarts. The
gateway exposes `<server>-authorize` for OAuth-backed remote servers, even when
they already have a valid token. Local stdio servers don't expose this helper.
If `code-mode` creates a generated tool, the tool is shared by clients connected
to the sandbox's gateway and disappears when the gateway is replaced or stops.

In MCP access policies, built-in gateway tools are `MCP::Primordial` resources
and use the `invokePrimordial` action. Tools from registered MCP servers are
`MCP::Tool` resources and use the `invokeTool` action. For details, see the
[MCP policy reference](governance/reference/mcp-policy.md).

## Manage registrations

List registered servers:

```console
$ sbx mcp ls
```

Inspect a registered server:

```console
$ sbx mcp inspect notion
```

Remove a registered server:

```console
$ sbx mcp rm notion
```

Removing a registration doesn't detach the server from sandboxes that have
already loaded it. Use
[`sbx mcp unload`](#remove-a-server-from-a-running-sandbox) for each sandbox
before removing the registration.

For OAuth-backed servers, `sbx mcp rm` removes the OAuth access token before it
removes the server registration. A client secret for a pre-registered OAuth
client and its identity binding remain in the host credential store so you can
reuse them when you re-add the same client. The command prints the
`sbx secret rm` commands for removing them. To remove only the OAuth access
token, use `sbx mcp auth rm`.

## Governance

Organizations with AI Governance can use
[MCP access policies](governance/access-controls/mcp.md) to control MCP server
registration, tool calls, gateway meta-tools, resources, prompts, and approval
requirements. MCP access policies are organization policies written in Cedar.