Skip to content

MCP Gateway

Purpose

Use the inference listener's MCP Streamable HTTP endpoint at POST /mcp to discover and invoke governed Connector tools. Mission Control publishes stable public names such as crm__lookup-customer, filters the catalog for the authenticated key owner, authorizes each call, and records Trace-native request and invocation evidence.

The current endpoint implements only MCP protocol revision 2026-07-28. It is stateless: every request is self-describing, and server/discover replaces the former handshake. Mission Control does not support initialize, protocol sessions, Mcp-Session-Id, legacy revisions, or automatic version negotiation. There is no automatic version negotiation or fallback on outbound Connector traffic either; a Connector server that does not advertise 2026-07-28 fails closed. Public results always identify mission-control-mcp-gateway; an upstream Connector's reserved io.modelcontextprotocol/serverInfo is overwritten at the gateway boundary.

Prerequisites

  • Use an MCP client that can be pinned to 2026-07-28. The official TypeScript SDK v2 client supports this mode.
  • Use a current personal or service virtual key for the Team. Discovery and listing return only visible eligible tools; calling a tool also requires Team allowance, current key policy with tool.invoke, matching discovery/schema state, an enabled eligible Connector, and available product limits.
  • Configure a Connector server that serves Streamable HTTP revision 2026-07-28; validate the files and restart gateway and Console its organization YAML, resolve its gateway-held credential, and complete Test connection plus Discover. Both administrator actions connect with that configured server credential. Test connection updates health only and does not replace schemas from the latest successful Discover.
  • For a browser client, add its exact origin to MCP_ALLOWED_ORIGINS and restart the gateway. Non-browser clients may omit Origin.
  • Run the following curl commands from any trusted client host that can reach the inference listener. The examples use fictional values and do not create a protocol session, new key, or Mission Control Run. Accept must contain exact application/json and text/event-stream media ranges with valid quality values greater than zero; wildcards do not substitute for either type.

Tool input schemas without $schema use JSON Schema 2020-12. The current SDK validator also accepts explicitly declared 2019-09, draft-07, and draft-06 dialects and rejects unsupported dialects. Network $ref values are not fetched automatically.

Procedure

1. Discover the server

Send the tagged per-request _meta envelope. io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities are required on every request; clientInfo is recommended and is display/debug metadata, not authority.

curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer mck_example_not_a_real_key" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, text/event-stream" \
  --header "MCP-Protocol-Version: 2026-07-28" \
  --header "Mcp-Method: server/discover" \
  --data '{"jsonrpc":"2.0","id":"discover-1","method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"example-client","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
  https://gateway.example.test/mcp

Expected shape:

{
  "jsonrpc": "2.0",
  "id": "discover-1",
  "result": {
    "resultType": "complete",
    "supportedVersions": ["2026-07-28"],
    "capabilities": { "tools": { "listChanged": false } },
    "ttlMs": 0,
    "cacheScope": "private",
    "_meta": {
      "io.modelcontextprotocol/serverInfo": {
        "name": "mission-control-mcp-gateway",
        "version": "1.0.0"
      }
    }
  }
}

The modern fields resultType, ttlMs, and cacheScope are emitted by the server. The server identity is self-reported display/debug metadata and never grants authority. cacheScope: "private" prevents sharing a filtered catalog across authorization contexts, while ttlMs: 0 makes it immediately stale.

2. List visible tools

curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer mck_example_not_a_real_key" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, text/event-stream" \
  --header "MCP-Protocol-Version: 2026-07-28" \
  --header "Mcp-Method: tools/list" \
  --data '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"example-client","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
  https://gateway.example.test/mcp

Each tool invocation uses its loaded Connector, declared schema hash and prepared credentials. Changes take effect after restart. Eligibility requires a matching declared schema from the last successful discovery of the same non-secret connection target key, plus current credentials, Team allowance, key policy, rules and approvals. Historical health failures never gate listing or invocation. Drift, undeclared/missing schemas, changed-target caches and disabled servers remain unavailable. Team allowance/key policy are live at authorization and budgets at admission.

Connector Discover, whether an administrator or the gateway's schedule ran it, stores one shared discovery result for each organization and server. tools/list reads that stored result and applies the current caller's authorization, Team allowance and key policy locally, including explicitly followed tool sets; it never contacts the upstream Connector to perform caller-specific discovery. The response is therefore a deterministic caller-specific list with resultType: "complete", ttlMs: 0, cacheScope: "private", and io.modelcontextprotocol/serverInfo. Omitted tools are not described. Listing consumes no product request limit and creates no gateway attempt or Connector invocation.

3. Call a tool

Mcp-Name is required for tools/call and must match params.name after MCP header decoding.

curl --fail-with-body \
  --request POST \
  --header "Authorization: Bearer mck_example_not_a_real_key" \
  --header "Content-Type: application/json" \
  --header "Accept: application/json, text/event-stream" \
  --header "MCP-Protocol-Version: 2026-07-28" \
  --header "Mcp-Method: tools/call" \
  --header "Mcp-Name: crm__lookup-customer" \
  --header "traceparent: 00-0123456789abcdef0123456789abcdef-0123456789abcdef-01" \
  --data '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"crm__lookup-customer","arguments":{"query":"customer-42"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"example-client","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
  https://gateway.example.test/mcp

If a property in the listed input schema declares, for example, "x-mcp-header": "Region", the request must mirror each non-null argument value in Mcp-Param-Region. Mission Control passes the already-discovered tool definition to the official v2 client, so the first outbound call carries the header without an invalid call, catalog refetch, or retry. The official client handles Mcp-Param-* and Base64 sentinel encoding automatically. A raw client must implement those rules exactly: malformed padding, invalid UTF-8, or an unencoded value that itself matches the =?base64?...?= sentinel is rejected with HTTP 400/-32020. Encode the complete literal value again when a real name or parameter has sentinel shape.

The gateway keeps one connected client per Connector server and outbound identity (the shared credential, or the calling person's own account) and shares it between concurrent calls, so a tool call after the first is one upstream round trip. A connection, timeout or authentication failure discards that client, an idle one is closed after five minutes, and a stop closes them all. A server whose calls ended unavailable three times in a row, timeouts included, is skipped by that gateway instance for 30 seconds, doubling on each failed probe up to five minutes: during the skip a call fails at once with the fixed HTTP 200/-32603 Tool invocation failed instead of waiting the server's timeoutMs again, and the first call after the skip probes the server: until that probe answers, other calls keep failing fast, an answer of any kind other than a timeout or a connection failure closes the skip, and a probe that reports nothing within a minute lets the next call probe again. The pooled client's connection handshake is bounded by timeoutMs too. The skip is per instance and is not stored.

A Connector configured as per-user acts as the calling person. When that person has not connected their own account under My connections, or the actual caller has no authorized person, tools/call returns HTTP 200 with result.isError: true, a text item that names the Connector and links to My connections, and _meta["mission-control/code"] of connector_user_auth_required; nothing is dispatched and no company credential is substituted. See Gateway errors and Use your own account with a Connector.

Two more answers come from Mission Control rather than the upstream. A rule an administrator declared on the tool's arguments or results can block the call: HTTP 200 / -32603 with the fixed Tool invocation blocked by policy and data.code of rule_blocked; a redaction instead changes the arguments the upstream receives or the result you read, marked [redacted:<rule id>], and is recorded on the decision only. An action tool that needs a person's approval pauses: HTTP 200 with result.isError: true, a text item linking to the approval page and _meta["mission-control/code"] of approval_required; nothing is dispatched, and repeating the identical call once it is approved runs it exactly once. See Rules on arguments and results, Approvals and Gateway errors.

4. Verify browser preflight

OPTIONS /mcp succeeds only for an exact configured origin requesting POST and approved headers. For example:

curl --include \
  --request OPTIONS \
  --header "Origin: https://agent-app.example.test" \
  --header "Access-Control-Request-Method: POST" \
  --header "Access-Control-Request-Headers: authorization, content-type, accept, mcp-protocol-version, mcp-method, mcp-name, mcp-param-region, traceparent" \
  https://gateway.example.test/mcp

Expected status and headers include:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://agent-app.example.test
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: authorization, content-type, accept, mcp-protocol-version, mcp-method, mcp-name, mcp-param-region, traceparent
Access-Control-Max-Age: 600
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers

The allowed request-header surface is Accept, Authorization, Content-Type, MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Param-*, traceparent, tracestate, baggage, and X-Mission-Control-Session-Id. Mission Control never returns a wildcard origin or enables cookie credentials. An unconfigured/missing origin, non-POST requested method, or any other requested header fails closed. Exact-Origin validation runs before method routing on every /mcp connection, including GET, DELETE, PUT, PATCH, and HEAD. An allowed Origin receives normal response CORS headers even when the method then returns 405; a denied Origin receives 403 first. Actual allowed-origin responses expose only traceparent, X-Mission-Control-Trace-Id, and X-Mission-Control-Correlation-Id to browser JavaScript.

Shared OAuth Connectors use the configured company grant, prepared after argument validation and before admission. Missing authorization, unavailable credentials and insufficient scopes produce the safe authentication error codes; no business tool replay occurs after 401/403. Callers continue to use their Mission Control key. See Shared MCP OAuth.

Expected result

  • Discovery advertises only 2026-07-28, tool capability, private zero-TTL caching, and Mission Control's display identity.
  • Listing contains only tools that are matched, healthy, enabled, bound, and policy-permitted for the caller.
  • A successful call returns a JSON-RPC result with resultType: "complete" and no Mcp-Session-Id.
  • Every accepted call returns safe Trace/correlation headers. inference_decision records the decision for the tool's resource ID (tool:<server-id>/<tool-name>) and, on completion, its terminal state, safe error code and any rule or approval that applied. The governance record stores neither tool arguments nor results.
  • Closing an in-flight HTTP response is cancellation in this revision. The client receives no later JSON-RPC body; an intermediary may record HTTP 499, while a dispatched operation whose upstream cancellation is unconfirmed remains indeterminate and keeps its limit lease until the lease expires.

Verification

Confirm all of the following:

  1. server/discover returns only supportedVersions: ["2026-07-28"].
  2. The response has no Mcp-Session-Id header.
  3. A request missing the envelope fails with HTTP 400/-32602.
  4. A required header that is missing or disagrees with the body fails with HTTP 400/-32020.
  5. A different protocol revision fails with HTTP 400/-32022 and data.supported: ["2026-07-28"]; the client does not send initialize afterward.
  6. An unknown RPC method returns HTTP 404/-32601. Posted JSON-RPC responses and batches return HTTP 400/-32600; a claim-less notification is accepted and dropped with HTTP 202/no body. GET, DELETE, PUT, PATCH, and HEAD return 405 with Allow: POST, OPTIONS after Origin validation.
  7. An allowed browser origin gets the exact-origin CORS headers, while a different origin gets 403 without Access-Control-Allow-Origin.
  8. A permitted call creates one governed attempt and, only after dispatch, one Connector invocation; a hidden or unauthorized tool reaches no upstream server and does not disclose its schema.

Failure recovery

  • HTTP 400/-32020: supply every required standard or Mcp-Param-* header and make its decoded value match the JSON-RPC body. Do not retry the same bytes.
  • HTTP 400/-32022: upgrade and pin the client or Connector server to 2026-07-28. Mission Control does not fall back to initialize or a legacy revision.
  • HTTP 401/-32001: install a current key or correct the signed managed-runtime call.
  • HTTP 403/-32003: correct the exact browser origin, Team state, allowance or key policy. Hidden-tool denials may instead be in-band HTTP 200/-32003 to avoid disclosure.
  • HTTP 404/-32601: use server/discover, tools/list, or tools/call; do not probe removed session methods.
  • HTTP 406/-32000: list both exact response media types with syntactically valid positive q-values; do not rely on */* or application/*.
  • HTTP 429/-31029: honor Retry-After and the safe error.data limit details, or change the relevant product limit. -31029 is Mission Control's application-defined limit code; it is outside the MCP-reserved range.
  • HTTP 499/no body: the caller closed the modern response. Reconcile authoritative evidence before retrying a side-effecting tool.
  • operation_denied or tool_ineligible: correct the Team/catalog state and discover again.
  • Upstream protocol or availability failure: restore the configured Connector. Never bypass the gateway with its credential. A server that failed three times in a row is skipped by each gateway instance for 30 seconds, doubling to five minutes, so a restored server is used again within one cool-down.
  • HTTP 503/-32003 Gateway is saturated with data.code of instance_saturated, or HTTP 429/-31029 with data.code of team_share_exceeded: this instance is full, or your Team holds half of it; nothing was recorded or dispatched. Honor retry-after (1 second); behind a load balancer the retry reaches another instance.
  • HTTP 200 result with isError: true and _meta["mission-control/code"] of connector_user_auth_required: the person connects their own account under My connections at the linked URL, then calls again. No company credential is substituted.
  • HTTP 200/-32603 with data.code of rule_blocked: an administrator's rule blocked the arguments or the result. The message is fixed and the decision names the rule; change the request or ask for the rule to be revised, and do not retry the same bytes.
  • HTTP 200 result with isError: true and _meta["mission-control/code"] of approval_required, approval_denied or approval_consumed: an Action tool is waiting for, was refused by, or already ran under a person's decision. Repeat the identical call after approval, read the note after a denial, and treat a consumed approval as executed once before asking again.
  • Unexpected tool failures: the public response is fixed HTTP 200/-32603 with Tool invocation failed; internal exception text, paths, and secrets are never relayed.