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: $ALICE_API_KEY" | jq -r '.content[0].text' | jq -c '.'Observe MCP traffic with the File Log Policy
Use the File Log Policy to write MCP tool activity to a local file, and set config.logging.audits: true on the AI MCP Server so every ACL decision is recorded.
This tutorial attaches a File Log Policy to the Petstore AI MCP Server you built in Control MCP tool access with AI Consumer and AI Consumer Group ACLs, generates allowed and denied tool calls with the MCP Inspector CLI, then reads the resulting ai.mcp log entries.
Prerequisites
Series Prerequisites
This page is part of the Control and observe MCP tool access with AI Gateway series.
Complete the previous page, Control MCP tool access with AI Consumer and AI Consumer Group ACLs before completing this page.
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
Attach a File Log Policy
Create a File Log Policy and attach it to the existing petstore-acl-mcp AI MCP Server.
Because kongctl manages each AI MCP Server declaratively, this configuration repeats the entity’s full definition from the previous tutorial, with two additions: the new AI Policy in policies, and config.logging.audits: true.
kongctl apply -f - --auto-approve --pat "$KONNECT_TOKEN" << 'EOF'
ai_gateway_policies:
- ref: my-file-log
ai_gateway: !lookup {id: !env AI_GATEWAY_ID}
name: my-file-log
display_name: "my-file-log"
type: file-log
enabled: true
global: false
config:
path: /tmp/mcp.json
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:
- !ref my-file-log#name
access:
auth_strategies:
- my-key-auth
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
audits: true
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
EOFGenerate MCP traffic
Call the AI MCP Server as two different AI Consumers using the MCP Inspector CLI and passing each one’s API key in the apikey header.
-
Alice, in
admin, successfully callsget-pet-by-id: -
Bob, in
support, is denieddelete-pet. The call is rejected withHTTP 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"
Validate the log entries
Each line in /tmp/mcp.json is a complete File Log entry, and MCP activity is in an ai.mcp object alongside the standard request, response, and consumer fields.
Read the log from inside your AI Gateway Docker container. Filtering to entries that carry an audit array narrows the output to the ACL decisions, skipping the initialize and notifications/initialized handshake requests that each MCP Inspector CLI invocation also generates:
docker exec kong-ai-quickstart-gateway cat /tmp/mcp.json \
| jq 'select(.ai.mcp.audit) | .ai.mcp'You get four entries, because each AI Consumer’s client discovers the tool list before invoking a tool, and discovery is itself an ACL decision. The entries without a primitive_name are the tools/list decisions; the two that name a tool are the calls.
An allowed call produces both an rpc entry and an audit entry. Alice’s get-pet-by-id call looks like the following:
{
"mcp_session_id": "98971c19-9539-4611-9cfe-a9eefc11f702",
"mcp_server_id": "8c3cd66f-4c4f-4809-a671-2f412a794d01",
"protocol_version": "2025-11-25",
"rpc": [
{
"method": "tools/call",
"tool_name": "get-pet-by-id",
"latency": 46,
"response_body_size": 290,
"id": "2"
}
],
"audit": [
{
"primitive": "tool",
"scope": "primitive",
"primitive_name": "get-pet-by-id",
"consumer": {
"name": "admin",
"id": "327670e0-4d13-47a8-9453-5b7370bfbba7",
"identifier": "consumer_group"
},
"action": "allow"
}
],
"tool_list_cache_hit": true
}In an audit entry, consumer.id is always the UUID of the AI Consumer that made the call, while consumer.name is the subject that actually matched the ACL rule. consumer.identifier tells you which kind of subject that was:
consumer_group: an AI Consumer Group granted the decision, andnameis the group. Alice’s call matched throughadmin.username: the individual AI Consumer matched, andnameis the AI Consumer’s own name.
That means the same AI Consumer can show up under either identifier depending on which rule decided a given request. For the full field list, see the audit log reference.
Bob’s denied delete-pet call has no rpc entry at all, only the audit entry recording the ACL decision:
{
"mcp_session_id": "1d8ca25a-50b5-4871-b4e9-81d0f2f3fa89",
"mcp_server_id": "8c3cd66f-4c4f-4809-a671-2f412a794d01",
"protocol_version": "2025-11-25",
"tool_list_cache_hit": true,
"audit": [
{
"primitive": "tool",
"scope": "primitive",
"primitive_name": "delete-pet",
"consumer": {
"name": "support",
"id": "ae05d1a5-1a43-4176-a882-af5c2d0b2e78",
"identifier": "consumer_group"
},
"action": "deny"
}
]
}Confirm that the denial was recorded:
docker exec kong-ai-quickstart-gateway cat /tmp/mcp.json \
| jq -r '.ai.mcp.audit[]? | select(.primitive_name == "delete-pet") | .action' \
| sort -uMCP traffic in Konnect
You can also see tool usage and ACL denials without setting up a File Log Policy, using Konnect Analytics. Go to Observability > Dashboards, create a dashboard from the Agentic analytics template, and filter by tool name or AI Consumer.
For metrics-based observability instead of raw log entries, see Monitor MCP traffic with OpenTelemetry.
Cleanup
Stop Petstore API
docker rm -f swagger-petstoreClean up AI Gateway resources
To clean up all AI Gateway resources created in this guide, run:
curl -Ls https://get.konghq.com/ai | bash -s -- -dFAQs
Why doesn’t a denied call have a rpc entry?
The ACL check runs before the request is dispatched as an RPC, so a denied call never becomes one. Only the ai.mcp.audit entry records it. That also means rpc latency and size data only ever covers calls that reached the upstream MCP server.
Why does a single tool call write several log entries?
The MCP Inspector CLI opens a fresh MCP session for each invocation, so one tools/call command produces a short sequence of requests: initialize, notifications/initialized, tools/list, and the tool call itself. Each one is a separate request through AI Gateway, so each gets its own File Log entry, and all of them share one ai.mcp.mcp_session_id.
Both tools/list and the tool call carry an ai.mcp.audit array, because discovery and invocation are separate ACL decisions. The discovery entry has no primitive_name, since it covers the tool list as a whole rather than one tool.
Which MCP protocol revision appears in ai.mcp.protocol_version?
Whichever revision the client negotiates during initialize, not a value you configure on the AI MCP Server. The MCP Inspector CLI used in this tutorial negotiates 2025-11-25, so entries include an ai.mcp.mcp_session_id. A client that negotiates 2026-07-28 has no session concept, and AI Gateway omits mcp_session_id for that traffic. See MCP version support.