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 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 policies match (allow or deny ) | No | deny |
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
→1
mcp.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 Forbidden
with 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
allow
policies withdefaultAction: deny
to 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
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.duration
histogram 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 logs
to inspect JWT content) - Policy expressions: Review policy expressions for syntax errors (field names are case-sensitive)
- Default action: Ensure
defaultAction
is set appropriately for your use case (deny
by 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
resourceMetadata
is 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.method
notmcp.Method
- JWT structure: Verify JWT claims structure matches policy expectations using
jwt.io
or 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
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