User-Based Tool Filtering¶
This guide shows how to filter tools/list responses based on the authenticated user's allowed tools. The MCP Gateway broker verifies a signed x-mcp-authorized header and only returns the tools listed in that header.
Prerequisites¶
- MCP Gateway is installed and working
- Authentication is configured
- At least one MCP server is registered with the gateway
- Kuadrant
AuthPolicyis available in your cluster - You know the name and namespace of your
MCPGatewayExtension - Your identity provider includes per-server tool permissions in the authenticated user's token claims
In the examples below:
- The gateway namespace is
gateway-system - The broker namespace is
mcp-system - The Authorino namespace is
kuadrant-system - The MCPGatewayExtension name is
mcp-gateway-extension - The AuthPolicy name is
mcp-auth-policy
Replace these values if your installation uses different names.
How It Works¶
- An upstream authorization system validates the user's identity
- It creates a signed JWT containing the user's allowed capabilities in an
allowed-capabilitiesclaim - This JWT is passed to the broker via the
x-mcp-authorizedheader - The broker validates the JWT signature and filters
tools/listresponses accordingly
The allowed-capabilities claim is a JSON-encoded string containing a capabilities map. The top-level key is the capability type, and the tools entry maps server names to allowed tool names:
{
"allowed-capabilities": "{\"tools\":{\"mcp-test/server1-route\":[\"greet\",\"time\"],\"mcp-test/server2-route\":[\"hello_world\"]}}",
"exp": 1760004918,
"iat": 1760004618
}
The value of allowed-capabilities is a string, not a nested JSON object. The broker deserializes this string to extract the tools map for filtering.
Step 1: Generate a signing key pair¶
Generate an ECDSA P-256 key pair. Authorino uses the private key to sign the x-mcp-authorized wristband, and the broker uses the public key to verify it.
openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
Verify that both files were created:
Step 2: Create the Kubernetes secrets¶
Create one secret for the broker's public key and one for Authorino's private key.
The public-key secret must be created in the same namespace as the MCPGatewayExtension. In this example, that namespace is mcp-system.
kubectl create secret generic trusted-headers-public-key \
--from-file=key=public-key.pem \
-n mcp-system \
--dry-run=client -o yaml | kubectl apply -f -
kubectl create secret generic trusted-headers-private-key \
--from-file=key.pem=private-key.pem \
-n kuadrant-system \
--dry-run=client -o yaml | kubectl apply -f -
Verify that both secrets exist:
kubectl get secret trusted-headers-public-key -n mcp-system
kubectl get secret trusted-headers-private-key -n kuadrant-system
Step 3: Configure the MCPGatewayExtension¶
Configure the MCPGatewayExtension to inject the public key into the broker by referencing the secret from spec.trustedHeadersKey.secretName:
kubectl patch mcpgatewayextension mcp-gateway-extension -n mcp-system --type='merge' \
-p='{"spec":{"trustedHeadersKey":{"secretName":"trusted-headers-public-key"}}}'
Wait for the broker deployment to roll out:
Verify the extension now references the trusted header key:
kubectl get mcpgatewayextension mcp-gateway-extension -n mcp-system \
-o jsonpath='{.spec.trustedHeadersKey.secretName}{"\n"}'
Expected output:
Step 4: Apply an AuthPolicy that generates x-mcp-authorized¶
Apply an AuthPolicy that:
- authenticates the user with your identity provider
- allows
tools/list,initialize, andnotifications/initialized - extracts the user's allowed tools from the identity claims
- returns the
x-mcp-authorizedwristband header signed with the private key
If you already created an authentication-only mcp-auth-policy, delete it first. This guide uses spec.rules, while the authentication guide uses spec.defaults.rules. Replacing the object avoids ending up with both shapes merged together.
Update the issuerUrl and resource_metadata values to match your environment before applying:
kubectl delete authpolicy mcp-auth-policy -n gateway-system --ignore-not-found
kubectl apply -f - <<EOF
apiVersion: kuadrant.io/v1
kind: AuthPolicy
metadata:
name: mcp-auth-policy
namespace: gateway-system
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: mcp-gateway
sectionName: mcp
when:
- predicate: "!request.path.contains('/.well-known')"
rules:
authentication:
keycloak:
jwt:
issuerUrl: https://keycloak.example.com/realms/mcp
authorization:
allow-mcp-method:
patternMatching:
patterns:
- predicate: |
!request.headers.exists(h, h == 'x-mcp-method') || (request.headers['x-mcp-method'] in ["tools/list","initialize","notifications/initialized"])
authorized-capabilities:
opa:
rego: |
allow = true
capabilities = {
"tools": { server: tools |
server := object.keys(input.auth.identity.resource_access)[_]
tools := [substring(r, count("tool:"), -1) |
r := input.auth.identity.resource_access[server].roles[_]
startswith(r, "tool:")
]
}
}
allValues: true
response:
success:
headers:
x-mcp-authorized:
wristband:
issuer: authorino
customClaims:
allowed-capabilities:
selector: auth.authorization.authorized-capabilities.capabilities.@tostr
tokenDuration: 300
signingKeyRefs:
- name: trusted-headers-private-key
algorithm: ES256
unauthenticated:
code: 401
headers:
WWW-Authenticate:
value: Bearer resource_metadata=https://mcp.example.com/.well-known/oauth-protected-resource/mcp
body:
value: |
{
"error": "Unauthorized",
"message": "Access denied: Authentication required."
}
unauthorized:
code: 403
body:
value: |
{
"error": "Forbidden",
"message": "Access denied."
}
EOF
Verify that the policy is enforced:
kubectl get authpolicy mcp-auth-policy -n gateway-system \
-o jsonpath='{.status.conditions[?(@.type=="Enforced")].status}{"\n"}'
Expected output:
Note: The
authorized-capabilitiesRego expects the authenticated user's tool permissions to be present inresource_access, keyed by MCP server name such asmcp-test/server1-route. Tool roles must be prefixed withtool:, such astool:greet.
Step 5: Verify that tools/list is filtered¶
Open MCP Inspector and sign in as a user who should only see a subset of tools:
Connect the inspector to your gateway's MCP endpoint using the authenticated flow from the authentication guide.
After login:
- Open Tools
- Run List Tools
- Confirm that only the tools allowed for that user are shown
For example, if the signed header only allows:
{
"tools": {
"mcp-test/server1-route": ["greet", "time"],
"mcp-test/server2-route": ["hello_world"]
}
}
Then the broker should only return the prefixed tools for those entries, such as test1_greet, test1_time, and test2_hello_world.
To verify that filtering is user-specific, sign out and authenticate as a different user with a different set of tool roles. The tools/list response should change to match that user's permissions.
Cleanup¶
If you only created the local key files for testing, remove them from your workstation: