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 |
+------------------+
- Admin creates a
Providerwithtype: mcpandexternalAccess.enabled: true. - Admin (or the developer, via CLI) mints an
AccessKeybound to the Provider. - Developer runs
pai gateway mcp <name>to emit a.mcp.jsonsnippet. - 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
| Endpoint | Transport | Purpose |
|---|---|---|
GET /ext/mcp/{name}/sse | SSE | Server-to-client event stream |
POST /ext/mcp/{name}/message | SSE | Client-to-server JSON-RPC frame (policy-checked) |
POST /ext/mcp/{name} | Streamable HTTP | Single-request transport (newer MCP clients) |
All three go through the same AccessKey + Provider policy pipeline.
Access control
| Control | Where | Effect |
|---|---|---|
spec.externalAccess.enabled | Provider | Must be true for external requests |
spec.policy.mcp.allowedTools | Provider | Allowlist of callable tools |
spec.policy.mcp.deniedTools | Provider | Denylist (evaluated first) |
spec.restrictions.allowedMcpTools | AccessKey | Further narrows the Provider allowlist for this key |
spec.restrictions.deniedMcpTools | AccessKey | Per-key tool denies |
spec.restrictions.allowedCIDRs | AccessKey | Client IP allowlist |
spec.limits.maxRequestsPerDay | AccessKey | Hard daily request cap |
spec.externalAccess.maxRequestsPerDay | Provider | Per-provider daily cap across all keys |
allowedCIDRsThe 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.requestsTodayAccessKey.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. LocalstdioMCP servers remain in-cluster — agents inside the cluster reach them via the harness, not the gateway. - Batched JSON-RPC. If any
tools/callin a batch is denied, the whole batch is denied. - Body cap.
POST /ext/mcp/{name}/messageaccepts up to 1 MiB per request.
Related
- LLM Gateway — the LLM-side companion to this document.
- External Provider Gateway — unified AccessKey model across LLM, Provider, and MCP surfaces.
- MCP Server provider — how to give cluster-side agents access to MCP servers through the sidecar proxy.