mcp-tools-acl
The mcp-tools-acl plugin controls which MCP tools each consumer can call or discover on a route powered by openapi-to-mcp. It enforces two kinds of restrictions:
tools/callblocking rejects calls to denied tools with an HTTP error response.tools/listfiltering removes denied tools from the list returned to the client, so disallowed tools are invisible to MCP clients. This applies to both JSON responses and Server-Sent Events (SSE) responses.
The plugin uses a rules-based configuration where each rule specifies either an allowlist or a denylist and, optionally, an expression condition. Rules are evaluated in order. The first matching rule is applied, and the remaining rules are skipped.
The plugin was introduced in API7 Enterprise 3.9.8 and 3.10.0.
Examples
Each example enables openapi-to-mcp and key-auth on the same route. Without an authenticated consumer, mcp-tools-acl passes all traffic unchanged.
Complete Prepare API7 Gateway before continuing so the required OpenAPI-to-MCP service is available.
Configure mcp-tools-acl on the consumer for per-consumer access control. If the plugin is configured on both the consumer and route, the consumer configuration takes precedence. The route configuration applies only when the authenticated consumer has no mcp-tools-acl configuration.
For the Admin API tabs, set the Dashboard address and gateway group, then enter a Dashboard token:
export API7_ADMIN_URL="https://localhost:7443"
export API7_GATEWAY_GROUP_ID="default"
printf "Dashboard token: "
read -rs API7_ADMIN_KEY && export API7_ADMIN_KEY && echo
The -k flag in these examples accepts the local Dashboard's self-signed certificate. Remove it when using a certificate trusted by the client.
For the ADC tabs, select the api7ee backend and configure the corresponding Dashboard address, token, and gateway group.
The examples use version 1.0.27 of the official Swagger Petstore image. The fixture serves both the OpenAPI document and the API. Its manifest digest is pinned so the access-control results remain reproducible.
Start Petstore
Start the fixture for the deployment type used by the gateway:
- Docker
- Kubernetes
Set GATEWAY_CONTAINER to the running API7 Gateway container, then place the gateway and Petstore on a dedicated network. The OpenAPI-to-MCP service must share the gateway's network namespace as described in Prepare API7 Gateway.
export GATEWAY_CONTAINER=replace-with-gateway-container-name
docker network inspect gateway-petstore-net > /dev/null 2>&1 || \
docker network create gateway-petstore-net
if ! docker inspect "$GATEWAY_CONTAINER" \
--format '{{json .NetworkSettings.Networks}}' \
| grep -q '"gateway-petstore-net"'; then
docker network connect gateway-petstore-net "$GATEWAY_CONTAINER"
fi
Create a Compose file for the pinned Petstore fixture on the shared network:
services:
petstore:
image: swaggerapi/petstore3:1.0.27@sha256:749432676c31cd05256f0d9e878302164debb94daa1c1223e193747e1b34e7d1
platform: linux/amd64
networks:
- gateway-petstore-net
ports:
- "18080:8080"
networks:
gateway-petstore-net:
external: true
Start or reconcile the fixture with a fixed Compose project name so the command can be rerun across these guides:
docker compose -p api7-docs-petstore \
-f docker-compose.petstore.yaml up -d
Once the container has started, confirm that the OpenAPI document is available:
curl -fsS "http://127.0.0.1:18080/api/v3/openapi.json" > /dev/null
The fixture image is published for linux/amd64. Create it in the same namespace as the gateway. The manifest and commands use aic, the Ingress Controller example namespace; replace every occurrence with the gateway namespace for another deployment, typically api7 for API7 Gateway.
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: petstore
spec:
replicas: 1
selector:
matchLabels:
app: petstore
template:
metadata:
labels:
app: petstore
spec:
nodeSelector:
kubernetes.io/arch: amd64
containers:
- name: petstore
image: swaggerapi/petstore3:1.0.27@sha256:749432676c31cd05256f0d9e878302164debb94daa1c1223e193747e1b34e7d1
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: petstore
spec:
selector:
app: petstore
ports:
- port: 8080
targetPort: 8080
Apply the manifest:
kubectl apply -f petstore.yaml
Wait for the fixture to become ready:
kubectl rollout status -n aic deployment/petstore
Restrict Tool Access with an Allowlist
The following example demonstrates how to configure an allowlist so that a consumer can only call specific MCP tools.
- Admin API
- ADC
- Ingress Controller
Create a consumer alice:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/alice?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "alice"
}'
Create a key-auth credential for alice:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/alice/credentials/cred-alice-key-auth?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "cred-alice-key-auth",
"plugins": {
"key-auth": {
"key": "alice-key"
}
}
}'
Configure the mcp-tools-acl plugin on consumer alice:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/alice?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "alice",
"plugins": {
"mcp-tools-acl": {
"rules": [
{
"allow_tools": ["getPetById", "getUserByName"]
}
]
}
}
}'
❶ Allow consumer alice to call only getPetById and getUserByName. All other tools are blocked and will not appear in the tool listing.
Create a service with the Petstore API as its upstream:
curl -k "${API7_ADMIN_URL}/apisix/admin/services/mcp-tools-acl-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "mcp-tools-acl-service",
"upstream": {
"type": "roundrobin",
"scheme": "http",
"pass_host": "node",
"nodes": [
{
"host": "petstore",
"port": 8080,
"weight": 1
}
]
}
}'
Create a route in the service with openapi-to-mcp and key-auth enabled:
curl -k "${API7_ADMIN_URL}/apisix/admin/routes/mcp-tools-acl-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "mcp-tools-acl-route",
"service_id": "mcp-tools-acl-service",
"paths": ["/mcp"],
"methods": ["POST"],
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"openapi_url": "http://petstore:8080/api/v3/openapi.json",
"base_url": "http://petstore:8080/api/v3",
"headers": {
"Authorization": "special-key"
}
},
"key-auth": {}
}
}'
Send a tools/list request as consumer alice:
curl -s "http://127.0.0.1:9080/mcp" \
-H "apikey: alice-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
You should see only getPetById and getUserByName in the response. All other tools have been filtered out.
Call an allowed tool:
curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: alice-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "getPetById",
"arguments": {
"pathParameters": {
"petId": 1
}
}
}
}'
You should see an HTTP/1.1 200 OK response with the Petstore pet payload.
Call a tool that is not on the allowlist:
curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: alice-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "deletePet",
"arguments": {
"pathParameters": {
"petId": 1
}
}
}
}'
You should see an HTTP/1.1 403 Forbidden response, indicating the tool call was rejected.
Create a consumer with key-auth credential and mcp-tools-acl configured, and create a route with openapi-to-mcp and key-auth enabled:
consumers:
- username: alice
labels:
docs-example: mcp-tools-acl
credentials:
- name: cred-alice-key-auth
type: key-auth
config:
key: alice-key
plugins:
mcp-tools-acl:
rules:
- allow_tools:
- getPetById
- getUserByName
services:
- name: mcp-tools-acl-service
labels:
docs-example: mcp-tools-acl
upstream:
type: roundrobin
scheme: http
pass_host: node
nodes:
- host: petstore
port: 8080
weight: 1
routes:
- name: mcp-tools-acl-route
uris:
- /mcp
methods:
- POST
plugins:
key-auth:
header: apikey
openapi-to-mcp:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
❶ Allow consumer alice to call only getPetById and getUserByName. All other tools are blocked and will not appear in the tool listing.
ADC reconciles the selected resource types as desired state. Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=mcp-tools-acl
Synchronize the reviewed services and consumers, including their nested credentials:
adc sync -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=mcp-tools-acl
- Gateway API
- APISIX CRD
Create a consumer and a route with openapi-to-mcp, key-auth, and mcp-tools-acl configured as follows:
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: alice
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: cred-alice-key-auth
config:
key: alice-key
plugins:
- name: mcp-tools-acl
config:
rules:
- allow_tools:
- getPetById
- getUserByName
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: mcp-tools-acl-plugin-config
spec:
plugins:
- name: key-auth
config:
header: apikey
- name: openapi-to-mcp
config:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: mcp-tools-acl-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /mcp
method: POST
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: mcp-tools-acl-plugin-config
backendRefs:
- name: petstore-external-domain
port: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: petstore-external-domain
spec:
type: ExternalName
externalName: petstore.aic.svc.cluster.local
ports:
- port: 8080
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: petstore-external-domain
spec:
targetRefs:
- group: ""
kind: Service
name: petstore-external-domain
passHost: node
scheme: http
❶ Allow consumer alice to call only getPetById and getUserByName.
Create a consumer and a route with openapi-to-mcp, key-auth, and mcp-tools-acl configured as follows:
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: alice
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: alice-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: petstore-external-domain
spec:
ingressClassName: apisix
scheme: http
passHost: node
externalNodes:
- type: Domain
name: petstore.aic.svc.cluster.local
port: 8080
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: mcp-tools-acl-route
spec:
ingressClassName: apisix
http:
- name: mcp-tools-acl-route
match:
paths:
- /mcp
methods:
- POST
upstreams:
- name: petstore-external-domain
plugins:
- name: key-auth
enable: true
config:
header: apikey
- name: openapi-to-mcp
enable: true
config:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
- name: mcp-tools-acl
enable: true
config:
rules:
- allow_tools:
- getPetById
- getUserByName
❶ Allow only getPetById and getUserByName on this route.
Apply the configuration to your cluster:
kubectl apply -f mcp-tools-acl-ic.yaml
Restrict Tool Access with a Denylist
The following example demonstrates how to block a specific tool while leaving all other tools accessible.
- Admin API
- ADC
- Ingress Controller
Building on the previous example, create consumer bob with a denylist:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/bob?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "bob",
"plugins": {
"mcp-tools-acl": {
"rules": [
{
"deny_tools": ["deletePet"]
}
]
}
}
}'
❶ Deny consumer bob from calling deletePet. All other tools remain accessible.
Create a key-auth credential for bob:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/bob/credentials/cred-bob-key-auth?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "cred-bob-key-auth",
"plugins": {
"key-auth": {
"key": "bob-key"
}
}
}'
Send a tools/list request as consumer bob:
curl -s "http://127.0.0.1:9080/mcp" \
-H "apikey: bob-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
You should see all tools listed except deletePet.
Call the denied tool:
curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: bob-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "deletePet",
"arguments": {
"pathParameters": {
"petId": 1
}
}
}
}'
You should see an HTTP/1.1 403 Forbidden response.
Update the consumer configuration in adc.yaml to change the ACL rule:
consumers:
- username: bob
labels:
docs-example: mcp-tools-acl
credentials:
- name: cred-bob-key-auth
type: key-auth
config:
key: bob-key
plugins:
mcp-tools-acl:
rules:
- deny_tools:
- deletePet
services:
- name: mcp-tools-acl-service
labels:
docs-example: mcp-tools-acl
upstream:
type: roundrobin
scheme: http
pass_host: node
nodes:
- host: petstore
port: 8080
weight: 1
routes:
- name: mcp-tools-acl-route
uris:
- /mcp
methods:
- POST
plugins:
key-auth:
header: apikey
openapi-to-mcp:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
❶ Deny consumer bob from calling deletePet. All other tools remain accessible.
ADC reconciles the selected resource types as desired state. Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=mcp-tools-acl
Synchronize the reviewed services and consumers, including their nested credentials:
adc sync -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=mcp-tools-acl
- Gateway API
- APISIX CRD
Update the ACL rule in your consumer or route configuration:
# Other Configs
# ---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: bob
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: cred-bob-key-auth
config:
key: bob-key
plugins:
- name: mcp-tools-acl
config:
rules:
- deny_tools:
- deletePet
❶ Deny consumer bob from calling deletePet.
Update the ACL rule in your consumer or route configuration:
# Other Configs
# ---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: mcp-tools-acl-route
spec:
ingressClassName: apisix
http:
- name: mcp-tools-acl-route
match:
paths:
- /mcp
methods:
- POST
upstreams:
- name: petstore-external-domain
plugins:
- name: key-auth
enable: true
config:
header: apikey
- name: openapi-to-mcp
enable: true
config:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
- name: mcp-tools-acl
enable: true
config:
rules:
- deny_tools:
- deletePet
❶ Deny calls to deletePet while allowing other tools on the route.
Apply the configuration to your cluster:
kubectl apply -f mcp-tools-acl-ic.yaml
Apply Different Rules Based on Route Conditions
The following example demonstrates how to use expression conditions (expr) to apply different ACL rules depending on request context, such as the route being accessed.
- Admin API
- ADC
- Ingress Controller
Create a consumer grace with conditional rules:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/grace?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"username": "grace",
"plugins": {
"mcp-tools-acl": {
"rules": [
{
"expr": [["route_id", "==", "route-pets"]],
"allow_tools": ["getPetById"]
},
{
"allow_tools": ["getUserByName"]
}
]
}
}
}'
❶ Rule 1: When the request hits the route route-pets, only getPetById is allowed.
❷ Rule 2 is a catch-all rule without expr. For all other routes, only getUserByName is allowed. This rule is reached only if Rule 1 does not match.
Create a key-auth credential for grace:
curl -k "${API7_ADMIN_URL}/apisix/admin/consumers/grace/credentials/cred-grace-key-auth?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "cred-grace-key-auth",
"plugins": {
"key-auth": {
"key": "grace-key"
}
}
}'
Create a service with the Petstore API as its upstream, or reuse the one from the first example:
curl -k "${API7_ADMIN_URL}/apisix/admin/services/mcp-tools-acl-service?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "mcp-tools-acl-service",
"upstream": {
"type": "roundrobin",
"scheme": "http",
"pass_host": "node",
"nodes": [
{
"host": "petstore",
"port": 8080,
"weight": 1
}
]
}
}'
The route route-pets below uses the /mcp path. If you created mcp-tools-acl-route in the previous examples, delete it first, because the Dashboard rejects a route that matches the same path as another route in the service:
curl -k "${API7_ADMIN_URL}/apisix/admin/routes/mcp-tools-acl-route?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X DELETE \
-H "X-API-KEY: ${API7_ADMIN_KEY}"
Create two routes in the service with openapi-to-mcp and key-auth:
curl -k "${API7_ADMIN_URL}/apisix/admin/routes/route-pets?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "route-pets",
"service_id": "mcp-tools-acl-service",
"paths": ["/mcp"],
"methods": ["POST"],
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"openapi_url": "http://petstore:8080/api/v3/openapi.json",
"base_url": "http://petstore:8080/api/v3",
"headers": { "Authorization": "special-key" }
},
"key-auth": {}
}
}'
curl -k "${API7_ADMIN_URL}/apisix/admin/routes/route-inventory?gateway_group_id=${API7_GATEWAY_GROUP_ID}" -X PUT \
-H "X-API-KEY: ${API7_ADMIN_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "route-inventory",
"service_id": "mcp-tools-acl-service",
"paths": ["/mcp2"],
"methods": ["POST"],
"plugins": {
"openapi-to-mcp": {
"transport": "streamable_http",
"openapi_url": "http://petstore:8080/api/v3/openapi.json",
"base_url": "http://petstore:8080/api/v3",
"headers": { "Authorization": "special-key" }
},
"key-auth": {}
}
}'
When grace calls route-pets, Rule 1 matches and only getPetById is allowed:
curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: grace-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "getPetById",
"arguments": {
"pathParameters": {
"petId": 1
}
}
}
}'
You should see an HTTP/1.1 200 OK response with the Petstore pet payload.
When grace calls route-inventory, Rule 1 does not match because the route_id is different. The catch-all Rule 2 applies, so only getUserByName is allowed:
curl -i "http://127.0.0.1:9080/mcp2" \
-H "apikey: grace-key" \
-H "Accept: application/json, text/event-stream" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "getUserByName",
"arguments": {
"pathParameters": {
"username": "user1"
}
}
}
}'
You should see an HTTP/1.1 200 OK response with the Petstore user payload.
Rules are evaluated top to bottom. The first matching rule takes effect and all subsequent rules are skipped. Place more specific rules (with expr) before broader catch-all rules (without expr).
If no rule matches because all rules have expr conditions that evaluate to false, the plugin does not enforce access control; all tools are passed through.
Create a consumer with conditional rules and two routes:
consumers:
- username: grace
labels:
docs-example: mcp-tools-acl
credentials:
- name: cred-grace-key-auth
type: key-auth
config:
key: grace-key
plugins:
mcp-tools-acl:
rules:
- expr:
-
- route_name
- ==
- pets-route
allow_tools:
- getPetById
- allow_tools:
- getUserByName
services:
- name: pets-mcp-service
labels:
docs-example: mcp-tools-acl
upstream:
type: roundrobin
scheme: http
pass_host: node
nodes:
- host: petstore
port: 8080
weight: 1
routes:
- name: pets-route
uris:
- /mcp
methods:
- POST
plugins:
key-auth:
header: apikey
openapi-to-mcp:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
- name: inventory-mcp-service
labels:
docs-example: mcp-tools-acl
upstream:
type: roundrobin
scheme: http
pass_host: node
nodes:
- host: petstore
port: 8080
weight: 1
routes:
- name: inventory-route
uris:
- /mcp2
methods:
- POST
plugins:
key-auth:
header: apikey
openapi-to-mcp:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
❶ When the request hits pets-route, only getPetById is allowed.
❷ A catch-all rule for all other routes. Here only getUserByName is allowed.
ADC reconciles the selected resource types as desired state. Preview changes owned by this example and confirm that the diff contains no unintended updates or deletions:
adc diff -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=mcp-tools-acl
Synchronize the reviewed services and consumers, including their nested credentials:
adc sync -f adc.yaml \
--include-resource-type service \
--include-resource-type consumer \
--label-selector docs-example=mcp-tools-acl
- Gateway API
- APISIX CRD
Configure conditional rules with separate MCP routes:
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: grace
spec:
gatewayRef:
name: apisix
credentials:
- type: key-auth
name: cred-grace-key-auth
config:
key: grace-key
plugins:
- name: mcp-tools-acl
config:
rules:
- expr:
-
- route_name
- ==
- pets-route
allow_tools:
- getPetById
- allow_tools:
- getUserByName
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: mcp-tools-acl-plugin-config
spec:
plugins:
- name: key-auth
config:
header: apikey
- name: openapi-to-mcp
config:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: pets-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /mcp
method: POST
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: mcp-tools-acl-plugin-config
backendRefs:
- name: petstore-external-domain
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: inventory-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /mcp2
method: POST
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: mcp-tools-acl-plugin-config
backendRefs:
- name: petstore-external-domain
port: 8080
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: petstore-external-domain
spec:
type: ExternalName
externalName: petstore.aic.svc.cluster.local
ports:
- port: 8080
---
apiVersion: apisix.apache.org/v1alpha1
kind: BackendTrafficPolicy
metadata:
namespace: aic
name: petstore-external-domain
spec:
targetRefs:
- group: ""
kind: Service
name: petstore-external-domain
passHost: node
scheme: http
❶ Apply getPetById only when the request matches pets-route.
❷ Apply getUserByName to all other matching routes.
Configure conditional rules with separate MCP routes:
apiVersion: apisix.apache.org/v2
kind: ApisixConsumer
metadata:
namespace: aic
name: grace
spec:
ingressClassName: apisix
authParameter:
keyAuth:
value:
key: grace-key
---
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: petstore-external-domain
spec:
ingressClassName: apisix
scheme: http
passHost: node
externalNodes:
- type: Domain
name: petstore.aic.svc.cluster.local
port: 8080
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: mcp-tools-acl-routes
spec:
ingressClassName: apisix
http:
- name: pets-route
match:
paths:
- /mcp
methods:
- POST
upstreams:
- name: petstore-external-domain
plugins:
- name: key-auth
enable: true
config:
header: apikey
- name: openapi-to-mcp
enable: true
config:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
- name: mcp-tools-acl
enable: true
config:
rules:
- expr:
-
- uri
- ==
- /mcp
allow_tools:
- getPetById
- allow_tools:
- getUserByName
- name: inventory-route
match:
paths:
- /mcp2
methods:
- POST
upstreams:
- name: petstore-external-domain
plugins:
- name: key-auth
enable: true
config:
header: apikey
- name: openapi-to-mcp
enable: true
config:
transport: streamable_http
base_url: http://petstore:8080/api/v3
headers:
Authorization: special-key
openapi_url: http://petstore:8080/api/v3/openapi.json
- name: mcp-tools-acl
enable: true
config:
rules:
- expr:
-
- uri
- ==
- /mcp
allow_tools:
- getPetById
- allow_tools:
- getUserByName
❶ Apply getPetById only when the request URI is /mcp.
❷ Use the catch-all rule to allow only getUserByName for the /mcp2 route.
Apply the configuration to your cluster:
kubectl apply -f mcp-tools-acl-ic.yaml
Troubleshooting
Plugin has no effect
Check that openapi-to-mcp is enabled on the same route and that an authentication plugin is configured. Without an authenticated consumer on the request, mcp-tools-acl passes all traffic unchanged by design.
tools/call returns HTTP 400
The request body is valid JSON but params is missing or params.name is not a string. The plugin returns {"message": "Invalid MCP tools/call request"} with HTTP 400. This is distinct from rejected_code (which applies to denied tools) and indicates a malformed request from the MCP client.
allow_tools: [] blocks all tools
An empty allowlist is schema-valid and denies all tools. Every tools/call will be rejected and tools/list will return an empty list. If you want consumers to access any tools, ensure the allow_tools array is non-empty.