Read-only AI agent API access: where the write boundary actually lives in your gateway
Read-only AI agent API access takes more than a GET-only token. See where the write boundary sits in an MCP gateway and how to enforce it layer by layer.
- ai-agents
- mcp
- access-control
- security
- least-privilege
Almost every enterprise AI agent rollout we see starts with the same promise to the security team: the agent will be read-only. It will look up orders, check account status, summarise tickets. It will not refund, cancel, close, or delete anything. Read-only AI agent API access is the condition under which the CISO signs off, and it is usually written into the change request in exactly those words.
Then the agent ships, and the question nobody answered surfaces in the first review: read-only according to what? The prompt says "do not modify data", which is an instruction, not a control. The MCP server exposes a tool list, which the agent can be talked out of respecting. The API key was issued by the backend team for an internal service, and it carries whatever scope that service had. Six weeks later the agent needs one write action, a card freeze or a ticket update, and the read-only boundary quietly becomes "mostly read-only".
The write boundary for an AI agent has to live somewhere the agent cannot negotiate with. That place is the gateway, and the useful question is which gateway checks actually enforce it, in what order, and what each one can and cannot see.
Why "read-only" usually isn't
The common approaches each enforce read-only at a layer the agent controls or the platform team cannot audit. Prompt-level restrictions fail the moment an injected instruction or a confused plan overrides them. Tool-level restrictions inside a standalone MCP server fail when that server holds a backend credential with write scope: whatever the server's tool list says, the credential can do more, and one bug or one added tool exposes it. Backend-level restrictions (a read-only database user, a read-only service account) work for one system, but they have to be rebuilt for every backend the agent touches, and nobody can answer "what can this agent write?" without reading five services' IAM config.
Traditional gateways don't close the gap on their own either. Kong, Apigee, and AWS API Gateway can all restrict HTTP methods on a route, but the AI agent typically doesn't come through those routes. It comes through an MCP server deployed next to the gateway, often by a different team, holding credentials the gateway never sees. Azure API Management and MuleSoft offer MCP exposure, but the agent's identity, the method restrictions, and the audit trail still have to be assembled across the gateway policy layer and the MCP layer. Every seam between them is a place where read-only stops being true.
There is also a protocol detail that catches teams out. MCP over Streamable HTTP sends every JSON-RPC message, including a pure read like tools/list, as an HTTP POST. If your write boundary is "block POST for this credential", you have just blocked the agent from talking MCP at all. If you allow POST so MCP works, you may have allowed every POST endpoint behind it. A real read-only design has to separate the method of the transport from the method of the call the agent is trying to make.
How Zerq enforces read-only AI agent API access
In Zerq, AI agents reach your APIs through Gateway MCP, which runs inside the same gateway binary as your REST traffic. There is no separate MCP server holding backend credentials. The agent connects with a client ID, a profile ID, and a credential, and when it calls the execute_endpoint tool, Zerq builds a normal gateway request and sends it back through the full gateway pipeline with the same X-Client-ID and X-Profile-ID, plus an X-Gateway-Source: mcp header so the traffic is identifiable in logs. Every check that applies to a REST app applies to the agent's call, a second time, against the method and path the agent actually requested.
That gives you five enforcement points on every call the agent makes through execute_endpoint, in the order the gateway applies them.
The request path, layer by layer
An engineer drawing this would put the AI agent on the left, sending POST /mcp to the gateway. Inside the gateway:
- Policy. The client's rate limit and quota, keyed per client ID, return
429once exceeded. - Identity. The client and profile must exist, be active, and the profile must belong to the client. The credential (token, JWT, OIDC, or mTLS, per the profile's auth type) must validate and must not be past its expiry. Failure returns
401. - Profile method check. The HTTP method of the request must be in the profile's
allowedMethods. Failure returns405withHTTP method not allowed. This check runs on the MCP transport request and again on the innerexecute_endpointrequest. - Client collection assignment. The request path must resolve to a collection the client has been assigned. Failure returns
403withAccess to this collection is not allowed for this client. - Proxy method check. The matched endpoint (proxy) must be published and must list the requested method in its own methods. Failure returns
405withMethod POST not allowed for this endpoint(or whichever method was attempted).
Only after all five does a request reach the backend on the right of the diagram. Discovery follows the same scoping: list_collections and list_endpoints only return active collections assigned to the client and only their published endpoints, and endpoint_details returns the endpoint's methods array. An agent scoped to read endpoints never learns that the write endpoints exist.
The design consequence is the important part. Because the MCP transport needs POST, the profile method check (layer 3) cannot by itself make an MCP agent read-only. For an MCP agent, the write boundary is layers 4 and 5: which collections the agent's client is assigned, and which methods each endpoint in those collections publishes. For a REST-only automation, a profile limited to GET is a complete read-only control on its own.
Step by step: a read identity and a separate write identity
Rate limits and collection assignments attach to the client, and a policy applies to all of a client's profiles combined. So the clean pattern is two clients per agent: one for reads, one for the narrow set of writes it is approved for. That keeps their blast radius and their budgets separate.
- Create the read collection. In the management UI, open Collections and create a collection such as
orders-readwith its own base path, for example/orders-read. Add only the endpoints the agent may read, each withGETas its only method, and publish them. Leave anything you are still reviewing in draft: draft endpoints are not returned bylist_endpoints. - Create the write collection. Create
orders-actionswith base path/orders-actionsand add only the approved write endpoints, for examplePOST /refunds/{orderId}. Nothing else goes in it. - Create the policies. Open Policies, click New Policy, and create a moderate read policy and a strict write policy. The write policy is where you cap damage from a runaway loop.
- Create the read client. Open Clients, create
ai-support-read, selectorders-readunder API Collections, select the read policy under Policy, and leave Developer Portal Access off. - Create the read client's profile. On the client's Profiles page, create a profile named
mcp-prod. Note that a new profile in the form starts with all seven methods selected under Allowed HTTP Methods; clear them and select onlyGETandPOST. MCP needsPOSTfor JSON-RPC messages andGETfor the server event stream. AddDELETEonly if your MCP client explicitly closes sessions. - Create the write client and profile. Repeat steps 4 and 5 with
ai-support-write, assigned onlyorders-actionsand the write policy, and its own credential. - Configure the agent. Give the agent runtime both MCP server entries. Most frameworks let you gate the second one behind an approval step or a separate sub-agent.
- Run the deny tests before go-live, as below, and confirm each
403and405appears in the request logs.
The two policies, as stored by the management API, look like this:
[
{
"name": "ai-support-read",
"description": "Read budget for the support agent",
"active": true,
"rate_limit_interval": "1m", // sliding window: 1m, 5m, or 1h
"rate_limit_requests": 120, // max requests per window, across all of the client's profiles
"quota_interval": "1d", // long-term cap: 1d, 7d, or 30d
"quota_requests": 20000 // max requests per quota period
},
{
"name": "ai-support-write",
"description": "Strict budget for approved write actions",
"active": true,
"rate_limit_interval": "1m",
"rate_limit_requests": 5, // a looping agent hits 429 after five writes
"quota_interval": "1d",
"quota_requests": 200 // hard daily ceiling on refunds the agent can issue
}
]
And the read client's profile:
{
"name": "mcp-prod",
"active": true,
"authType": "oidc", // none | token | jwt | oidc | mtls
"allowedMethods": ["GET", "POST"], // transport needs POST; reads are limited by the proxies
"requireCertificateBoundToken": false // set true to require RFC 8705 certificate-bound tokens
}
If you create profiles through the API or Management MCP and omit allowedMethods, the backend defaults the profile to GET only. That is a safe default for REST automation, and the first thing to change for an MCP agent.
What the agent sees when it tries to write
Here is the read agent trying a refund through its read identity. The agent sends a tools/call to /mcp:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "execute_endpoint",
"arguments": {
"method": "POST",
"path": "/orders-read/orders/ORD-9182",
"body": "{\"action\":\"refund\"}"
}
}
}
The outer request passes: it is a POST to /mcp and the profile allows POST. The inner request also passes the profile check and the collection check, because /orders-read is assigned. It stops at the proxy, which publishes only GET. The tool result comes back as an error the agent can read and reason about:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "{\"status_code\":405,\"body\":\"{\\\"error\\\":\\\"Method POST not allowed for this endpoint\\\"}\",\"headers\":{...}}"
}
],
"isError": true,
"cause": { "message": "Gateway returned 405" }
}
}
If the agent instead targets the write path, /orders-actions/refunds/ORD-9182, through its read identity, it gets a 403 at layer 4, because ai-support-read is not assigned that collection. The only route to a refund is the write identity, with its own credential, its own five-per-minute limit, and its own lines in the logs.
The audit trail for denied writes
Every inner execute_endpoint call is a gateway request, so it lands in the request logs in your own MongoDB like any other. The denied refund attempt above is stored with these fields:
{
"method": "POST", // the method the agent attempted
"path": "/orders-read/orders/ORD-9182", // the path the agent attempted
"status": 405, // denied at the proxy method check
"client_id": "66f1c2a9e4b0a1d2c3f40a11", // ai-support-read
"profile_id": "66f1c2b4e4b0a1d2c3f40a12", // mcp-prod
"collection": "66f1c1e0e4b0a1d2c3f40a01", // orders-read
"client_ip": "10.20.4.17",
"request_id": "b7d0c6e2-1f4a-4c55-9a7e-2f1e9d3c8a40",
"request_headers": { "X-Gateway-Source": "mcp" }, // abbreviated; marks MCP-originated traffic
"request_body": "{\"action\":\"refund\"}", // what the agent tried to send
"response_body": "{\"error\":\"Method POST not allowed for this endpoint\"}",
"latency": 3,
"created_at": "2026-10-08T09:14:22Z"
}
For a compliance team, this is the record that turns "the agent is read-only" from an assertion into evidence: who the agent was, which identity it used, what it attempted, and that the gateway refused. Requests denied at layer 3 also carry an error field such as HTTP method not allowed. Filter Logs by client to see an agent's full activity, and use the observability views to watch denied-write counts over time. A rising count is a signal worth investigating, whether the cause is a prompt change, an injection attempt, or a tool description the agent is misreading.
The boundary itself is also audited. Changing a profile's allowedMethods is a PUT to /api/v1/clients/:clientId/profiles/:id, which requires a modifier role and writes an audit log entry with resource_type: "profile", action: "update", the actor's email in actor_id, their roles in actor_type, and the request body in metadata.request_body. The same applies when a platform engineer makes the change through Management MCP, because those tools call the same management API under the same OIDC session and RBAC.
One gap to close: workflow-enabled endpoints
When an endpoint has the workflow builder enabled, the gateway hands the request to the workflow rather than the default proxy path, and the proxy method check in layer 5 does not run. The workflow receives the request method in $json['http_trigger'].request.method, so enforce the method there as the first node after the trigger:
{
"id": "method_guard",
"type": "condition_node",
"config": {
"conditions": [
{ "condition": "$json['http_trigger'].request.method === 'GET'", "output": "allowed" }
],
"default_output": "denied"
}
}
Wire denied to a response_node with "status": 405 and a body naming the refused method, and allowed to the rest of the flow. Test both branches before publishing. If you have workflow-enabled endpoints in an agent's read collection, add this guard to each one; it is the difference between a read collection and a collection that happens to contain read endpoints.
Pre-go-live checklist
Run these against the production identities, and keep the request log entries as evidence:
tools/listandlist_endpointsthrough the read identity return onlyGETendpoints from the read collection.execute_endpointwithPOSTto a read endpoint returns405from the proxy.execute_endpointto a write-collection path through the read identity returns403.- Every workflow-enabled endpoint in the read collection returns
405for a non-GETmethod. - Six rapid writes through the write identity produce a
429on the sixth. - A profile update produces an audit log entry naming the person who made it.
More on how these controls fit the wider platform is on the security page.
What this looks like in practice
A retail bank's digital team built a customer-service agent that answers questions about card status, recent transactions, and dispute progress. The pilot connected it to an MCP server the team wrote themselves, which called the core banking APIs with a service account borrowed from the mobile app backend. That account could freeze cards, update contact details, and initiate disputes. The agent's prompt said it would only read. The bank's operational risk team declined to approve production use, and they were right to: nothing outside the prompt prevented a write.
The team moved the agent onto Zerq Gateway MCP. They created a cards-read collection with six published GET endpoints and a cards-actions collection with exactly one endpoint, POST /cards/{cardId}/freeze, because freezing a card on a customer's request was the one write the business wanted. The read client got a 120-per-minute policy; the write client got three per minute and fifty per day. The agent framework routed any freeze request through a confirmation step before calling the write identity. Two endpoints in the read collection used workflows for response shaping, so the team added a method guard to each.
The risk review changed character. Instead of reading MCP server code, the reviewers ran the checklist themselves and read the denied attempts in the request logs, filtered by the read client's ID. Their approval named the two clients and the one write endpoint. When the business later asked for dispute initiation, the change was one new endpoint in cards-actions, reviewed on its own, with the profile and collection changes visible in the audit log. The same structure carries over to open banking and partner-facing agents; the banking use case covers the regulatory side.
The write boundary belongs in the gateway
Zerq makes read-only AI agent API access something you configure and prove rather than something you ask the agent to respect. Agents call your APIs through the same gateway pipeline as your apps, so the read boundary is enforced on every inner call, not just the MCP connection. Collection assignment and per-endpoint methods define what an agent can write, independent of what its prompt or tool list says. Separate read and write clients give writes their own credential, their own rate limit, and their own quota. Every denied write and every change to the boundary is recorded in your own MongoDB, ready for your auditors. The full set of capabilities is on the capabilities page, and the deployment model that keeps all of it inside your perimeter is on the architecture page.
Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.