Skip to content

HTTP surface reference ​

Every route the gateway serves, by prefix. All of them live on the one port the gateway listens on; the reverse proxy needs no path-based routing.

MCP endpoints ​

MethodPathNotes
POST/{endpoint}/mcpJSON-RPC over Streamable HTTP for every supported MCP revision.
GET/{endpoint}/mcpServer-to-client event stream for handshake-era sessions. Requires Mcp-Session-Id; 405 without one.
DELETE/{endpoint}/mcpEnds a handshake-era session. Requires Mcp-Session-Id (405 without one) and the endpoint's credential.

Responses:

StatusWhen
200A JSON-RPC result, as JSON or as an SSE stream. A tools/call is always streamed.
202A client's JSON-RPC response to a server request, accepted on a session.
204DELETE ended the session.
400A JSON-RPC error, including a malformed envelope. See Request envelope.
401Missing or invalid credentials, with a WWW-Authenticate challenge carrying the endpoint's resource_metadata. A bearer that was presented and refused (issued for another endpoint, expired, revoked or unknown) adds error="invalid_token", which tells an OAuth client to authorise again for this endpoint; a request with no credential gets the challenge without an error.
403A browser request from an origin the endpoint does not allow, as a JSON-RPC error with id: null, before any credential is checked.
404An unknown or disabled endpoint, or a session id the gateway does not hold for this endpoint and caller.
405GET or DELETE without Mcp-Session-Id, with Allow: POST.
413A request body over the limit (4 MiB by default), as a JSON-RPC error with id: null.
429A rate limit, with Retry-After.
503The live-state store (Valkey) is unreachable, with Retry-After. The session is kept; retry it.

CORS on these routes follows each endpoint's allowed browser origins. For a request whose Origin the endpoint allows (or the gateway's own), the response reflects it in Access-Control-Allow-Origin (with Vary: Origin) and exposes Mcp-Session-Id and WWW-Authenticate, and an OPTIONS preflight is answered for GET, POST and DELETE with the headers the MCP transport sends (Authorization, Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version, Mcp-Method, Last-Event-ID, X-API-Key), cacheable for ten minutes. A request or preflight from any other origin gets 403 and no CORS headers. A request without an Origin (any client that is not a browser) is not affected. Credentials are never allowed, and these routes never authenticate by cookie: a caller presents a bearer token or an API key in a header. Answers given before the endpoint is resolved (404 for an unknown endpoint, a parse error, 413) carry no CORS grant. BUNDLEMCP_WEB_ORIGINS governs /api/v1 and the rest of the gateway only.

Request envelope ​

The gateway serves two MCP eras on the same URL and decides which one a POST belongs to from the request itself. This is the part of the surface an SDK normally hides, so it matters only if you are building requests by hand.

The request carriesEraSession
params._meta with a protocol versionStatelessNone; each request stands alone
Mcp-Session-Id, from an earlier initializeSessionHeld in Valkey
Neither—Refused, 400

Stateless requests ​

A stateless request repeats its context every time. Both _meta keys below are required, and three headers must mirror the body exactly:

WhereKeyValue
params._metaio.modelcontextprotocol/protocolVersion2026-07-28
params._metaio.modelcontextprotocol/clientCapabilitiesAn object; {} is valid
params._metaio.modelcontextprotocol/clientInfoOptional {"name":…,"version":…}
HeaderMCP-Protocol-VersionEqual to the _meta protocol version
HeaderMcp-MethodEqual to the body's method
HeaderMcp-NameFor tools/call, prompts/get and resources/read only: equal to the body's name (or uri for resources/read)

A tool whose input schema annotates a property with x-mcp-header also expects that argument mirrored as Mcp-Param-<Name>.

The mirrored headers exist so a proxy can route on them without parsing the body. They are checked, not merely read: any disagreement with the body is refused rather than resolved in the body's favour.

bash
V=2026-07-28
curl -s https://gw.example.com/assistant-ci/mcp \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "MCP-Protocol-Version: $V" \
  -H "Mcp-Method: tools/list" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{
        "io.modelcontextprotocol/protocolVersion":"'"$V"'",
        "io.modelcontextprotocol/clientCapabilities":{}}}}'

Errors from this validation are JSON-RPC errors with HTTP 400:

CodeNameCause
-32602invalid paramsparams._meta is absent, not an object, or missing a required key
-32020header mismatchMCP-Protocol-Version, Mcp-Method, Mcp-Name or an Mcp-Param-* header disagrees with the body, or is absent
-32022unsupported protocol versionThe requested revision is not served; data lists supported and requested
-32601method not foundAnswered with HTTP 404

Session requests ​

A session client opens with initialize, receives Mcp-Session-Id, and sends it on every later request; none of the _meta or mirrored-header rules apply. Served revisions are 2025-11-25, 2025-06-18 and 2025-03-26, newest first. Setting BUNDLEMCP_LEGACY_PROTOCOL_ENABLED=false serves the stateless era only.

Every official SDK speaks one era or the other, so the practical advice is to use one — see Scripts and CI.

OAuth authorization server ​

MethodPathNotes
GET/.well-known/oauth-authorization-serverRFC 8414 metadata.
GET/.well-known/oauth-protected-resource/{endpoint}/mcpProtected-resource metadata for one endpoint.
POST/oauth/registerRFC 7591 dynamic client registration; public clients, redirect URIs must be https or localhost and on BUNDLEMCP_DCR_ALLOWED_REDIRECT_URIS when it is set. Rate-limited per IP unless every redirect URI is allow-listed, and capped by a global quota of unused clients; either limit answers 429 with too_many_requests. Metadata is bounded: at most 10 redirect URIs of at most 2048 characters each, a client_name of at most 200 characters, a scope of at most 1000, and at most 10 grant_types and 10 response_types; more is refused with 400 invalid_client_metadata.
GET/oauth/authorizeAuthorization code with S256 PKCE. Accepts resource (RFC 8707) naming the endpoint. Redirects to the identity provider when no browser session exists. Client ids that are https:// URLs are resolved as client ID metadata documents, fetched from public addresses only, at most 16 KiB within 5 seconds; a URL that failed is refused for a minute without a new fetch.
POST/oauth/consentThe consent form's submit.
POST/oauth/tokenauthorization_code and refresh_token grants. Refresh tokens rotate; reuse of a rotated token revokes the whole family.
POST/oauth/introspectRFC 7662. Requires a service API key, as a bearer, in X-API-Key, or as the HTTP Basic client secret.
POST/oauth/revokeRFC 7009. The client may revoke only its own tokens.

Every authorization response, including error redirects, carries iss=<issuer> (RFC 9207).

Browser login and upstream connect ​

MethodPathNotes
GET/login?next=Starts the OIDC login; returns to next afterwards. Sets the login binding cookie (below).
GET/login/callbackThe redirect URI to register with the identity provider. Answers 400 unless the browser presents the binding cookie of the login it finishes; it always clears that cookie.
POST/logoutEnds the bundlemcp session.
GET/oauth/upstream/{server}/connectStarts the upstream OAuth flow for a server. Admins establish the shared connection; ?as=me connects the signed-in user's own account on a per_user server.
GET/oauth/upstream/callbackThe redirect URI to register with upstream OAuth providers: <public url>/oauth/upstream/callback.

A login can be finished only by the browser that started it, so a callback link handed to someone else signs nobody in. /login sets a binding cookie holding a fresh random value, and the stored login state keeps only its hash. The cookie is HttpOnly, SameSite=Lax, host-only on Path=/, and lives 10 minutes, as long as the login state. On an https public URL it is named __Host-bmcp_login and is Secure; on plain http it is named bmcp_login. Starting a second login in the same browser replaces the cookie, so the earlier tab's callback answers 400 and that login must be started again. A login started before an upgrade to this behaviour fails the same way once.

These pages, and the consent page GET /oauth/authorize renders, carry hardening headers: a CSP with default-src 'none', frame-ancestors 'none' and form-action 'self', plus X-Frame-Options: DENY and Cache-Control: no-store. The consent page's form-action also names the redirect URI of the authorization it decides (scheme, host, port and path), because browsers apply form-action to the redirect that delivers the code or access_denied to the client. A redirect URI that cannot be written into a CSP (for example one whose path holds ; or ,) keeps the page same-origin, and Approve or Deny then answers with a page linking to the client instead of a redirect.

Management API ​

PathNotes
/api/v1/…See Management API.
/api/v1/docsInteractive documentation.
/api/v1/openapi.jsonThe OpenAPI document.

CORS on /api/v1 is restricted to BUNDLEMCP_WEB_ORIGINS.

Probes ​

MethodPathNotes
GET/healthzLiveness. Unauthenticated.
GET/readyzReadiness: Postgres answers a trivial query and Valkey answers a ping. Unauthenticated.

Reserved names ​

Because endpoints are served at the root, these first path segments cannot be endpoint names: api, oauth, .well-known, healthz, readyz, metrics, docs, login, consent, static.

Limits ​

LimitDefault
Request body4 MiB (max_request_bytes), rejected with 413 before it is parsed, chunked bodies included
Request body under /oauth/64 KiB, fixed; rejected with 413 and invalid_request before it is parsed, chunked bodies included
One upstream message16 MiB (max_upstream_response_bytes); a larger tool result is answered with an error
Single tool call60 s, raised to at most 1800 s by runtime configuration
Access token lifetime1 hour
Refresh token lifetime7 days
Authorization code lifetime10 minutes, single use
Admin session maximum age12 hours
Session idle expiry24 hours
Tool-list cache hint300 s
Auth-failure lockout20 failures per 300 s per IP

Next steps ​