Skip to main content

Permit MCP Gateway architecture

Learn how Permit MCP Gateway is built: which components run, where data lives, how a tool call is authorized, and how upstream Model Context Protocol (MCP) servers verify gateway traffic. This page is for platform operators, security architects, and decision makers who review the gateway before a rollout or plan a self-hosted deployment. For what the gateway does and why, start with the Permit MCP Gateway overview.

Architecture at a glance

Permit MCP Gateway is a proxy between MCP clients (such as Cursor, Claude Desktop, and Claude Code) and upstream MCP servers. For each connection and tool call, the gateway does three things:

  1. The consent service authenticates the user and records which trust level the user grants to the agent.
  2. The gateway calls permit.check() on each call_tool request, and Permit.io evaluates the call against policy.
  3. Permit.io records each decision in the audit log.

The design has these properties:

PropertyHow it works
Transparent proxyMCP clients connect to the gateway URL instead of the upstream server URL. The upstream MCP server receives proxied requests and needs no code changes.
Identity bindingEach tool call is tied to a human (the user who signed in) and an agent (the MCP client acting for that user), through a relationship-based access control (ReBAC) model.
Policy check on each tool callThe decision uses the trust level the user granted the agent, the maximum trust level the admin granted the user, and the trust level of the tool.
Server-side sessionsThe gateway stores upstream OAuth tokens and session state in Redis, so MCP clients never hold upstream tokens and users don't sign in again on each connection.
Host isolationEach host (a named gateway instance) has its own subdomain, policies, users, and sessions.

Deployment models and where each component runs

The same gateway software runs in three deployment models. The components on this page are the same in each model. What changes is who runs them and where the control plane and the policy decision point (PDP) run.

ComponentHosted (SaaS)Customer-controlledFully on-premises
Gateway and consent servicePermit runs them on *.agent.securityYou run them in your networkYou run them in your Kubernetes cluster, on your own domain
Admin dashboardPermit runs it on app.agent.securityContact Permit for your deploymentYou run it in your Kubernetes cluster, on your own domain
PDPPermit.io Cloud PDPA PDP in your networkA PDP in your cluster, set per host in the admin dashboard
Control plane (policies, audit logs)Permit.io cloudPermit.io cloudYour self-hosted Permit Platform

The diagrams on this page use the hosted names (*.agent.security, app.agent.security, Cloud PDP). In a self-hosted deployment, replace them with your own domain, your PDP, and your Permit Platform. For the full comparison of the three models, see Enterprise deployment. To install the gateway in your cluster, see On-prem installation.

Permit.io as control plane and data plane

Permit.io is the control plane and the default data plane for Permit MCP Gateway.

  • Control plane: Permit.io holds the authorization model: resource types, actions, roles, relations, derived roles, and role assignments. The admin dashboard (app.agent.security) and the consent service write policy changes through the Permit API. Admins can also inspect and edit the same policies in the Permit dashboard (app.permit.io).
  • Data plane: the Permit PDP evaluates each permit.check() call from the gateway. The hosted gateway uses the Cloud PDP. Self-hosted deployments use a PDP in your network, so authorization decisions stay inside your network.

Each gateway host maps to one Permit environment. The admin sets this link when creating the host by selecting a Permit project and environment. All resources, users, roles, and audit logs for the host live in that environment.

Because gateway policies are standard Permit objects, they work with Permit's role-based (RBAC), attribute-based (ABAC), and relationship-based (ReBAC) access control models, with policy updates through Open Policy Administration Layer (OPAL), and with the Permit audit log API. For how hosts, tools, and trust levels map to Permit objects, see How Permit MCP Gateway works with Permit.io.

System components

Click to enlarge
ComponentHosted addressWhat it does
Admin dashboard (Platform UI)app.agent.securityAdmins manage hosts, MCP servers, humans, and agents. See the admin dashboard reference.
Edge router (NGINX)<host>.agent.securityRoutes each request by path to the gateway or the consent service.
Gateway<host>.agent.security/mcpProxies MCP traffic and enforces authorization on each tool call.
Gateway Admin APIInternalStores host configuration, sessions, and OAuth state in Redis for the admin dashboard and the consent service.
Consent service<host>.agent.securityOAuth 2.1 authorization server: user sign-in, consent, and upstream MCP OAuth. See Consent service.
Permit PDPCloud PDP, or your PDP when self-hostedEvaluates each permit.check() call.
RedisInternalHost configuration, MCP sessions, upstream OAuth tokens, and the gateway JWT signing key. Only the gateway reads and writes Redis.
PostgreSQLInternalUser accounts, OAuth clients, and authentication sessions for the consent service.
Only the gateway accesses Redis

The consent service doesn't connect to Redis. The consent service stores session and OAuth state through the Gateway Admin API, so the Redis schema lives in one component.

Where data lives and how it flows

FromToWhat moves
MCP clientEdge routerMCP Streamable HTTP requests (/mcp), OAuth discovery and registration (/.well-known/*, /oauth/*), and browser traffic for sign-in and consent.
Edge routerGateway/mcp, /oauth/*, and /.well-known/* requests.
Edge routerConsent serviceSign-in, consent, and upstream OAuth callback requests (/login, /consent, /api/auth/*, /api/consent/*, /api/mcp/*).
Consent serviceGateway Admin APISessions, OAuth states, and pending upstream tokens, over HTTP with a bearer token.
Gateway Admin APIRedisAll Redis reads and writes.
Consent servicePostgreSQLUsers, authentication sessions, and OAuth clients.
GatewayConsent serviceJSON Web Key Set (JWKS) fetch to verify access tokens. The gateway caches the keys.
GatewayPermit PDPOne permit.check() call for each tool call.
Consent servicePermit APIResource instances, relations, and role assignments written when a user accepts consent.
GatewayUpstream MCP serverMCP Streamable HTTP with the upstream OAuth token and a signed X-Gateway-Auth JWT. See Upstream authentication.

In the hosted gateway, MCP traffic, including tool call parameters and upstream responses, passes through Permit's managed infrastructure. To keep that traffic inside your network, use a customer-controlled or fully on-premises deployment.

Policy model

Trust levels for tools

Each tool on an MCP server has a trust level. When an admin imports an MCP server, the admin dashboard suggests a trust level from the tool name. The admin can override the trust level for any tool.

Trust levelSuggested for tool names that containTypical operations
Highdelete, remove, destroy, drop, purge, erase, truncate, terminate, kill, revokeDestructive
Mediumcreate, write, update, set, modify, edit, put, post, insert, add, send, execute, run, invoke, submit, push, publish, deploy, apply, patchWrite
LowAny other nameRead

The high-level words are checked first, so a tool named delete_and_create gets High. Trust levels are hierarchical: an agent with a higher trust level can call all tools at lower levels.

How the gateway builds ReBAC policy

Permit MCP Gateway generates Google Zanzibar-style ReBAC policies from:

  • The roles for users and agents.
  • The MCP servers imported into the host.
  • The trust level each user grants an agent during consent.
  • A user_profile resource that links each user to the agents acting for that user.

Each MCP server maps to a Permit resource type whose key is the server key, such as linear. The server's tools become actions on that resource type, using slugified tool names such as create_issue. There are no separate resource instances per tool: the server is both the resource type and the single resource instance.

The agent's permission on a server comes from a three-part chain: Agent has a role on the UserProfile, and the UserProfile has a relation to the MCP server. The effective permission is min(agent_role_on_profile, profile_relation_to_server). The human's relation to the server is a ceiling: an agent can't use a higher trust level than the admin granted the human, whatever trust level the user selected during consent.

For the full derivation, see Authorization: trust ceiling.

Authentication and authorization flows

How users and agents are identified

The gateway builds a Permit user key from the caller type:

  • Human: human|{subject}, where subject is the signed-in user's ID. The consent service and the admin dashboard use this key for policy management, such as granting a user access to an MCP server.
  • Agent: agent|{client_id}, where client_id is the OAuth client ID of the MCP client. The gateway uses this key in the permit.check() call for each tool call.

The flows below run in this order:

  1. Admin setup, once per host: the admin creates a host, imports MCP servers, and grants users access.
  2. User connection, once per MCP client and server: the user signs in, picks a server, completes upstream OAuth if the server requires it, and grants a trust level. The gateway then issues a JWT access token to the MCP client.
  3. Each tool call: the gateway verifies the JWT, identifies the agent, checks Permit, and proxies allowed calls.

Sequence: admin setup flow

The admin creates a host in the admin dashboard by selecting a Permit project and environment. The admin dashboard gets the environment's API key, the gateway resolves the key's scope, and the gateway stores the host configuration in Redis.

For the admin steps in the dashboard, see Set up hosts for your organization.

The first time a user connects an MCP client, the client discovers the gateway's OAuth endpoints and the user signs in and grants consent. If the upstream MCP server requires OAuth, the consent service runs that OAuth flow and stores the upstream token in the gateway. The diagram shows the protocol messages. For each screen the user sees, and for session expiry and re-consent, see Consent service.

Server allow-list

By default, a user can connect only to MCP servers that the admin imported and granted the user access to. The consent service checks that the upstream URL matches one of those servers. If the admin turns on Dynamic MCPs for the host, users can also enter their own upstream MCP server URL during consent. See Dynamic MCPs.

Sequence: authorization decision flow

When a call_tool request reaches the gateway, the gateway runs these steps before it proxies the call to the upstream MCP server.

A denied call returns MCP error code -32004 (Permission denied) to the MCP client. When a host uses Agent Interrogation or a tool requires human-in-the-loop approval, those checks run in the same chain before the call reaches the upstream server. To investigate a decision, see Audit logs.

Tool lists are not filtered

The gateway returns all upstream tools in list_tools responses, whatever the agent's trust level. Enforcement happens on call_tool, so an agent can see a tool that it can't call.

Authorization: trust ceiling (min logic)

The effective permission on an MCP server is the minimum of two values: the agent's trust level and the human's relation to the server. The human's relation is a ceiling on what any of that human's agents can do.

Permit implements min() with nine derived role rules for each MCP server:

Agent role on profileProfile relation to serverEffective server role
{server}-highhighhigh
{server}-highmediummedium (capped)
{server}-highlowlow (capped)
{server}-mediumhighmedium
{server}-mediummediummedium
{server}-mediumlowlow (capped)
{server}-lowhighlow
{server}-lowmediumlow
{server}-lowlowlow

The effective role decides which tools the agent can call:

Tool trust levelEffective roles that can call the tool
Lowlow, medium, high
Mediummedium, high
Highhigh

Example outcomes by agent

The trust level a user selects during consent becomes the agent's role. This table shows example results for fictional users, assuming each user's maximum trust level is at least the level granted:

Agent (MCP client)UserMCP serverTrust level grantedExample allowed toolsExample denied tools
Cursoralicelinear_mcpMediumget_issues, create_issuedelete_project
Claude Desktopalicegithub_mcpLowlist_repos, get_filecreate_issue, delete_repo
VS Code Copilotbobgithub_mcpHighget_file, create_issue, delete_repoNone: High includes all tools
Claude Codecarolslack_mcpMediumsearch_messages, send_messageremove_member

To change these grants in the admin dashboard, see Managing humans and agents.

Upstream authentication (gateway JWT)

When the gateway proxies a request to an upstream MCP server, it adds a short-lived JSON Web Token (JWT), signed by the gateway, in the X-Gateway-Auth header. The upstream OAuth token stays in the Authorization header. An upstream server that verifies the X-Gateway-Auth JWT can reject agents that connect to the upstream URL directly and skip the gateway's authentication, authorization, and consent checks.

The gateway always sends the header, and there is nothing to turn on. Verification is up to the upstream server. An upstream server that doesn't verify the header ignores it and behaves as before.

How the gateway signs upstream requests

  • The gateway signs JWTs with an Ed25519 key stored in Redis and shared by all gateway replicas, so any replica serves the same public key.
  • The public key is published at GET /.well-known/gateway-jwks.json on the host. The endpoint needs no authentication and sends Cache-Control: public, max-age=300 (5 minutes).
  • Each JWT carries the signed-in user's ID, the upstream URL as audience, and the host subdomain.
  • The gateway reuses a JWT until 30 seconds before it expires, then signs a new one. Long-lived MCP sessions always send a valid token without signing on every request.

JWT claims

ClaimValuePurpose
issagent-security-gatewayIdentifies the issuer
subSigned-in user IDThe user the request is made for
audCanonical upstream URL: host lowercased, all trailing slashes (including the root /) removedIntended recipient
expIssue time plus the token lifetime (5 minutes by default)Token expiry
iatIssue time minus 30 seconds (clock-skew leeway)When the token was issued
nbfIssue time minus 30 seconds (clock-skew leeway)Not valid before this time
jtiUnique UUIDUnique token ID
tenantHost subdomainThe host the request belongs to

Two authentication headers on upstream requests

Each upstream request carries two headers with different purposes:

HeaderIssued byWhat it proves
Authorization: Bearer <token>The upstream server's OAuth providerThe user authorized this request with the upstream service
X-Gateway-Auth: Bearer <jwt>The gatewayThe request came from this gateway host

Verify gateway requests on the upstream MCP server

To reject traffic that doesn't come through the gateway, verify the X-Gateway-Auth header on each request against the gateway's JWKS. This example uses the jose library in a Node.js request handler, where req and res are the HTTP request and response:

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
new URL('https://<tenant>.agent.security/.well-known/gateway-jwks.json')
);

const auth = req.headers['x-gateway-auth'];
if (typeof auth !== 'string' || !auth.startsWith('Bearer ')) {
return res.status(401).end();
}

const { payload } = await jwtVerify(
auth.slice(7).trim(),
JWKS,
{
issuer: 'agent-security-gateway',
audience: 'https://your-mcp-server.example.com', // canonical: lowercase host, no trailing slash
algorithms: ['EdDSA'],
clockTolerance: 30,
}
);

Replace these values:

PlaceholderReplace with
<tenant>Your host subdomain. In a self-hosted deployment, use your gateway base domain instead of agent.security.
https://your-mcp-server.example.comThe canonical form of your upstream URL: host lowercased and all trailing slashes removed. For example, https://MCP.Example.com/ becomes https://mcp.example.com, and https://your-server.example.com/v1/ becomes https://your-server.example.com/v1.

jwtVerify throws an error when the signature, issuer, audience, or time claims don't match. Return HTTP 401 in that case, and for any request without the header. A request that passes verification returns the claims in payload, with the user ID in payload.sub.

The JWKS URL and a sample verification snippet for each host are in the admin dashboard under Settings > Upstream Authentication (JWT).

Set a clock tolerance of at least 30 seconds

The gateway backdates iat and nbf by 30 seconds. If your verifier's clock runs ahead of the gateway's clock and the verifier has no clock tolerance, the verifier rejects valid tokens. Set a tolerance of at least 30 seconds, such as clockTolerance: 30 in jose.

Replay protection is up to the verifier

The jti claim is unique per token, but jwtVerify doesn't check whether it has seen a jti before. For replay protection, store each jti you accept for at least the token lifetime plus 30 seconds, for example in a Redis set with that expiry, and reject a jti that is already stored.

Signing key rotation

The gateway doesn't rotate its signing key automatically. After the key is 90 days old, the gateway logs a warning and reports the key's age in the gateway_jwt_signing_key_age_seconds metric. Each JWT has a kid (key ID) header. A JWT library with JWKS caching, such as jose, fetches the JWKS again when it sees an unknown kid, so a rotation needs no change on upstream verifiers.

If you run the gateway yourself, contact your Permit team for the rotation procedure for your deployment.

Rate limiting

The hosted gateway limits requests per client IP address on authentication, OAuth client registration, and MCP endpoints. There is nothing to configure. A request over a limit gets HTTP 429 with this JSON body:

{
"error": "rate_limited",
"message": "You have exceeded the rate limit. Please try again later."
}

Permit sets the limit values and can change them, so this page doesn't list them. The limits allow for corporate networks where many users share one public IP address. If users behind one IP address get HTTP 429 responses in normal use, contact Permit support.

Handle HTTP 429 responses in MCP clients

When an MCP client or agent gets an HTTP 429 response:

  1. Check the response body. An error value of rate_limited means a gateway rate limit was hit, not an application error.
  2. Wait and retry. Back off for a few seconds before retrying, and increase the wait on each retry.
  3. Reduce concurrency. If several agents share one IP address, lower the number of parallel requests.

Dynamic MCPs

Dynamic MCPs let users connect to an MCP server URL of their choice during consent, in addition to the servers the admin imported. To turn on Dynamic MCPs, see Admin dashboard reference: Dynamic MCPs.

In Permit, the consent service creates a separate resource type for each user's dynamic MCP server. The resource type key is {serverKey}-{userId}, and the resource has the attribute mcp_server_type: "dynamic". The host-level setting is enforced through a connect_dynamic_mcp action on the user_profile resource.

Glossary

TermMeaning
MCPModel Context Protocol, an open standard that connects AI tools to external services
HumanThe user who signs in and delegates access to agents
AgentAn MCP client that acts for a user
HostA named gateway instance with its own subdomain, policies, users, and sessions
Trust levelRisk classification (low, medium, high) that decides which tools an agent can call
PDPPolicy decision point, the Permit component that evaluates authorization requests
HITLHuman-in-the-loop approval of a tool call
ReBACRelationship-based access control
OPALOpen Policy Administration Layer, which pushes policy updates to PDPs
OPAOpen Policy Agent, the policy engine inside the Permit PDP

Next steps