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:
- The consent service authenticates the user and records which trust level the user grants to the agent.
- The gateway calls
permit.check()on eachcall_toolrequest, and Permit.io evaluates the call against policy. - Permit.io records each decision in the audit log.
The design has these properties:
| Property | How it works |
|---|---|
| Transparent proxy | MCP 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 binding | Each 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 call | The 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 sessions | The 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 isolation | Each 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.
| Component | Hosted (SaaS) | Customer-controlled | Fully on-premises |
|---|---|---|---|
| Gateway and consent service | Permit runs them on *.agent.security | You run them in your network | You run them in your Kubernetes cluster, on your own domain |
| Admin dashboard | Permit runs it on app.agent.security | Contact Permit for your deployment | You run it in your Kubernetes cluster, on your own domain |
| PDP | Permit.io Cloud PDP | A PDP in your network | A PDP in your cluster, set per host in the admin dashboard |
| Control plane (policies, audit logs) | Permit.io cloud | Permit.io cloud | Your 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
| Component | Hosted address | What it does |
|---|---|---|
| Admin dashboard (Platform UI) | app.agent.security | Admins manage hosts, MCP servers, humans, and agents. See the admin dashboard reference. |
| Edge router (NGINX) | <host>.agent.security | Routes each request by path to the gateway or the consent service. |
| Gateway | <host>.agent.security/mcp | Proxies MCP traffic and enforces authorization on each tool call. |
| Gateway Admin API | Internal | Stores host configuration, sessions, and OAuth state in Redis for the admin dashboard and the consent service. |
| Consent service | <host>.agent.security | OAuth 2.1 authorization server: user sign-in, consent, and upstream MCP OAuth. See Consent service. |
| Permit PDP | Cloud PDP, or your PDP when self-hosted | Evaluates each permit.check() call. |
| Redis | Internal | Host configuration, MCP sessions, upstream OAuth tokens, and the gateway JWT signing key. Only the gateway reads and writes Redis. |
| PostgreSQL | Internal | User accounts, OAuth clients, and authentication sessions for the consent service. |
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
| From | To | What moves |
|---|---|---|
| MCP client | Edge router | MCP Streamable HTTP requests (/mcp), OAuth discovery and registration (/.well-known/*, /oauth/*), and browser traffic for sign-in and consent. |
| Edge router | Gateway | /mcp, /oauth/*, and /.well-known/* requests. |
| Edge router | Consent service | Sign-in, consent, and upstream OAuth callback requests (/login, /consent, /api/auth/*, /api/consent/*, /api/mcp/*). |
| Consent service | Gateway Admin API | Sessions, OAuth states, and pending upstream tokens, over HTTP with a bearer token. |
| Gateway Admin API | Redis | All Redis reads and writes. |
| Consent service | PostgreSQL | Users, authentication sessions, and OAuth clients. |
| Gateway | Consent service | JSON Web Key Set (JWKS) fetch to verify access tokens. The gateway caches the keys. |
| Gateway | Permit PDP | One permit.check() call for each tool call. |
| Consent service | Permit API | Resource instances, relations, and role assignments written when a user accepts consent. |
| Gateway | Upstream MCP server | MCP 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 level | Suggested for tool names that contain | Typical operations |
|---|---|---|
| High | delete, remove, destroy, drop, purge, erase, truncate, terminate, kill, revoke | Destructive |
| Medium | create, write, update, set, modify, edit, put, post, insert, add, send, execute, run, invoke, submit, push, publish, deploy, apply, patch | Write |
| Low | Any other name | Read |
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_profileresource 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}, wheresubjectis 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}, whereclient_idis the OAuth client ID of the MCP client. The gateway uses this key in thepermit.check()call for each tool call.
The flows below run in this order:
- Admin setup, once per host: the admin creates a host, imports MCP servers, and grants users access.
- 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.
- 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.
Sequence: user consent flow
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.
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.
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 profile | Profile relation to server | Effective server role |
|---|---|---|
{server}-high | high | high |
{server}-high | medium | medium (capped) |
{server}-high | low | low (capped) |
{server}-medium | high | medium |
{server}-medium | medium | medium |
{server}-medium | low | low (capped) |
{server}-low | high | low |
{server}-low | medium | low |
{server}-low | low | low |
The effective role decides which tools the agent can call:
| Tool trust level | Effective roles that can call the tool |
|---|---|
| Low | low, medium, high |
| Medium | medium, high |
| High | high |
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) | User | MCP server | Trust level granted | Example allowed tools | Example denied tools |
|---|---|---|---|---|---|
| Cursor | alice | linear_mcp | Medium | get_issues, create_issue | delete_project |
| Claude Desktop | alice | github_mcp | Low | list_repos, get_file | create_issue, delete_repo |
| VS Code Copilot | bob | github_mcp | High | get_file, create_issue, delete_repo | None: High includes all tools |
| Claude Code | carol | slack_mcp | Medium | search_messages, send_message | remove_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.jsonon the host. The endpoint needs no authentication and sendsCache-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
| Claim | Value | Purpose |
|---|---|---|
iss | agent-security-gateway | Identifies the issuer |
sub | Signed-in user ID | The user the request is made for |
aud | Canonical upstream URL: host lowercased, all trailing slashes (including the root /) removed | Intended recipient |
exp | Issue time plus the token lifetime (5 minutes by default) | Token expiry |
iat | Issue time minus 30 seconds (clock-skew leeway) | When the token was issued |
nbf | Issue time minus 30 seconds (clock-skew leeway) | Not valid before this time |
jti | Unique UUID | Unique token ID |
tenant | Host subdomain | The host the request belongs to |
Two authentication headers on upstream requests
Each upstream request carries two headers with different purposes:
| Header | Issued by | What it proves |
|---|---|---|
Authorization: Bearer <token> | The upstream server's OAuth provider | The user authorized this request with the upstream service |
X-Gateway-Auth: Bearer <jwt> | The gateway | The 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:
| Placeholder | Replace with |
|---|---|
<tenant> | Your host subdomain. In a self-hosted deployment, use your gateway base domain instead of agent.security. |
https://your-mcp-server.example.com | The 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).
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.
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:
- Check the response body. An
errorvalue ofrate_limitedmeans a gateway rate limit was hit, not an application error. - Wait and retry. Back off for a few seconds before retrying, and increase the wait on each retry.
- 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
| Term | Meaning |
|---|---|
| MCP | Model Context Protocol, an open standard that connects AI tools to external services |
| Human | The user who signs in and delegates access to agents |
| Agent | An MCP client that acts for a user |
| Host | A named gateway instance with its own subdomain, policies, users, and sessions |
| Trust level | Risk classification (low, medium, high) that decides which tools an agent can call |
| PDP | Policy decision point, the Permit component that evaluates authorization requests |
| HITL | Human-in-the-loop approval of a tool call |
| ReBAC | Relationship-based access control |
| OPAL | Open Policy Administration Layer, which pushes policy updates to PDPs |
| OPA | Open Policy Agent, the policy engine inside the Permit PDP |
Next steps
- Enterprise deployment: compare the hosted, customer-controlled, and fully on-premises models.
- On-prem installation: install the gateway in your Kubernetes cluster.
- Consent service: the screens users see when they authorize an agent, and session expiry.
- How Permit MCP Gateway works with Permit.io: the policy objects the gateway creates in Permit.
- Audit logs: investigate allowed and denied tool calls.