ALICE_API_KEY=$(curl -X POST "https://us.api.konghq.com/v1/ai-gateways/$AI_GATEWAY_ID/consumers/$ALICE_ID/credentials" \
--no-progress-meter --fail-with-body \
-H "Authorization: Bearer $KONNECT_TOKEN"\
-H "Content-Type: application/json"\
-H "Accept: application/json, application/problem+json" \
--json '{
"display_name": "Alice key",
"name": "alice-key",
"type": "api-key"
}' | jq -r '.api_key'
)Control MCP tool access with AI Consumer and AI Consumer Group ACLs
Use the AI MCP Server entity to control access to MCP tools with default and per-tool ACLs based on AI Consumers and AI Consumer Groups.
This tutorial converts the Swagger Petstore API into MCP tools, authenticates callers with a key-auth AI Auth Strategy, then gates each tool by AI Consumer Group membership and by individual AI Consumer.
Prerequisites
AI Gateway running
This is a Konnect tutorial and requires a Konnect personal access token.
-
Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.
-
Export your token to an environment variable:
export KONNECT_TOKEN='YOUR_KONNECT_PAT' -
Run the AI Gateway quickstart script to automatically provision a control plane and data plane in Kong Konnect, and configure your environment:
curl -Ls https://get.konghq.com/ai | bash -s -- -k $KONNECT_TOKEN
This sets up a AI Gateway control plane named ai-quickstart, provisions a local data plane, and prints out the following environment variables export:
export AI_GATEWAY_ID=your-gateway-id
export KONNECT_TOKEN=$KONNECT_TOKEN
export KONNECT_CONTROL_PLANE_NAME=ai-quickstart
export KONNECT_CONTROL_PLANE_URL=https://us.api.konghq.com
export KONNECT_PROXY_URL='http://localhost:8000'Copy and paste these into your terminal to configure your session.
kongctl v1.16.0+
This tutorial uses kongctl to manage Konnect resources programmatically. We recommend keeping kongctl up to date with the latest version (1.16.0).
- Install kongctl from developer.konghq.com/kongctl.
-
Verify the installation:
kongctl version
Petstore API
This tutorial uses Swagger’s Petstore API, run the following command to start the server:
docker run -d \
--name swagger-petstore \
--network kong-ai-quickstart-net \
--network-alias host.docker.internal \
-p 8080:8080 \
swaggerapi/petstore3:latestCreate AI Consumer Groups for each access tier
Configure three different AI Consumer Groups that reflect access levels:
admin: Full access, including destructive toolssupport: Read-only access to pet and store datasuspended: Blocked from MCP tools
These groups govern MCP tool permissions.
kongctl apply -f - --auto-approve --pat "$KONNECT_TOKEN" << 'EOF'
ai_gateway_consumer_groups:
- ref: admin
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
display_name: Admin
name: admin
policies: []
- ref: support
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
display_name: Support
name: support
policies: []
- ref: suspended
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
display_name: Suspended
name: suspended
policies: []
EOFCreate AI Consumers
-
Configure individual AI Consumers and add them to their groups. Each one inherits the ACL rules of its group, and Eason, who belongs to no group, is only reachable through the tool-level ACLs you’ll set in the next section:
kongctl apply -f - --auto-approve --pat "$KONNECT_TOKEN" << 'EOF' ai_gateway_consumers: - ref: alice ai_gateway: !lookup {id: !env AI_GATEWAY_ID} display_name: Alice name: alice type: api-key policies: [] - ref: bob ai_gateway: !lookup {id: !env AI_GATEWAY_ID} display_name: Bob name: bob type: api-key policies: [] - ref: carol ai_gateway: !lookup {id: !env AI_GATEWAY_ID} display_name: Carol name: carol type: api-key policies: [] - ref: eason ai_gateway: !lookup {id: !env AI_GATEWAY_ID} display_name: Eason name: eason type: api-key policies: [] ai_gateway_consumer_groups: - ref: admin ai_gateway: !lookup {id: !env AI_GATEWAY_ID} name: admin display_name: Admin policies: [] consumers: - !ref alice#name - ref: support ai_gateway: !lookup {id: !env AI_GATEWAY_ID} name: support display_name: Support policies: [] consumers: - !ref bob#name - ref: suspended ai_gateway: !lookup {id: !env AI_GATEWAY_ID} name: suspended display_name: Suspended policies: [] consumers: - !ref carol#name EOFConsumer credentials for production environments: In a production environment, we recommend using OpenID Connect with consumer credentials instead of key auth to authenticate human users. For a complete tutorial, see Identify AI Consumers on AI Model traffic with Kong Identity.
-
Export each AI Consumer’s ID as an environment variable using
kongctl get. The credential requests that follow identify each consumer by ID, not by name:export ALICE_ID=$(kongctl get ai-gateway consumers --gateway-id "$AI_GATEWAY_ID" alice --output json --jq '.id' -r) export BOB_ID=$(kongctl get ai-gateway consumers --gateway-id "$AI_GATEWAY_ID" bob --output json --jq '.id' -r) export CAROL_ID=$(kongctl get ai-gateway consumers --gateway-id "$AI_GATEWAY_ID" carol --output json --jq '.id' -r) export EASON_ID=$(kongctl get ai-gateway consumers --gateway-id "$AI_GATEWAY_ID" eason --output json --jq '.id' -r) -
Create an API key credential for Alice, and save the generated key. AI Gateway generates the key value; it isn’t set by you and can’t be retrieved again after this step:
If this fails with a
405error, log in withkongctl loginand export the AI Consumer IDs again. -
Create an API key credential for Bob:
BOB_API_KEY=$(curl -X POST "https://us.api.konghq.com/v1/ai-gateways/$AI_GATEWAY_ID/consumers/$BOB_ID/credentials" \ --no-progress-meter --fail-with-body \ -H "Authorization: Bearer $KONNECT_TOKEN"\ -H "Content-Type: application/json"\ -H "Accept: application/json, application/problem+json" \ --json '{ "display_name": "Bob key", "name": "bob-key", "type": "api-key" }' | jq -r '.api_key' ) -
Create an API key credential for Carol:
CAROL_API_KEY=$(curl -X POST "https://us.api.konghq.com/v1/ai-gateways/$AI_GATEWAY_ID/consumers/$CAROL_ID/credentials" \ --no-progress-meter --fail-with-body \ -H "Authorization: Bearer $KONNECT_TOKEN"\ -H "Content-Type: application/json"\ -H "Accept: application/json, application/problem+json" \ --json '{ "display_name": "Carol key", "name": "carol-key", "type": "api-key" }' | jq -r '.api_key' ) -
Create an API key credential for Eason:
EASON_API_KEY=$(curl -X POST "https://us.api.konghq.com/v1/ai-gateways/$AI_GATEWAY_ID/consumers/$EASON_ID/credentials" \ --no-progress-meter --fail-with-body \ -H "Authorization: Bearer $KONNECT_TOKEN"\ -H "Content-Type: application/json"\ -H "Accept: application/json, application/problem+json" \ --json '{ "display_name": "Eason key", "name": "eason-key", "type": "api-key" }' | jq -r '.api_key' )
Configure the AI MCP Server
Configure the AI MCP Server to convert the Petstore API into MCP tools and apply tool-level access rules. Access is determined by AI Consumer Groups and individual AI Consumers using allow and deny lists. A tool ACL replaces the default rule when present.
The following table shows the effective permissions for this configuration:
|
MCP tool |
Admin group |
Support group |
Eason consumer |
Suspended group |
|---|---|---|---|---|
get-pets-by-status
|
Supported | Supported | Supported | Not supported |
get-pet-by-id
|
Supported | Supported | Not supported | Not supported |
get-inventory
|
Supported | Supported | Not supported | Not supported |
get-order-by-id
|
Supported | Supported | Not supported | Not supported |
delete-pet
|
Supported | Not supported | Not supported | Not supported |
Apply the following configuration:
- A
key-authAI Auth Strategy so each AI Consumer presents their key in theapikeyheader - The AI MCP Server, its converted Petstore tools, and their ACL rules
kongctl apply -f - --auto-approve --pat "$KONNECT_TOKEN" << 'EOF'
ai_gateway_auth_strategies:
- ref: my-key-auth
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: my-key-auth
display_name: "my-key-auth"
type: key-auth
config:
key_names:
- apikey
key_in_header: true
key_in_query: false
ai_gateway_mcp_servers:
- ref: petstore-acl-mcp
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: petstore-acl-mcp
display_name: "Petstore API"
type: conversion-listener
enabled: true
policies: []
access:
auth_strategies:
- !ref my-key-auth#name
acl_attribute_type: consumer
acls:
allow: []
default_tool_acls:
allow:
- admin
deny: []
config:
url: http://host.docker.internal:8080/api/v3
route:
paths:
- /petstore-acl
logging:
payloads: false
server:
timeout: 60000
tools:
- name: get-pets-by-status
description: Find pets by status
method: GET
path: /petstore-acl/pet/findByStatus
access:
acls:
allow:
- admin
- support
- eason
parameters:
- name: status
in: query
required: true
schema:
type: string
enum:
- available
- pending
- sold
description: Status value to filter pets by
- name: get-pet-by-id
description: Get a pet by ID
method: GET
path: /petstore-acl/pet/{petId}
access:
acls:
allow:
- admin
- support
parameters:
- description: ID of the pet to retrieve
in: path
name: petId
required: true
schema:
type: integer
- name: get-inventory
description: Get pet inventories by status
method: GET
path: /petstore-acl/store/inventory
access:
acls:
allow:
- admin
- support
- name: get-order-by-id
description: Get a purchase order by ID
method: GET
path: /petstore-acl/store/order/{orderId}
access:
acls:
allow:
- admin
- support
parameters:
- description: ID of the order to retrieve
in: path
name: orderId
required: true
schema:
type: integer
- name: delete-pet
description: Delete a pet
method: DELETE
path: /petstore-acl/pet/{petId}
access:
acls:
allow:
- admin
deny:
- support
parameters:
- description: ID of the pet to delete
in: path
name: petId
required: true
schema:
type: integer
EOF
suspendedhas no per-tool ACL entry anywhere, so Carol falls through toaccess.default_tool_acls, which only allowsadmin. This blocks Carol from every tool without needing an explicitdeny.
Validate
Validate the ACL rules with the MCP Inspector CLI, passing each AI Consumer’s API key in the apikey header. The set of tools each AI Consumer can discover and call reflects their group membership.
Alice sees and can call every tool
Alice is in admin, which access.default_tool_acls allows and every tool’s allow list names, so Alice discovers all five tools:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/list \
--header "apikey: $ALICE_API_KEY" | jq -r '.tools[].name' | sortYou should see the following output:
delete-pet
get-inventory
get-order-by-id
get-pet-by-id
get-pets-by-statusCalling delete-pet, the only tool restricted to admin, also succeeds:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/call \
--tool-name delete-pet \
--tool-arg path_petId=10 \
--header "apikey: $ALICE_API_KEY"The tool result confirms the deletion with Pet deleted.
Bob can read, but not delete
Bob is in support, which is on the deny list for delete-pet. Tool discovery filters that tool out, so Bob only sees four:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/list \
--header "apikey: $BOB_API_KEY" | jq -r '.tools[].name' | sortYou should see the following output:
get-inventory
get-order-by-id
get-pet-by-id
get-pets-by-statusThe tools Bob can reach work as normal. Calling get-pet-by-id returns Lion 1:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/call \
--tool-name get-pet-by-id \
--tool-arg path_petId=7 \
--header "apikey: $BOB_API_KEY" | jq -r '.content[0].text' | jq -c '.'You should see the following response:
{"id":7,"category":{"id":4,"name":"Lions"},"name":"Lion 1","photoUrls":["url1","url2"],"tags":[{"id":1,"name":"tag1"},{"id":2,"name":"tag2"}],"status":"available"}Invoking delete-pet directly confirms the same rule that filtered it out of his tool list. The call is rejected with HTTP 403 Forbidden, which the MCP Inspector CLI reports as a transport error and a non-zero exit code:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/call \
--tool-name delete-pet \
--tool-arg path_petId=9 \
--header "apikey: $BOB_API_KEY"You should see output similar to the following:
Failed to call tool delete-pet: Streamable HTTP error: Error POSTing to endpoint: ...403 Forbidden...Carol is blocked from every tool
Carol is in suspended, which no tool allows and access.default_tool_acls doesn’t include, so Carol’s tools/list succeeds but returns an empty list:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/list \
--header "apikey: $CAROL_API_KEY" | jq '.tools | length'Invoking a tool directly returns HTTP 403 Forbidden:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/call \
--tool-name get-inventory \
--header "apikey: $CAROL_API_KEY"You should see output similar to the following:
Failed to call tool delete-pet: Streamable HTTP error: Error POSTing to endpoint: ...403 Forbidden...Eason only has access to the pet catalogue
Eason belongs to no AI Consumer Group, but the get-pets-by-status tool’s own access.acls.allow names Eason directly, alongside admin and support. Every other tool falls back to access.default_tool_acls, which doesn’t include them, so Eason sees a single tool:
npx -y @modelcontextprotocol/inspector@0.22.0 --cli \
$KONNECT_PROXY_URL/petstore-acl \
--transport http --method tools/list \
--header "apikey: $EASON_API_KEY" | jq -r '.tools[].name' | sortYou should see the following output:
get-pets-by-statusThis is how an individual AI Consumer can be granted an exception without creating an AI Consumer Group for them, and why a tool ACL has to name every subject it allows: get-pets-by-status lists admin explicitly, because its own ACL replaced access.default_tool_acls rather than extending it.
FAQs
Why does a denied tool call fail at the transport layer instead of returning a JSON-RPC error?
An ACL denial is enforced on the route, before the request is dispatched as an MCP RPC, so AI Gateway returns a plain HTTP 403 Forbidden response rather than a JSON-RPC error object. The MCP Inspector CLI surfaces this as a Streamable HTTP error and exits with a non-zero status. An MCP client that assumes every response is JSON-RPC needs to handle the status code itself.
Why does tools/list succeed for a blocked AI Consumer instead of failing?
Tool discovery and tool invocation are evaluated separately. Discovery filters the list down to the tools the AI Consumer is allowed to reach, so a fully blocked AI Consumer gets an HTTP 200 response with an empty tools array instead of an outright rejection. Invoking a tool directly returns HTTP 403 Forbidden.
Why does a per-tool ACL have to repeat groups that access.default_tool_acls already allows?
A per-tool ACL replaces the default for that tool, it doesn’t merge with it. When a tool defines its own access.acls, AI Gateway ignores access.default_tool_acls for that tool entirely, so the tool’s allow list must name every subject that should reach it. See How default and per-tool ACLs work.