Control MCP tool access with AI Consumer and AI Consumer Group ACLs

TL;DR

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

This is a Konnect tutorial and requires a Konnect personal access token.

  1. Create a new personal access token by opening the Konnect PAT page and selecting Generate Token.

  2. Export your token to an environment variable:

    export KONNECT_TOKEN='YOUR_KONNECT_PAT'
  3. 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.

This tutorial uses kongctl to manage Konnect resources programmatically. We recommend keeping kongctl up to date with the latest version (1.16.0).

  1. Install kongctl from developer.konghq.com/kongctl.
  2. Verify the installation:

    kongctl version

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:latest

Create AI Consumer Groups for each access tier

Configure three different AI Consumer Groups that reflect access levels:

  • admin: Full access, including destructive tools
  • support: Read-only access to pet and store data
  • suspended: 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: []
EOF

Create AI Consumers

  1. 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
    EOF

    Consumer 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.

  2. 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)
  3. 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:

    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'
    )

    If this fails with a 405 error, log in with kongctl login and export the AI Consumer IDs again.

  4. 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'
    )
  5. 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'
    )
  6. 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-auth AI Auth Strategy so each AI Consumer presents their key in the apikey header
  • 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

suspended has no per-tool ACL entry anywhere, so Carol falls through to access.default_tool_acls, which only allows admin. This blocks Carol from every tool without needing an explicit deny.

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' | sort

You should see the following output:

delete-pet
get-inventory
get-order-by-id
get-pet-by-id
get-pets-by-status

Calling 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' | sort

You should see the following output:

get-inventory
get-order-by-id
get-pet-by-id
get-pets-by-status

The 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' | sort

You should see the following output:

get-pets-by-status

This 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

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.

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.

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.

Help us make these docs great!

Kong Developer docs are open source. If you find these useful and want to make them better, contribute today!