Skip to main content

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/call blocking — rejects calls to denied tools with an HTTP error response.
  • tools/list filtering — strips denied tools from the list returned to the client, so disallowed tools are invisible to MCP clients. This applies to both JSON responses and SSE (Server-Sent Events) streaming 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 rule whose conditions match is applied and the rest are skipped.

This plugin is available in API7 Enterprise from version 3.9.8. It is not available in APISIX yet.

Prerequisites

Before using this plugin, ensure that:

  1. The openapi-to-mcp plugin is enabled on the same route.
  2. An authentication plugin (e.g., key-auth) is configured on the route. Without an authenticated consumer on the request, mcp-tools-acl passes all traffic unchanged.

Examples

The examples below demonstrate how you can use the mcp-tools-acl plugin for different scenarios.

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.

Create a consumer alice:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "alice"
}'

Create a key-auth credential for alice:

curl "http://127.0.0.1:9180/apisix/admin/consumers/alice/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-alice-key-auth",
"plugins": {
"key-auth": {
"key": "alice-key"
}
}
}'

Configure the mcp-tools-acl plugin on consumer alice:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "alice",
"plugins": {
"mcp-tools-acl": {
"rules": [
{
"allow_tools": ["get_weather", "list_cities"]
}
]
}
}
}'

❶ Allow consumer alice to call only get_weather and list_cities. All other tools are blocked and will not appear in the tool listing.

tip

Configure mcp-tools-acl on the consumer (or consumer group) to enable per-consumer tool access control. The route only needs openapi-to-mcp and an auth plugin.

When the plugin is configured on both the consumer and the route, the consumer configuration takes priority and the route-level configuration is ignored for that consumer. If a consumer has no mcp-tools-acl configuration, the route-level configuration applies as a fallback.

Create a route with openapi-to-mcp and key-auth enabled:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "mcp-tools-acl-route",
"uri": "/mcp",
"methods": ["GET", "POST"],
"plugins": {
"openapi-to-mcp": {
"openapi_url": "http://your-service/openapi.json",
"base_url": "http://your-service"
},
"key-auth": {}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"your-service:80": 1
}
}
}'

Send a tools/list request as consumer alice:

curl -s "http://127.0.0.1:9080/mcp" \
-H "apikey: alice-key" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

You should see only get_weather and list_cities in the response — all other tools have been filtered out.

Call a tool that is not on the allowlist:

curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: alice-key" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_record","arguments":{}}}'

You should see an HTTP/1.1 403 Forbidden response, indicating the tool call was rejected.

Restrict Tool Access with a Denylist

The following example demonstrates how to block a specific tool while leaving all other tools accessible.

Building on the previous example, create consumer bob with a denylist:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "bob",
"plugins": {
"mcp-tools-acl": {
"rules": [
{
"deny_tools": ["delete_record"]
}
]
}
}
}'

❶ Deny consumer bob from calling delete_record. All other tools remain accessible.

Create a key-auth credential for bob:

curl "http://127.0.0.1:9180/apisix/admin/consumers/bob/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "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" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'

You should see all tools listed except delete_record.

Call the denied tool:

curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: bob-key" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_record","arguments":{}}}'

You should see an HTTP/1.1 403 Forbidden response.

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.

This example applies to API7 Enterprise version 3.9.8 and later. It is not applicable to APISIX, as the mcp-tools-acl plugin is not yet supported.

Create a consumer grace with conditional rules:

curl "http://127.0.0.1:9180/apisix/admin/consumers" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"username": "grace",
"plugins": {
"mcp-tools-acl": {
"rules": [
{
"expr": [["route_id", "==", "route-pets"]],
"allow_tools": ["findPetsByStatus"]
},
{
"allow_tools": ["addPet"]
}
]
}
}
}'

❶ Rule 1: When the request hits the route route-pets, only findPetsByStatus is allowed.

❷ Rule 2: A catch-all rule (no expr) — for all other routes, only addPet is allowed. This rule is reached only if Rule 1 does not match.

Create a key-auth credential for grace:

curl "http://127.0.0.1:9180/apisix/admin/consumers/grace/credentials" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "cred-grace-key-auth",
"plugins": {
"key-auth": {
"key": "grace-key"
}
}
}'

Create two routes with openapi-to-mcp and key-auth:

curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "route-pets",
"uri": "/mcp",
"methods": ["GET", "POST"],
"plugins": {
"openapi-to-mcp": { "openapi_url": "http://petstore/openapi.json", "base_url": "http://petstore" },
"key-auth": {}
},
"upstream": { "type": "roundrobin", "nodes": { "petstore:80": 1 } }
}'
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${ADMIN_API_KEY}" \
-d '{
"id": "route-inventory",
"uri": "/mcp2",
"methods": ["GET", "POST"],
"plugins": {
"openapi-to-mcp": { "openapi_url": "http://inventory/openapi.json", "base_url": "http://inventory" },
"key-auth": {}
},
"upstream": { "type": "roundrobin", "nodes": { "inventory:80": 1 } }
}'

When grace calls route-pets, Rule 1 matches and only findPetsByStatus is allowed:

curl -i "http://127.0.0.1:9080/mcp" \
-H "apikey: grace-key" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"findPetsByStatus","arguments":{}}}'

You should see an HTTP/1.1 202 Accepted response, indicating the tool call was accepted by the MCP server.

When grace calls route-inventory, Rule 1 does not match (wrong route_id), so the catch-all Rule 2 applies — only addPet is allowed:

curl -i "http://127.0.0.1:9080/mcp2" \
-H "apikey: grace-key" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"addPet","arguments":{}}}'

You should see an HTTP/1.1 202 Accepted response, indicating the tool call was accepted by the MCP server.

note

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 (all rules have expr conditions and none evaluate to true), the plugin does not enforce any access control — all tools are passed through.

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.

API7.ai Logo

The digital world is connected by APIs,
API7.ai exists to make APIs more efficient, reliable, and secure.

Sign up for API7 newsletter

Product

API7 Gateway

SOC2 Type IIISO 27001HIPAAGDPRRed Herring

Copyright © APISEVEN PTE. LTD 2019 – 2026. Apache, Apache APISIX, APISIX, and associated open source project names are trademarks of the Apache Software Foundation