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",
"email": "[email protected]"
}
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 |
List Policy Data Context
List policies have access to:
- JWT claims (
jwt.*) - All claims from the authenticated user - MCP method (
mcp.method) - The method being checked (tools/call,resources/read, orprompts/get) - Item identifier:
- For tools and prompts:
mcp.params.name(tool or prompt name) - For resources:
mcp.params.uri(resource URI)
- For tools and prompts:
List policies do NOT have access to runtime parameters (for example, mcp.params.arguments.*) because these are not known at list time.
Understanding the Methods
The middleware automatically maps list methods to their corresponding call methods for filtering:
| List Method | Maps To Call Method | Item Identifier Field |
|---|---|---|
tools/list | tools/call | mcp.params.name (tool name) |
prompts/list | prompts/get | mcp.params.name (prompt name) |
resources/list | resources/read | mcp.params.uri (resource URI) |
The middleware automatically sets mcp.method to the appropriate call method when evaluating list policies. You typically don't need to check the method in your list policy expressions—only focus on JWT claims and item identifiers.
# Good: Simple list policy focusing on item name and JWT claims
listPolicies:
- match: Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`)
action: show
# Also valid but redundant: Including method check
listPolicies:
- match: Equals(`mcp.method`, `tools/call`) && Prefix(`mcp.params.name`, `dev_`)
action: show
# Wrong: Only checking the method without filtering specific tools
listPolicies:
- match: Equals(`mcp.method`, `tools/call`) # Matches ALL tools - no filtering!
action: show
List Filtering Examples
Filter Tools by User Group
Show only development tools to users in the "developers" group:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: filtered-mcp
spec:
plugin:
mcp:
resourceMetadata:
resource: https://api.example.com/mcp
authorizationServers:
- https://auth.example.com
# Runtime authorization policies
policies:
- match: Equals(`mcp.method`, `tools/call`) && Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`)
action: allow
defaultAction: deny
# List filtering policies
listPolicies:
- match: Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`)
action: show
- match: Prefix(`mcp.params.name`, `admin_`) && Contains(`jwt.groups`, `admin`)
action: show
listDefaultAction: hide
Result:
- Developers see
dev_*tools in the list - Admins see
admin_*tools in the list - All other tools are hidden from the list
Filter Resources by Tenant
Show only resources belonging to the user's tenant:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: multitenant-mcp
spec:
plugin:
mcp:
# Runtime authorization policies
policies:
- match: Equals(`mcp.method`, `resources/read`) && Contains(`mcp.params.uri`, `tenant-${jwt.tenant_id}`)
action: allow
defaultAction: deny
# List filtering policies
listPolicies:
- match: Contains(`mcp.params.uri`, `tenant-${jwt.tenant_id}`)
action: show
listDefaultAction: hide
Result: Users only see resources with URIs containing their tenant ID in resources/list responses.
Allow-All Listing with Runtime Restrictions
Show all tools in the list, but enforce runtime restrictions:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: runtime-restricted-mcp
spec:
plugin:
mcp:
# Runtime authorization policies with parameter checks
policies:
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `transfer_funds`) && Lte(`mcp.params.arguments.amount`, `${jwt.daily_limit}`)
action: allow
- match: Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `approve_expense`) && Lte(`mcp.params.arguments.amount`, `${jwt.approval_limit}`)
action: allow
defaultAction: deny
# List filtering - show all tools (no listPolicies defined)
listDefaultAction: show
Result:
- All tools appear in
tools/list(no list filtering) - Runtime authorization enforces amount limits based on JWT claims
- Users discover all available tools, but authorization is enforced when they try to use them
Best Practices for List Filtering
1. Decouple List Filtering from Runtime Authorization
List policies and runtime policies serve different purposes:
- List policies: Control capability discovery (what users can see)
- Runtime policies: Control runtime authorization (what users can do)
# Good: Separate concerns
listPolicies:
- match: Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`)
action: show
listDefaultAction: hide
policies:
- match: Equals(`mcp.method`, `tools/call`) && Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`) && Lte(`mcp.params.arguments.complexity`, `${jwt.max_complexity}`)
action: allow
defaultAction: deny
2. Use Simpler Rules for List Filtering
List policies should avoid runtime-dependent conditions since parameters are not available:
# Good: Simple, list-time conditions
listPolicies:
- match: Equals(`mcp.params.name`, `get_weather`) && Contains(`jwt.permissions`, `tool:weather`)
action: show
# Problematic: Uses runtime parameter (not available at list time)
listPolicies:
- match: Equals(`mcp.params.name`, `get_weather`) && Equals(`mcp.params.arguments.city`, `Paris`)
action: show # Arguments not available - this will never match!
3. When in Doubt, Show More Rather Than Less
If you're unsure whether to show a tool/resource in the list:
- Prefer showing it and enforcing restrictions at runtime
- This improves capability discovery
- Runtime policies will still prevent unauthorized access
# Conservative approach: Show all tools to developers
listPolicies:
- match: Contains(`jwt.groups`, `developers`)
action: show
listDefaultAction: hide
# Enforce strict limits at runtime based on user permissions
policies:
- match: Equals(`mcp.method`, `tools/call`) && Contains(`jwt.allowed_tools`, `${mcp.params.name}`)
action: allow
defaultAction: deny
4. Align List Policies with Runtime Policies
Ensure list policies don't show items that will always be denied at runtime:
# Aligned: Show tools that users can actually use
listPolicies:
- match: Prefix(`mcp.params.name`, `admin_`) && Contains(`jwt.groups`, `admin`)
action: show
policies:
- match: Equals(`mcp.method`, `tools/call`) && Prefix(`mcp.params.name`, `admin_`) && Contains(`jwt.groups`, `admin`)
action: allow
List Filtering vs Runtime Authorization
| Aspect | List Filtering (listPolicies) | Runtime Authorization (policies) |
|---|---|---|
| Purpose | Control what appears in list responses | Control runtime authorization of tools/resources |
| When Evaluated | When MCP server responds to tools/list, prompts/list, resources/list | When client calls tools/call, resources/read, prompts/get |
| Available Context | JWT claims, item name/URI | JWT claims, all request parameters including arguments |
| Actions | show or hide | allow or deny |
| Default Action | listDefaultAction (default: show) | defaultAction (default: deny) |
| Impact on Client | Items don't appear in discovery | Calls return 403 Forbidden |
Integration with JWT Middleware
The MCP middleware requires the JWT middleware to be configured upstream to provide authentication context. The JWT middleware should be configured with:
| Configuration | Description |
|---|---|
wwwAuthenticate | URL to the auto-generated well-known OAuth resource endpoint (RFC 6750). This should match the endpoint automatically created by the MCP Gateway based on your resourceMetadata.resource configuration. Format: https://<host>/.well-known/oauth-protected-resource/<resource-path> (for example, https://api.example.com/.well-known/oauth-protected-resource/mcp-server) |
forwardAuthorization | Set to true to forward the Authorization header to the MCP server, enabling On-Behalf-Of (OBO) authentication where the MCP server can act with the client's identity and permissions |
forwardHeaders | Configure headers to pass user information if needed |
Example JWT configuration for MCP integration:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: mcp-jwt
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-Tenant-ID: tenant_id
X-User-Groups: groups
OAuth Resource Discovery
When resourceMetadata is configured, the middleware automatically creates a well-known endpoint that returns OAuth resource metadata according to RFC 8414:
Endpoint Pattern: /.well-known/oauth-protected-resource/<resource-path>
For example, if resourceMetadata.resource is https://api.example.com/mcp-server, the auto-generated endpoint will be:
- URL:
https://api.example.com/.well-known/oauth-protected-resource/mcp-server
Response Example:
{
"resource": "https://api.example.com/mcp-server",
"authorization_servers": ["https://auth.example.com"],
"bearer_methods_supported": ["header"],
"scopes_supported": ["mcp:tools", "mcp:resources"],
"resource_documentation": "https://docs.example.com/mcp-server"
}
This enables MCP clients to discover authorization requirements automatically.
Session Handling and Load Balancing
MCP (Model Context Protocol) requires session affinity to maintain proper communication between clients and servers. MCP sessions are stateful, meaning that once a client establishes a connection with a specific MCP server instance, subsequent requests from that client should be routed to the same server instance.
Configuring Consistent Server-Side Session Affinity
To ensure session affinity for MCP traffic, configure your services to use the Highest Random Weight (HRW) load balancing algorithm. HRW provides consistent routing based on the client's IP address without requiring sticky cookies.
- TraefikService with HRW
- IngressRoute with HRW Service
apiVersion: traefik.io/v1alpha1
kind: TraefikService
metadata:
name: mcp-server-lb
spec:
highestRandomWeight:
services:
- name: mcp-server-1
port: 80
weight: 10
- name: mcp-server-2
port: 80
weight: 10
- name: mcp-server-3
port: 80
weight: 10
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: mcp-server
spec:
routes:
- kind: Rule
match: Host(`api.example.com`)
middlewares:
- name: mcp-jwt-auth
- name: mcp-gateway
services:
- name: mcp-server-lb
kind: TraefikService # Use the HRW TraefikService
Benefits of HRW for MCP
- Session Affinity: Clients consistently reach the same MCP server instance
- No Client State: Unlike sticky cookies, no client-side configuration is required
- Automatic Failover: If a server becomes unavailable, requests are redistributed consistently among remaining servers
- Weighted Distribution: Different server instances can handle different loads based on their capacity
Common Deployment Patterns
Basic MCP Server Protection
Protect an MCP server with JWT authentication and allow all operations for authenticated users:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: basic-mcp
spec:
plugin:
mcp:
resourceMetadata:
resource: https://api.example.com/mcp
authorizationServers:
- https://auth.example.com
defaultAction: allow
Task-Based Access Control
Implement Task-Based Access Control (TBAC) to authorize AI agents based on the tasks they perform, the tools they access, and transaction-level constraints. This approach is more appropriate for AI agents than traditional role-based access control because agents complete tasks across multiple domains. See Understanding TBAC for the complete conceptual framework.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: tbac-mcp
spec:
plugin:
mcp:
resourceMetadata:
resource: https://api.example.com/mcp
authorizationServers:
- https://auth.example.com
scopesSupported:
- mcp:read
- mcp:write
- mcp:admin
policies:
# Tool authorization: Developers can access development tools
- match: Contains(`jwt.groups`, `developers`) && Prefix(`mcp.params.name`, `dev_`)
action: allow
# Task authorization: Admins can perform all tasks
- match: Contains(`jwt.groups`, `admin`)
action: allow
# Transaction authorization: Regular users can only read resources
- match: Equals(`mcp.method`, `resources/read`) && Contains(`jwt.scope`, `mcp:read`)
action: allow
# Deny everything else
defaultAction: deny
Multi-Tenant Resource Access
Control access to resources based on tenant information in JWT claims:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: multitenant-mcp
spec:
plugin:
mcp:
policies:
# Users can only access resources in their tenant
- match: Equals(`mcp.method`, `resources/read`) && Contains(`mcp.params.uri`, `tenant-${jwt.tenant_id}`)
action: allow
# Tools are restricted by tenant permissions
- match: Equals(`mcp.method`, `tools/call`) && Contains(`jwt.permissions`, `tool:${mcp.params.name}`)
action: allow
defaultAction: deny
Transaction-Level Authorization with Numeric Limits
Implement fine-grained transaction controls using numeric comparison functions to enforce per-user limits on operations:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: transaction-control-mcp
spec:
plugin:
mcp:
resourceMetadata:
resource: https://api.example.com/financial-mcp
authorizationServers:
- https://auth.example.com
scopesSupported:
- financial:approve
- financial:transfer
policies:
# Allow expense approvals within user's approval limit
- match: Equals(`mcp.params.name`, `approve_expense`) && Lte(`mcp.params.arguments.amount`, `${jwt.approval_limit}`)
action: allow
# Allow transfers below daily limit
- match: Equals(`mcp.params.name`, `transfer_funds`) && Lte(`mcp.params.arguments.amount`, `${jwt.daily_transfer_limit}`)
action: allow
# Allow budget adjustments only for amounts within authorized range
- match: Equals(`mcp.params.name`, `adjust_budget`) && Lte(`mcp.params.arguments.amount`, `${jwt.max_budget_change}`)
action: allow
# Deny any transaction exceeding user limits
defaultAction: deny
This pattern uses JWT claims like approval_limit, daily_transfer_limit, and max_budget_change to dynamically enforce transaction limits.
The IdP issues these limits based on the user's role, department, or other attributes, enabling centralized policy management.
Observability
The MCP middleware provides OpenTelemetry-compatible metrics and traces for monitoring AI agent tool invocations.
Metrics
The MCP Gateway records detailed metrics including:
- Operation duration:
mcp.client.operation.durationhistogram tracking MCP operation latency - Method-level tracking: Metrics tagged with MCP method names (
tools/call,resources/read, etc.) - Tool-specific metrics: Automatic tagging by tool name, prompt name, and resource URI
- Argument tracking: Request arguments captured as metric attributes for detailed analysis
Metric Attributes
The following attributes are automatically attached to metrics when available:
| Attribute | Description | Example |
|---|---|---|
mcp.session.id | MCP session identifier from the Mcp-Session-Id header | session-abc123 |
mcp.method.name | The MCP method being invoked | tools/call, resources/read |
mcp.request.id | JSON-RPC request ID (string or number) | 1, "req-123" |
mcp.tool.name | Tool name (for tools/call methods) | get_weather, search_database |
mcp.prompt.name | Prompt name (for prompts/get methods) | code_review, summarize |
mcp.resource.uri | Resource URI (for resource methods) | file://safe/data.json |
mcp.request.argument.* | Tool arguments (for tools/call with arguments) | mcp.request.argument.city=Paris |
Example Metrics Query
Using Prometheus, you can query MCP operation metrics:
# Average MCP operation duration by method
rate(mcp_client_operation_duration_sum[5m]) / rate(mcp_client_operation_duration_count[5m])
# MCP operations per second by tool name
sum(rate(mcp_client_operation_duration_count{mcp_tool_name!=""}[5m])) by (mcp_tool_name)
# 95th percentile MCP operation latency
histogram_quantile(0.95, sum(rate(mcp_client_operation_duration_bucket[5m])) by (le))
Tracing Integration
MCP operations are automatically included in OpenTelemetry traces when tracing is enabled in Traefik Hub. The middleware creates spans with the same attributes as metrics, allowing you to correlate performance issues with specific MCP methods and tools.
Troubleshooting
403 Forbidden Responses
When requests are denied with HTTP 403, check the following:
- JWT middleware configuration: Verify JWT middleware is configured and placed before MCP middleware in the middleware chain
- JWT claims: Check that JWT contains required claims referenced in policies (use
kubectl logsto inspect JWT content) - Policy expressions: Review policy expressions for syntax errors (field names are case-sensitive)
- Default action: Ensure
defaultActionis set appropriately for your use case (denyby default)
Testing tip: Start with defaultAction: allow to verify JWT authentication works, then add policies incrementally.
Well-Known Endpoint Not Found
If the OAuth resource discovery endpoint returns 404:
- Resource metadata: Confirm
resourceMetadatais configured in the MCP middleware - URL format: Verify the resource URL format matches
https://<host>/<path>pattern - Route application: Check that the middleware is applied to the correct IngressRoute
- Path verification: The well-known endpoint is automatically generated at
/.well-known/oauth-protected-resource/<resource-path>
Example: If resource: https://api.example.com/mcp-server, the endpoint will be at https://api.example.com/.well-known/oauth-protected-resource/mcp-server
Policy Not Matching
When policies don't match expected requests:
- Field names: Use exact field names in expressions (case-sensitive). Use
mcp.methodnotmcp.Method - JWT structure: Verify JWT claims structure matches policy expectations using
jwt.ioor similar tools - Nested fields: Access nested fields with dot notation:
mcp.params.arguments.city - Variable substitution: Only use
${...}syntax in the second parameter (the value), not the first parameter (the field name)- ✓ Correct:
Equals('jwt.department', '${mcp.params.arguments.department}') - ✗ Wrong:
Equals('${jwt.department}', 'sales')
- ✓ Correct:
- Testing: Start with simple conditions and add complexity gradually
Debug approach: Enable Traefik logs and check for policy evaluation messages to see which policies are being evaluated.
MCP Session Issues
MCP sessions require consistent routing to the same server instance:
Symptoms:
- Connection errors after initial handshake
- Unexpected session termination
- State loss between requests
Solutions:
- Load balancing: Configure HRW (Highest Random Weight) algorithm for session affinity (see Session Handling)
- Server verification: Verify that MCP clients are connecting to the same server instance
- Stateful connections: Check that MCP server instances can handle stateful connections properly
- Alternative: Consider using sticky sessions if HRW is not available in your setup
Testing: Use kubectl logs on MCP server pods to verify the same pod handles requests from a given client.
JWT Claims Not Available in Policies
If JWT claims aren't accessible in policy expressions:
Common causes:
- JWT middleware not configured with
forwardAuthorization: true - JWT middleware placed after MCP middleware in the chain (wrong order)
- JWT validation failing silently
- Claims not present in the JWT token
Solutions:
-
Verify middleware order in IngressRoute:
middlewares:
- name: jwt-auth # Must be first
- name: mcp-gateway # Must be second -
Check JWT middleware configuration:
forwardAuthorization: true # Required for OBO -
Inspect JWT token claims using a tool like https://jwt.io
Expression Syntax Errors
Common expression syntax errors and fixes:
Backticks required: Field names and string values must use backticks:
- ✓ Correct:
Equals(`mcp.method`, `tools/call`) - ✗ Wrong:
Equals("mcp.method", "tools/call")
Logical operators: Use &&, ||, ! for boolean logic:
- ✓ Correct:
Equals(`mcp.method`, `tools/call`) && Contains(`jwt.groups`, `admin`) - ✗ Wrong:
Equals(`mcp.method`, `tools/call`) AND Contains(`jwt.groups`, `admin`)
Array checking: Use Contains() for arrays, not Equals():
- ✓ Correct:
Contains(`jwt.groups`, `admin`)(checks if "admin" is in the groups array) - ✗ Wrong:
Equals(`jwt.groups`, `admin`)(tries exact match of entire array)
Variable substitution placement: Only in second parameter:
- ✓ Correct:
Equals(`jwt.tenant_id`, `${mcp.params.arguments.tenant}`) - ✗ Wrong:
Equals(`${jwt.tenant_id}`, `tenant-123`)
Debugging Tips
1. Check JWT Claims
Ensure the JWT middleware is forwarding claims correctly:
# Check JWT middleware logs
kubectl logs -n traefik deployment/traefik | grep jwt
# Test JWT decoding manually
echo "eyJhbGc..." | base64 -d | jq .
2. Simplify Policies
Start with basic policies and add complexity gradually:
# Start simple
policies:
- match: "Equals(`mcp.method`, `tools/call`)"
action: allow
# Add complexity incrementally
policies:
- match: "Equals(`mcp.method`, `tools/call`) && Equals(`mcp.params.name`, `get_weather`)"
action: allow
3. Review Logs
Check Traefik Hub logs for policy evaluation messages:
kubectl logs -n traefik deployment/traefik --tail=100 -f
4. Test Expressions
Verify expression syntax matches the available data context by testing with curl:
# Send test request
curl -X POST https://api.example.com/mcp \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"test"}}'
# Check response and logs
5. Validate Policy Syntax
Test policies with minimal configuration first, then expand:
# Minimal test - should allow all
defaultAction: allow
# Then add deny-by-default
defaultAction: deny
policies:
- match: "Exists(`jwt.sub`)" # Allow any authenticated user
action: allow
List Filtering Not Working
If tools, prompts, or resources aren't being filtered as expected in list responses:
Common causes:
- List policies using runtime parameters that aren't available at list time
- Incorrect method in list policy match expression
- List policy actions conflicting with expectations
Solutions:
-
Verify list policy syntax - Focus on item identifiers and JWT claims:
# Good: Simple, focused list policy
listPolicies:
- match: Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`)
action: show
# Also valid but redundant: Including method check
listPolicies:
- match: Equals(`mcp.method`, `tools/call`) && Prefix(`mcp.params.name`, `dev_`)
action: show
# Wrong: Using list method instead of call method
listPolicies:
- match: Equals(`mcp.method`, `tools/list`) # This won't work!
action: show -
Remove runtime parameters from list policies:
# Good: Only uses list-time data
listPolicies:
- match: Equals(`mcp.params.name`, `get_weather`) && Contains(`jwt.groups`, `weather-users`)
action: show
# Bad: Uses runtime parameter (not available)
listPolicies:
- match: Equals(`mcp.params.name`, `get_weather`) && Equals(`mcp.params.arguments.city`, `Paris`)
action: show # Won't work - arguments not available at list time -
Check default action:
# If you want to show items by default
listDefaultAction: show # Items shown unless explicitly hidden
# If you want to hide items by default
listDefaultAction: hide # Items hidden unless explicitly shown -
Test with logs:
# Enable debug logging to see filtering decisions
kubectl logs -n traefik deployment/traefik-hub --tail=100 | grep "item was"
Debugging approach:
Start with listDefaultAction: show and no list policies to see all items, then add policies incrementally:
# Step 1: Show everything
listDefaultAction: show
# Step 2: Add simple policy
listPolicies:
- match: Contains(`jwt.groups`, `developers`)
action: show
listDefaultAction: hide
# Step 3: Add more specific policies
listPolicies:
- match: Prefix(`mcp.params.name`, `dev_`) && Contains(`jwt.groups`, `developers`)
action: show
listDefaultAction: hide
Security Considerations
- JWT Validation: Ensure JWT middleware validates tokens before MCP middleware
- Scope Verification: Use OAuth scopes to limit access to specific MCP capabilities
- Resource Isolation: Design policies to prevent cross-tenant data access
- Audit Logging: Enable access logs to track MCP server interactions
- Token Expiry: Configure appropriate JWT expiration times for security
Related Content
- Read the Chat Completion documentation
- Read the JWT Middleware documentation
- Learn about the AI Gateway