Skip to main content

MCP Gateway

The Pai MCP Gateway lets developers outside the cluster reach MCP servers through Pai instead of connecting directly. The laptop never holds the real MCP bearer token — Pai injects it server-side — and every tools/call goes through the same tool allow/deny policy you configure for in-cluster agents.

This is the companion to the LLM Gateway: same token type (AccessKey), same pai gateway … CLI pattern, different upstream (MCP server instead of LLM provider).

How it works

Developer laptop                    Pai cluster                    MCP server
+---------------+ HTTPS +------------------+ HTTPS +---------------+
| Claude Code | -----------> | Pai Gateway | ----------> | mcp.linear.app|
| (any MCP | pak_... | - auth check | real key | mcp.notion.com|
| client) | AccessKey | - tool policy | from | ... |
+---------------+ | - per-key caps | Secret +---------------+
| - audit log |
+------------------+
  1. Admin creates a Provider with type: mcp and externalAccess.enabled: true.
  2. Admin (or the developer, via CLI) mints an AccessKey bound to the Provider.
  3. Developer runs pai gateway mcp <name> to emit a .mcp.json snippet.
  4. Claude Code / any MCP client talks to /ext/mcp/{name} on the gateway; Pai validates policy, injects the real token, and forwards.

The developer's machine never sees the upstream MCP token.

Setup

1. Create an MCP Provider with external access

apiVersion: pai.io/v1
kind: Provider
metadata:
name: notion-docs
namespace: team-a
spec:
type: mcp
mcp:
transport: sse
url: https://mcp.notion.com/sse
auth:
type: api-key
secretRef: notion-mcp-token # Pai secret with key "token"
policy:
mcp:
allowedTools: [search_pages, fetch_document]
deniedTools: [delete_page]
externalAccess:
enabled: true
maxRequestsPerDay: 2000

policy.mcp.allowedTools / deniedTools govern tools/call frames. The Provider's tool list is discovered from the upstream tools/list response and filtered before forwarding.

2. Mint an AccessKey for the developer

pai access-key create --name alice-laptop \
-n team-a \
--provider notion-docs \
--allowed-cidr 10.0.0.0/8 \
--allowed-mcp-tool search_pages

The CLI prints the raw pak_... once — store it securely. AccessKey restrictions can only narrow the Provider's policy, never widen it. Rotate with pai access-key rotate alice-laptop.

3. Emit the client config

pai gateway mcp notion-docs --key alice-laptop > ~/.mcp-notion.json

Point your MCP client (Claude Code, Cline, etc.) at the generated file. A typical .mcp.json looks like:

{
"mcpServers": {
"notion-docs": {
"transport": "sse",
"url": "https://gateway.pairun.dev/ext/mcp/notion-docs/sse",
"headers": {
"Authorization": "Bearer pak_..."
}
}
}
}

The gateway handles transport negotiation — SSE for older clients, Streamable HTTP (POST /ext/mcp/{name}) for newer ones.

Gateway endpoints

EndpointTransportPurpose
GET /ext/mcp/{name}/sseSSEServer-to-client event stream
POST /ext/mcp/{name}/messageSSEClient-to-server JSON-RPC frame (policy-checked)
POST /ext/mcp/{name}Streamable HTTPSingle-request transport (newer MCP clients)

All three go through the same AccessKey + Provider policy pipeline.

Access control

ControlWhereEffect
spec.externalAccess.enabledProviderMust be true for external requests
spec.policy.mcp.allowedToolsProviderAllowlist of callable tools
spec.policy.mcp.deniedToolsProviderDenylist (evaluated first)
spec.restrictions.allowedMcpToolsAccessKeyFurther narrows the Provider allowlist for this key
spec.restrictions.deniedMcpToolsAccessKeyPer-key tool denies
spec.restrictions.allowedCIDRsAccessKeyClient IP allowlist
spec.limits.maxRequestsPerDayAccessKeyHard daily request cap
spec.externalAccess.maxRequestsPerDayProviderPer-provider daily cap across all keys
Client IP for allowedCIDRs

The gateway trusts X-Forwarded-For only when the direct peer is in the configured gateway.externalProviderGateway.trustedProxies. Otherwise the peer address is used. See the External Provider Gateway guide for details.

Audit

Every /ext/mcp/* request is logged in the gateway audit chain with source=external, access_key=…, provider=…, tool=…, client_ip=…. Usage counters are exposed on the CRs:

  • Provider.status.requestsToday
  • AccessKey.status.requestsToday

Switch the Provider to audit.enforcement: audit to log policy violations without blocking — useful when rolling out a new tool allowlist.

Limitations (v1)

  • Transport. Only sse (and Streamable HTTP) are supported externally. Local stdio MCP servers remain in-cluster — agents inside the cluster reach them via the harness, not the gateway.
  • Batched JSON-RPC. If any tools/call in a batch is denied, the whole batch is denied.
  • Body cap. POST /ext/mcp/{name}/message accepts up to 1 MiB per request.