Model Context Protocol (MCP)
The Model Context Protocol (MCP) middleware enables secure, governed access to MCP servers by acting as an OAuth-compliant gateway. It provides centralized access control, resource metadata discovery, and fine-grained policy enforcement for MCP tools and resources.
Key Features and Benefits
- OAuth 2.1 / 2.0 Compliant Access Control: Implements OAuth 2.0/2.1 Resource Server specification for MCP server protection
- Resource Metadata Discovery: Automatically exposes
/.well-known/oauth-protected-resource/<resource-path>endpoints - Task-Based Access Control (TBAC): Fine-grained authorization across three dimensions—tasks (business objectives), tools (system access), and transactions (parameter-level constraints). See Understanding TBAC for details.
- JWT Integration: Seamless integration with existing JWT authentication middleware
- Centralized Governance: Unified access control across multiple MCP servers and tools
Requirements
-
You must have MCP Gateway enabled:
helm upgrade traefik traefik/traefik -n traefik --wait \--reset-then-reuse-values \--set hub.mcpgateway.enabled=trueOptionally configure the maximum request body size (default: 1MB):
helm upgrade traefik traefik/traefik -n traefik --wait \--reset-then-reuse-values \--set hub.mcpgateway.enabled=true \--set hub.mcpgateway.maxRequestBodySize=2097152 # 2MB in bytes -
JWT authentication middleware must be configured for the route to provide authentication context
-
MCP servers must support the Streamable HTTP transport protocol
-
Session Affinity: For MCP sessions to work correctly, configure load balancing with Highest Random Weight (HRW) algorithm to ensure consistent routing to the same service instance
How It Works
The MCP middleware operates as an OAuth 2.1/2.0 Resource Server, intercepting requests to MCP servers and enforcing access control policies:
-
Resource Metadata Exposure: Automatically creates
/.well-known/oauth-protected-resource/<resource-path>endpoints for each configured MCP server, providing OAuth discovery information. -
Request Interception: Intercepts POST requests with JSON-RPC payloads destined for MCP servers.
-
JWT Claims Extraction: Retrieves JWT claims from the authentication context provided by upstream JWT middleware.
-
Policy Evaluation: Evaluates configured policies against the MCP request content and JWT claims using expression-based matching.
-
Access Decision: Allows or denies requests based on policy matches and default actions.
The middleware automatically allows initialize and notifications/initialized methods which are essential for MCP protocol handshake.
The MCP Gateway (JWT + MCP middleware) is fully compatible with OAuth 2.1 as it implements the Resource Server role. OAuth 2.1's security improvements (removal of implicit/password grant flows, mandatory PKCE) primarily affect authorization servers and clients, not resource servers. The MCP Gateway validates JWT access tokens issued by OAuth 2.1-compliant authorization servers without requiring any additional configuration.
Configuration Example
- Middleware
- JWT Middleware
- IngressRoute
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: mcp-gateway
spec:
plugin:
mcp:
resourceMetadata:
resource: https://api.example.com/mcp-server
authorizationServers:
- https://auth.example.com
scopesSupported:
- mcp:tools
- mcp:resources
resourceDocumentation: https://docs.example.com/mcp-server
policies:
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `get_weather`) && Contains(`jwt.groups`, `weather-users`)
action: allow
- match: Equals(`mcp.method`, `resources/read`) && Prefix(`mcp.params.uri`, `file://safe/`)
action: allow
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `admin_tool`)
action: deny
defaultAction: deny
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: mcp-jwt-auth
spec:
plugin:
jwt:
jwksUrl: https://auth.example.com/.well-known/jwks.json
wwwAuthenticate: https://api.example.com/.well-known/oauth-protected-resource/mcp-server
forwardAuthorization: true
forwardHeaders:
X-User-ID: sub
X-User-Groups: groups
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mcp-server
spec:
routes:
- kind: Rule
match: Host(`api.example.com`)
middlewares:
- name: mcp-jwt-auth # JWT authentication first
- name: mcp-gateway # Then MCP access control
services:
- name: mcp-server-service
port: 80
Configuration Options
| Field | Description | Required | Default |
|---|---|---|---|
resourceMetadata | OAuth resource metadata configuration block | No | |
resourceMetadata.resource | The resource identifier URL for this MCP server | Yes (if resourceMetadata specified) | |
resourceMetadata.authorizationServers | Array of authorization server URLs | Yes (if resourceMetadata specified) | |
resourceMetadata.scopesSupported | Array of OAuth scopes supported by this resource | No | |
resourceMetadata.resourceDocumentation | URL to resource documentation | No | |
policies | Array of policy rules for runtime access control | No | |
policies[].match | Expression that must evaluate to true for the policy to apply | Yes | |
policies[].action | Action to take when the policy matches (allow or deny) | Yes | |
defaultAction | Default action when no runtime policies match (allow or deny) | No | deny |
listPolicies | Array of list filtering policies for controlling capability discovery | No | |
listPolicies[].match | Expression evaluated for each item in list responses (tools/list, prompts/list, resources/list) | Yes | |
listPolicies[].action | Action to take when the list policy matches (show or hide) | Yes | |
listDefaultAction | Default action when no list policies match (show or hide) | No | show |
Policy Expression Language
Policies use an expression language that provides access to MCP request data and JWT claims:
Available Data Context
Policy expressions have access to two main data sources: the MCP JSON-RPC request and JWT claims from the authentication context.
MCP Request Data (mcp.*)
The entire JSON-RPC request body is available under the mcp namespace. You can access any field from the request structure:
| Field | Description | Example Value |
|---|---|---|
mcp.jsonrpc | JSON-RPC version | "2.0" |
mcp.method | MCP method being invoked | "tools/call", "resources/read", "prompts/get" |
mcp.id | Request identifier (string or number) | 1, "req-123" |
mcp.params.* | Method parameters (nested access supported) | See examples below |
Common MCP Method Parameters:
| Method | Parameter Path | Description | Example |
|---|---|---|---|
tools/call | mcp.params.name | Tool name | "get_weather", "search_database" |
tools/call | mcp.params.arguments.* | Tool arguments | mcp.params.arguments.city → "Paris" |
resources/read | mcp.params.uri | Resource URI | "file:///project/data.json" |
resources/subscribe | mcp.params.uri | Resource URI to subscribe to | "file:///logs/app.log" |
prompts/get | mcp.params.name | Prompt name | "code_review", "summarize" |
prompts/get | mcp.params.arguments.* | Prompt arguments | mcp.params.arguments.language → "go" |
Example JSON-RPC Request Structure:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"city": "Paris",
"units": "metric"
}
}
}
Access in policies:
mcp.method→"tools/call"mcp.id→1mcp.params.name→"get_weather"mcp.params.arguments.city→"Paris"mcp.params.arguments.units→"metric"
JWT Claims Data (jwt.*)
All JWT claims from the authenticated user are available under the jwt namespace. You can access standard and custom claims using dot notation for nested fields.
Common JWT Claims:
| Field | Description | Example Value |
|---|---|---|
jwt.sub | Subject (user identifier) | "user-123", "[email protected]" |
jwt.iss | Issuer | "https://auth.example.com" |
jwt.aud | Audience | "api.example.com" |
jwt.exp | Expiration time (Unix timestamp) | 1735689600 |
jwt.iat | Issued at time (Unix timestamp) | 1735686000 |
jwt.scope | OAuth scopes (space-separated string) | "mcp:read mcp:write" |
jwt.groups | User groups (array) | ["developers", "admin"] |
jwt.tenant_id | Tenant identifier (custom claim) | "tenant-abc" |
jwt.permissions | User permissions (custom claim) | ["tool:weather", "resource:read"] |
jwt.* | Any custom claim | Access any field in your JWT |
Example JWT Claims:
{
"sub": "user-123",
"groups": ["developers", "beta-testers"],
"scope": "mcp:read mcp:write",
"tenant_id": "acme-corp",
}
Access in policies:
jwt.sub→"user-123"jwt.groups→["developers", "beta-testers"]jwt.scope→"mcp:read mcp:write"jwt.tenant_id→"acme-corp"jwt.email→"[email protected]"
Variable Substitution
You can use JWT claims as variables in your expressions using the ${jwt.field} syntax:
# Allow users to access their own tenant resources
- match: Contains(`mcp.params.uri`, `tenant-${jwt.tenant_id}`)
action: allow
# Allow users to access their personal directory
- match: Prefix(`mcp.params.uri`, `file:///users/${jwt.sub}/`)
action: allow
Expression Functions
| Function | Description | Example |
|---|---|---|
Equals(field, value) | Exact string match | Equals('mcp.method', 'tools/call') |
Contains(field, substring) | Check if field contains substring (for strings) or contains value (for arrays) | Contains('jwt.groups', 'admin') |
Prefix(field, prefix) | Check if field starts with prefix | Prefix('mcp.params.uri', 'file://safe/') |
Exists(field) | Check if field exists in the data context | Exists('jwt.tenant_id') |
SplitContains(field, separator, value) | Split field by separator and check if any part matches value | SplitContains('jwt.scope', ' ', 'mcp:read') |
OneOf(field, value1, value2, ...) | Check if field matches any of the provided values | OneOf('mcp.method', 'tools/call', 'tools/list') |
Lt(field, value) | Numeric less than comparison (supports variable substitution) | Lt('mcp.params.arguments.amount', '1000') |
Lte(field, value) | Numeric less than or equal comparison (supports variable substitution) | Lte('mcp.params.arguments.amount', '${jwt.approval_limit}') |
Gt(field, value) | Numeric greater than comparison (supports variable substitution) | Gt('jwt.rate_limit', '100') |
Gte(field, value) | Numeric greater than or equal comparison (supports variable substitution) | Gte('mcp.params.arguments.priority', '${jwt.min_priority}') |
Policy Examples
policies:
# Allow weather tool for weather-users group
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `get_weather`) && Contains(`jwt.groups`, `weather-users`)
action: allow
# Allow reading safe file resources
- match: Equals(`mcp.method`, `resources/read`) && Prefix(`mcp.params.uri`, `file://safe/`)
action: allow
# Deny admin tools for non-admin users
- match: Equals(`mcp.method`, `tools/call`) && Prefix(`mcp.params.name`, `admin_`) && !Contains(`jwt.groups`, `admin`)
action: deny
# Allow specific user to access personal resources
- match: Equals(`mcp.method`, `resources/read`) && Contains(`mcp.params.uri`, `user-${jwt.sub}`)
action: allow
# Numeric comparison: Allow expense approvals within user's limit
- match: Equals(`mcp.params.name`, `approve_expense`) && Lte(`mcp.params.arguments.amount`, `${jwt.approval_limit}`)
action: allow
# Numeric comparison: Allow high-priority tasks for users with sufficient clearance
- match: Equals(`mcp.method`, `tools/call`) && Gte(`jwt.clearance_level`, `${mcp.params.arguments.required_level}`)
action: allow
Client Behavior When Requests Are Blocked
When the MCP middleware denies a request, it returns an HTTP 403 Forbidden response. The client experience depends on the MCP client implementation:
What happens:
- The middleware returns
403 Forbiddenwith the plain text body"Forbidden" - The MCP client receives this as a broken session error
- Most MCP clients (including Claude Desktop) report:
calling "tools/call": broken session: 403 Forbidden
Client-side impact:
- Claude Desktop: The tool call fails with an error message visible in the conversation. Claude will typically inform the user that the tool is unavailable or access was denied. The MCP session remain broken until Claude Desktop is restarted, as there is no automatic session recovery.
- VSCode: Logs the error but recovers automatically for subsequent requests. The MCP context is preserved, allowing the agent to continue working.
- Other MCP clients: Behavior varies by implementation. Some may attempt reconnection, while others will report the error and require manual intervention.
Best practices for policy design:
- Use
allowpolicies withdefaultAction: denyto create explicit allow-lists rather than deny-lists - Test policies to avoid unintentionally blocking legitimate tool calls
- Monitor access logs to identify and debug policy-related denials
- Consider providing users with clear documentation about which tools/resources require specific permissions
Resource and Tool Filtering
The MCP Gateway provides list filtering capabilities that control which tools, prompts, and resources appear in list responses (tools/list, prompts/list, resources/list).
This enables clients to discover only the capabilities they're authorized to use, improving capability discovery and user experience.
How List Filtering Works
When an MCP client requests a list of available capabilities, the middleware:
- Intercepts list responses from the MCP server (
tools/list,prompts/list,resources/list) - Evaluates list policies for each item in the response
- Filters items based on policy matches and default action
- Returns filtered list containing only authorized items
The filtering uses dedicated listPolicies that are evaluated at list time using available context (JWT claims, item name/URI).
Runtime parameters (like tool arguments) are not available during list filtering because the client has not yet sent tool invocation requests.
At this stage of the MCP protocol flow, only authentication and list discovery requests have occurred.
Configuration
List filtering is configured separately from runtime authorization policies:
| Field | Description | Required | Default |
|---|---|---|---|
listPolicies | Array of list filtering policies | No | [] |
listPolicies[].match | Expression evaluated for each list item | Yes | |
listPolicies[].action | Action to take when policy matches (show or hide) | Yes | |
listDefaultAction | Default action when no list policies match (show or hide) | No | show |