Share feedback
Answers are generated based on the documentation.

sbx mcp add

DescriptionRegister an MCP server
Usagesbx mcp add <name> (--url <url> | --command <cmd>) [flags]

Description

Register an MCP server by name. The server is validated and its specification is stored for use with sbx create/run --static-mcp.

This command only registers the server. To attach an already-registered server to a running sandbox, use 'sbx mcp load'.

The --url flag accepts four input formats; the type is auto-detected:

  • Remote MCP endpoint URL (https://host/mcp — talks MCP at the URL; OAuth metadata is discovered via RFC 9728/8414).
  • MCP community-registry URL (https://registry.modelcontextprotocol.io/v0/servers/...) — fetches the registry envelope and resolves the OCI image.
  • Server-manifest URL (any URL returning a server.json or server.yaml body shaped like the MCP community-registry schema — GitHub raw URLs, internal HTTP servers, ad-hoc CDN links all work).
  • Docker Hardened Images (DHI) image ref (dhi.io/: or dhi.io/@sha256:... — the server.json manifest is extracted from the image's in-toto attestation via the OCI Referrers API).

Other image refs (inputs without "://" that are not dhi.io, e.g. docker.io/foo:tag) are no longer accepted. Use a server manifest instead.

SSRF guard and --skip-ssrf-check: A --url whose host resolves to a private/RFC1918, loopback, link-local, or cloud-metadata address is fetched anyway, but flagged: the add proceeds and a warning naming the resolved address is printed (this protects against manifest URLs that reach internal services, cloud metadata, or DNS-rebinding targets by making them visible, not by blocking them). Some legitimate servers live on private networks (split-horizon DNS, internal load balancers, VPN-only endpoints, PrivateLink), so their public hostname resolves to a private address and the warning is expected noise for them. OAuth authorization-server metadata has a separate SSRF guard that blocks disallowed addresses. Pass --skip-ssrf-check to disable both checks for this add, including OAuth metadata redirects, when you trust the provider and its discovery destinations; use it only for URLs you control.

OAuth for remote endpoints (--oauth-authorization-server / --client-id): Two related options configure OAuth for a remote --url server (both are only valid with --url):

--oauth-authorization-server hand-supplies the authorization-server metadata for a server that publishes no well-known RFC 9728/8414 metadata (e.g. Gmail). It is a local file path or an http(s) URL to a JSON document conforming to the RFC 8414 oauth-authorization-server shape (authorization_endpoint and token_endpoint are required). --client-id is required alongside it UNLESS the metadata document itself advertises a registration_endpoint, in which case a client is registered dynamically (RFC 7591) and --client-id may be omitted.

--client-id supplies a PRE-REGISTERED OAuth client. It may be given WITHOUT --oauth-authorization-server: the server's authorization metadata is then discovered normally and the supplied client is attached to it. This is the right mode for a server whose discoverable metadata exposes no registration_endpoint (so Dynamic Client Registration is impossible) but which accepts a client id the operator registered ahead of time.

Client secrets (confidential clients): There is no --client-secret flag. The secret for a confidential client lives in the secret store in the global scope under the service name "mcp::client_secret", and is read from there whenever the server is used:

  sbx secret set mcp:<server>:client_secret

Run it with no -t so the value is read from stdin instead of landing in
your shell history. The secret is never written to the MCP registration
on disk. Remove it later with 'sbx secret rm mcp:<server>:client_secret'.

The stored secret is bound to the OAuth identity (client id, issuer and
token endpoint) that first used it. Re-registering the same server name
against a different client or authorization server therefore does NOT
reuse it — store the secret again for the new client. To let a new
identity claim the existing secret, drop the recorded binding with
'sbx secret rm mcp:<server>:client_secret:identity'.

Two rules apply on the discovered path (they do not affect a server that advertises a registration_endpoint, the hand-supplied --oauth-authorization-server path, or --command servers):

- If the discovered authorization metadata has NO registration_endpoint,
  Dynamic Client Registration is impossible, so --client-id is REQUIRED;
  the add fails without it. This is the Slack shape (discoverable metadata,
  no DCR, a pre-registered client).
- A stored client secret is REQUIRED when the server's advertised
  token_endpoint_auth_methods_supported (RFC 8414) does NOT include "none"
  — i.e. it accepts only confidential clients (client_secret_basic /
  client_secret_post). Registration still succeeds without one, but the
  add-time authorization is skipped; store the secret and run
  'sbx mcp auth <server>' to finish. When the list includes "none" a
  public/PKCE client is allowed and --client-id alone is enough. If the
  server advertises no token_endpoint_auth_methods_supported at all (the
  field is optional in RFC 8414), the requirement cannot be determined and
  the add proceeds as usual.

Default OAuth scopes (--scope / --no-scope): --scope records the DEFAULT set of scopes to request at consent time for a remote --url OAuth server (repeatable). Precedence at authorization time is --no-scope > an explicit 'sbx mcp auth --scope' > the set recorded here > the scope set the RESOURCE itself says it requires (from its RFC 9728 protected-resource metadata or its WWW-Authenticate challenge) > whichever of openid, email, profile, and offline_access are advertised. Other advertised scopes are excluded from this fallback. If no scopes are selected, the scope parameter is omitted so the authorization server can apply its own default grant (RFC 6749 §3.3).

A resource that publishes a required set therefore gets it requested with no flag at all, and the consent block marks that set as derived rather than chosen. --no-scope suppresses all scope fallbacks and requests the server's default grant.

Scopes you name are checked against the union of two documents a server can publish: the authorization server's RFC 8414 scopes_supported and the resource's own RFC 9728 protected-resource metadata (some servers, e.g. GitHub, document their real scopes on the resource and advertise almost nothing over RFC 8414). A scope in neither prints a warning naming the offender but is still requested — a mismatch is often the server's own documentation gap, not a typo, and either document is allowed to be non-exhaustive. This is a recognition check, not a promise: a recognized scope can still be refused at consent time.

Scope values may be URN-shaped (urn:ietf:params:oauth:scope:mail) or URL-shaped (https://www.fastmail.com/dev/mcp). Neither needs quoting — a scope token cannot contain a space or a quote — and both are percent-encoded normally on the wire. --scope applies both to a hand-supplied override and to a plain --url server whose OAuth metadata is discovered.

RFC 8707 resource indicator (--resource): Every authorization request, code exchange and token refresh names the server the token is for, in the 'resource' parameter the MCP authorization specification requires (RFC 8707). That value is normally DERIVED, and you do not need this flag: it comes from the server's own RFC 9728 protected-resource metadata when it publishes some, and otherwise from the --url endpoint (lowercased scheme/host, path kept, fragment dropped).

--resource replaces that derivation with a value you supply. Use it when the derived one is wrong — most often a server whose published identifier is its bare origin (https://api.example.com) while its endpoint has a path (https://api.example.com/mcp), which is indistinguishable from publishing nothing, so the endpoint URL is what gets sent. An authorization server entitled to reject an unknown target answers 'invalid_target'.

The value must be an absolute URI with a scheme and a host, and RFC 8707 §2 forbids a fragment; a bad value fails the add rather than being repaired or dropped later. It is sent VERBATIM — nothing is lowercased and no path or trailing slash is adjusted, because the string names a server and changing it could name a different one. Only one value is accepted: a gateway backend connects to exactly one MCP endpoint.

It outranks both the published value and the URL derivation, and also an authorization server advertising resource_indicators_supported=false (the add says so when that happens). It is recorded on the registration, so there is no 'sbx mcp auth --resource': one value is used by the add-time authorization, every later 'sbx mcp auth', and every token refresh — a resource that differed between them is the audience mismatch this parameter exists to prevent. To change it, 'sbx mcp rm' the server and add it again; the existing token was minted for the old resource anyway.

Not 'audience': RFC 8707 'resource' is what the MCP specification requires, and an 'audience' parameter on an authorization-code request is an Auth0/Okta vendor extension that sbx does not send. See 'sbx mcp inspect ' for the effective value and where it came from.

Scope limit: this governs the OAuth flow sbx itself runs — a local data plane (SBX_MCP_URL=none), and any server registered with --oauth-authorization-server/--client-id. For a plain remote in hosted mode the control-plane gateway is the OAuth client and does not carry the field yet, so the value is recorded and the add warns that it is not sent for that server.

Custom request headers for remote endpoints (--header): --header adds an HTTP header to every request sent to a remote --url endpoint, written in the curl convention 'Header-Name: header value'. Repeat the flag for more headers; each header name may be given once. Headers the transport owns (Host, Content-Length, Connection, Proxy-*, …) are rejected.

Only a remote endpoint can carry them: --header is refused with --command and --local, and an add whose --url resolves to a stdio server (a registry or manifest URL naming an OCI image) fails rather than dropping them.

A header value may reference a secret with ${placeholder}. The placeholder is stored in the registration exactly as typed and the secret itself never is; the value is read from the encrypted secret store and substituted when a sandbox connects to the server. Store it with:

sbx secret set mcp:<server>:<placeholder>

For example:

sbx mcp add acme --url https://mcp.acme.com/mcp --header 'Authorization: Bearer ${api-key}'
sbx secret set mcp:acme:api-key

On an OAuth-protected server an explicit Authorization header takes precedence over the OAuth access token.

Header secrets are read from the LOCAL secret store, so a header-bearing server only works where this host connects it. Adding one while your MCP gateway is the hosted (SaaS) one is rejected rather than registered with headers that would be silently dropped — unless the server carries a hand-supplied OAuth override (--oauth-authorization-server), which this host connects directly in every gateway mode.

Alternative input — local stdio command (--command + --args): The command runs as a subprocess on the HOST, outside the sandbox.

WARNING: Local servers are for ad-hoc development only. They have no identity, no verifiable supply chain, and no sandboxing. The process runs with your host user's full permissions — it can read your filesystem, access your network, and call any API your user can. Do not use --command with untrusted executables.

Options

OptionDefaultDescription
--argsCommand-line arguments for the command
--callback-port0Local port to bind the OAuth callback listener to during add-time authorization (default: OS-assigned ephemeral port). Useful when the port must be pre-registered in an OAuth app's allowed redirect URIs. Applies to --url remote OAuth servers; ignored with a warning if the server turns out not to need OAuth.
--client-idOAuth client id for a pre-registered client (with --url; may be used with or without --oauth-authorization-server). A confidential client's secret comes from 'sbx secret set mcp:<server>:client_secret'
--commandExecutable to run for a local stdio server
--dirWorking directory (cwd) for a --command host server
--headerCustom HTTP header to send to a remote --url endpoint, in curl form 'Name: value' (repeatable). A ${placeholder} in the value is substituted at connect time from 'sbx secret set mcp:<server>:<placeholder>'
--localRun registry OCI server locally via docker run
--no-scopeRequest no scopes during add-time authorization, so the authorization server applies its own default grant. Suppresses required and OIDC fallback scopes; cannot be combined with --scope. Applies to --url remote OAuth servers.
--oauth-authorization-serverPath or http(s) URL to an RFC 8414 oauth-authorization-server metadata JSON document
--resourceRFC 8707 resource indicator to send during OAuth: an absolute URI (scheme and host, no fragment) naming the server the token is for. Overrides the value the server publishes in its RFC 9728 metadata and the one derived from --url; set it only when that derived value is wrong. Not honoured by the hosted gateway for a plain remote yet. Applies to --url remote OAuth servers.
--scopeDefault OAuth scope to request at consent time (repeatable; a scope the server's advertised metadata does not recognize prints a warning but is still requested, since advertising a scope never promised the server would grant it either). With no --scope, the scope set the resource itself requires is requested; with neither, whichever of openid, email, profile, and offline_access are advertised are requested; other advertised scopes are excluded from this fallback. Applies to --url remote OAuth servers.
--skip-authRegister an OAuth server without starting the hosted OAuth flow
--skip-ssrf-checkSkip SSRF checks for this add, including OAuth authorization-server metadata and redirects (operator asserts the provider is trusted)
--urlMCP server manifest URL, remote endpoint URL, or dhi.io image ref

Global options

OptionDefaultDescription
--cloudDispatch to Docker Cloud Sandboxes API instead of local sandboxd (supported by a growing set of verbs — run 'sbx --cloud --help' for the current list)
--cloud-api-urlhttps://api.sandboxes-cloud.docker.comCloud Sandboxes API base URL; only used with --cloud. Defaults to prod (https://api.sandboxes-cloud.docker.com). Set DOCKER_CLOUD_API_URL or pass this flag to override; a legacy value ending in /v1 is accepted.
-D, --debugEnable debug logging

Examples

# Remote MCP endpoint (OAuth auto-detected)
sbx mcp add notion --url https://mcp.notion.com/mcp
sbx mcp add linear --url https://mcp.linear.app/mcp

# MCP community-registry URL
sbx mcp add fetch --url https://registry.modelcontextprotocol.io/v0/servers/fetch-mcp/versions/latest

# Plain server-manifest URL (server.json / server.yaml)
sbx mcp add opine --url https://example.com/mcp/opine/server.yaml

# Docker Hardened Image (manifest is extracted from the image attestation)
sbx mcp add fetch --url dhi.io/fetch-mcp:latest

# Registry URL, local mode (runs on host via docker run; stdio packages only)
sbx mcp add fetch --local --url https://registry.modelcontextprotocol.io/v0/servers/fetch-mcp/versions/latest

# Private-network endpoint (host resolves to a private address) — opt out of the SSRF guard
sbx mcp add internal --url https://private.example.com/mcp --skip-ssrf-check

# Remote endpoint with a hand-supplied OAuth override (server publishes no
# well-known OAuth metadata): --oauth-authorization-server is a path or
# http(s) URL to an RFC 8414 metadata document, --client-id the OAuth client id
sbx mcp add acme --url https://mcp.acme.com/mcp --oauth-authorization-server ./acme-as.json --client-id my-client

# Pre-registered client on a DISCOVERABLE server that has no registration
# endpoint — no --oauth-authorization-server needed (metadata is discovered)
sbx mcp add slack --url https://slack.example.com/mcp --client-id my-preregistered-client

# Confidential client — store the secret first (prompted, never in argv or
# shell history), then register; the secret is read from the secret store
sbx secret set mcp:slack:client_secret
sbx mcp add slack --url https://slack.example.com/mcp --client-id my-preregistered-client

# Record default OAuth scopes to request at consent time (a scope the
# server's authorization metadata does not advertise warns but is still
# requested; repeat --scope for each one)
sbx mcp add acme --url https://mcp.acme.com/mcp --scope read --scope write

# URN- and URL-shaped scope values are ordinary scopes and need no quoting
sbx mcp add fastmail --url https://api.fastmail.com/mcp --scope https://www.fastmail.com/dev/mcp --scope offline_access

# Correct the RFC 8707 'resource' indicator when the derived one is wrong (a
# server whose published identifier is not its endpoint URL)
sbx mcp add acme --url https://mcp.acme.com/mcp --resource https://api.acme.com/mcp

# Remote endpoint with custom headers (curl convention; repeatable). The
# ${api-key} value is read from the secret store when a sandbox connects
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

# Local stdio command (runs on host — development only)
sbx mcp add github --command npx --args @modelcontextprotocol/server-github
sbx mcp add postgres --command docker --args "run,-i,--rm,mcp/postgres"

# Local stdio command with a working directory (cwd) for the host process
sbx mcp add local-fs --command node --args server.js --dir /srv/data