Designing APIs for AI Agents When the Caller Cannot Read Your Docs
An API read by a model has different requirements from one read by a developer. Names become prompt, errors become instructions and idempotency stops being optional once the retry loop is autonomous.
An endpoint I wrote a few years ago got called forty-one times in ninety seconds and created forty-one near-identical rows. No human had ever done that to it. The caller was a model in a retry loop, and each retry was individually reasonable: the write was slow, the client timed out, the loop tried again with the same payload. Designing APIs for AI agents begins at that exact point, where a contract that was perfectly fine in front of a person becomes a liability in front of something that never gets tired and never reads the docs twice.
The difference has nothing to do with intelligence. It is a question of who is reading. A developer hits your 422 once, curses, patches the client and never sees that error again. A model treats the 422 as input. It will reshape its arguments and call you back inside a second, and it will keep doing that until your loop budget runs out. Everything else follows from this: naming, error text, enumerations, retry semantics, scope and how much of your surface you expose in the first place.
What follows is the pattern I keep arriving at, written from the server side. Some of it is now protocol. Most of it is still taste.
What the 2026 MCP revision actually settled
The Model Context Protocol shipped its 2026-07-28 revision on 28 July 2026, the largest change since the protocol was released in November 2024. Much of it reads like a concession to people who run load balancers. The initialize handshake is gone. So is the Mcp-Session-Id header. Every request now carries its own protocol version and client capabilities in _meta, and tools/list, resources/list and prompts/list no longer vary per connection. Servers that need continuity across calls are told to mint an explicit handle and accept it back as an ordinary tool argument.
Two smaller changes matter more for API design than the headline. Every result now carries a resultType of complete or input_required, because Multi Round-Trip Requests replaced server-initiated calls: a server needing more information returns inputRequests, and the client retries the original call with inputResponses under a new JSON-RPC id. And SSE stream resumability is gone. Last-Event-ID and event IDs were removed from the Streamable HTTP transport, so a broken response stream loses the in-flight request and the client is required to re-issue it as an entirely new one.
Read that last sentence as an operator rather than as a spec reader. The transport now guarantees you will sometimes be called twice for one intended action, and it has deliberately removed the mechanism that used to paper over it. Idempotency moved from good manners to a correctness requirement.
The rest is housekeeping with teeth. List results now require a ttlMs freshness hint and a cacheScope of public or private, servers should return tools in a deterministic order so client and prompt caches actually hit, error codes from -32020 to -32099 are reserved for the specification, and Roots, Sampling and Logging are all deprecated under a lifecycle policy with a twelve-month minimum window.

A developer hits your 422 once, curses, patches the client and never sees that error again. A model treats the 422 as input.
Designing APIs for AI agents starts with the names
MCP allows tool names of one to 128 characters using ASCII letters, digits, underscore, hyphen and dot, case sensitive, unique within a server. That is the whole constraint. Everything else about naming is on you, and it is load bearing in a way it never was for a human API, because the name is not a label attached to documentation. The name is the documentation. It sits in context beside the description and the parameter names, and it is re-read every time a decision gets made.
Anthropic's engineering guidance of 11 September 2025 says the same thing plainly: prefix tools by service and resource, so asana_projects_search rather than search, call a parameter user_id rather than user and return semantic identifiers instead of UUIDs wherever possible, because resolving an opaque key back into a human name reduces hallucination in retrieval.
Here is the part I did not expect. I read Chinese-language service docs before the English wrapper, which after eight years in Shenzhen is habit rather than virtue. On the manufacturing and automation side, internal method names still routinely arrive as pinyin initials: cxdd for 查询订单, query order. A Chinese engineer on that team parses cxdd instantly and correctly. A model sees four consonants and a coin flip. The highest-value change I made preparing one of those services for agent use was not a schema, an eval or a prompt. It was renaming about two dozen methods into English verbs with objects and writing one honest sentence per parameter. Nothing else that month moved tool selection accuracy nearly as far.
Make the invalid call unrepresentable
A free-form string parameter is an invitation to invent. Give a model status: string and across a thousand calls you will get open, OPEN, Open, active, pending_review and 待处理. Give it an enum of five values and you get five values. That is a specification problem wearing a model-quality costume, and enums are the cheapest fix in the discipline.
Provider tooling agrees. OpenAI's function calling guide recommends strict mode for production, which requires additionalProperties: false on every object and every field listed in required, with optional fields expressed as a nullable union rather than an absent key. It also suggests keeping fewer than twenty functions live at the start of a turn, a soft number that is still useful. MCP loosened inputSchema and outputSchema in this revision to accept any JSON Schema 2020-12 keywords with $ref resolution rules attached, so the constraint can be expressed properly instead of described in prose and hoped for. For a tool with no parameters the spec recommends an object with additionalProperties: false, because a bare empty object quietly accepts anything.
Pagination deserves its own paragraph, because agents fail at it in a specific and expensive way. A nextCursor is a machine-readable way of saying there is more. It does not say how much more, on what sort order or whether what you returned was filtered. An agent that cannot distinguish truncation from completeness will tell a user an account has three open tickets when it has ninety-four. Return the shape of what you withheld: how many matched, how many you sent, the sort key and an explicit partial flag. Anthropic goes further and suggests a response_format enum with concise and detailed modes; in their Slack example the concise form cost seventy-two tokens against two hundred and six, a sixty-five per cent cut, with the identifiers needed for the next call still intact.
Errors are instructions, not diagnostics
MCP splits failure into two channels and the split is the whole lesson. Structural problems, an unknown tool or a malformed request, come back as JSON-RPC protocol errors, and the spec is blunt that handing those to a model rarely produces recovery. Everything else, validation failures, business rule violations and upstream API problems, comes back inside a normal result with isError set to true, and clients are told to pass those to the model precisely so it can self-correct.
So the text inside isError is not a log line. It is the next prompt. The spec's own example is worth copying as a template: it names the field, states the constraint and supplies the fact the model was missing, saying the departure date must be in the future and then giving today's date. Three parts. What failed, what the rule is and what to do next.
Apply that everywhere. An expired state handle should say the handle expired, that baskets last twenty-four hours and that create_basket will mint a new one. The best-designed error in the whole stack is one almost nobody copies: an under-scoped call gets HTTP 403, error="insufficient_scope" and a scope parameter naming exactly what is missing, files:write. The error carries its own remedy. The spec even tells servers to emit every required scope in a single challenge, because drip-feeding them forces a fresh authorization round trip per attempt.
The inverse rule matters as much. Never return a bare identifier and expect inference. Error code 4012 means nothing to a model. It means nothing to most humans either, which is a hint about who your original API was really serving.
Idempotency and rate limits when the retry loop is autonomous
MCP ships four behavioural hints on a tool: readOnlyHint, destructiveHint, idempotentHint and openWorldHint. Look at the defaults, chosen by people who have watched this go wrong. readOnlyHint defaults to false. idempotentHint defaults to false. destructiveHint defaults to true. openWorldHint defaults to true. Absent an explicit claim from you, every tool is assumed to change things, reach the outside world and be unsafe to repeat. That is the correct prior.
Setting idempotentHint: true is a promise, so keep it. The convention everyone reaches for, the Idempotency-Key request header popularised by Stripe, never became a standard: draft-ietf-httpapi-idempotency-key-header reached revision 07 on 15 October 2025 and then expired without publication. The RateLimit and RateLimit-Policy header fields sit in the same position, still an Internet-Draft in the httpapi working group as of August 2026. Two of the most load-bearing conventions in agent-facing API design are folklore with good PR.
Which means implementing them yourself, twice. Once at the HTTP layer for the client. Once in the tool contract for the model, because the model never sees your response headers. A throttle has to appear as text in a tool result: rate limited, retry after thirty seconds, four calls remaining this window. An agent told only 429 will retry immediately, then again, and your rate limiter becomes a busy loop with a network hop in it. For write tools, accept a caller-supplied key as an ordinary parameter, store the result against it and return the stored result on replay. Twenty lines. It turns the transport's new promise into a no-op.
Scope per tool, and versioning for a reader who never reads changelogs
The authorization rules in this revision are stricter than most internal APIs manage. Servers must implement OAuth 2.0 Protected Resource Metadata, RFC 9728. Clients must send the RFC 8707 resource parameter on both authorization and token requests, naming the exact server the token is for. Servers must validate that a token was issued for them specifically and must not accept or transit anything else. Token passthrough, where a gateway forwards a downstream credential because it happened to be in the request, is prohibited rather than discouraged.
The design consequence is more interesting than the compliance one. The tool list may vary by the authorization presented on the request, since credentials are per-request input rather than connection state. Least privilege then covers two things at once: what a call may do and what the model can see at all. A read-only token should produce a tool list containing no delete_ tools, because a tool the model cannot see is a tool it cannot be talked into calling. Split the admin tool into three scoped ones.
Versioning is where this gets genuinely hard, because the consumer cannot read your changelog. It does not subscribe to your blog and it has no memory of last Tuesday's announcement. MCP's protocol-level answer is a feature lifecycle with Active, Deprecated and Removed states, a twelve-month minimum deprecation window and a public registry of deprecated features. That is the floor. At tool level, put the version in the name when the shape breaks, which the spec's own naming examples anticipate with DATA_EXPORT_v2, run both and rewrite the old tool's description to state its removal date and name its replacement. The description field is the only changelog your consumer will ever read.
The strongest objection: let the agent write code instead
The best argument against everything above comes from the people who wrote most of the guidance. On 4 November 2025 Anthropic published a case for presenting MCP servers as code APIs rather than direct tool calls, letting the model write TypeScript that imports your tools, filters inside the sandbox and returns only what matters. Their worked example, pulling a meeting transcript from one system and attaching it to a record in another, went from roughly 150,000 tokens to roughly 2,000, a 98.7 per cent reduction. Three weeks later came tool search with deferred loading, and programmatic tool calling that cut one research workload from 43,588 tokens to 27,297.
The objection writes itself. If the model writes code against a typed SDK, agent-specific API design is scaffolding for a transitional period, and the honest investment is a well-typed client library, good docstrings and a sandbox. Design for developers, because the agent now behaves like one.
Half of that is right, and it is the half about presentation. Code execution moves where the schema is read, out of the context window and into a file the agent lists and greps. It does not change what the schema has to say. Names still decide which function gets called, now read as identifiers in source rather than tool names in a prompt, which is no gain in legibility. Enumerations still constrain arguments, except an invalid value now fails as a runtime exception inside a sandbox rather than a validation error at your boundary, which is worse for recovery unless your exception text is written as an instruction.
The properties that actually prevent damage sit below the presentation layer entirely. Idempotency is a property of your write path. Audience-bound tokens are a property of your authorization server. Per-tool scope is a property of your permission model. A dropped stream re-issuing a request does not care whether the call came from a JSON tool block or a line of generated TypeScript. Code execution raises the ceiling on what an agent can accomplish against your API. It does not raise the floor under what happens when your API is wrong.
The part that stays unsolved
Go back to those four hints. readOnlyHint, destructiveHint, idempotentHint, openWorldHint. Every one is a claim the server makes about itself, and the specification instructs clients to treat all of them as untrusted unless the server is trusted. Nothing in the protocol verifies that a tool named search_documents only searches. Nothing checks that idempotentHint: true is backed by a dedupe table. The hints are self-reported and the defaults are pessimistic because self-reporting is exactly as reliable as you would expect.
So the safety of the whole design rests on a trust decision made outside the protocol, by whoever added the server. In practice that is an engineer pasting a URL into a config file, deciding in well under a minute, on the strength of a README. Registries index many thousands of these. The security guidance answers by telling clients to keep a human in the loop and show tool inputs before the call, which is sound and which also assumes someone is watching at three in the morning on call number four hundred.
We spent two years teaching models to read our APIs properly. The reading problem is close to solved. The believing problem has not been started, and every well-named, enum-constrained, idempotent tool we ship makes the badly-named lying one look exactly as trustworthy.
Tools referenced
OpenAI Agents SDK, reviewed here: OpenAI Agents SDK review.
LangGraph, reviewed here: LangGraph review.
AgentScope, reviewed here: AgentScope review.
Temporal, reviewed here: Temporal review.
Langfuse, reviewed here: Langfuse review.
Promptfoo, reviewed here: Promptfoo review.
Sources
Model Context Protocol, 2026-07-28 Key Changes (changelog): https://modelcontextprotocol.io/specification/2026-07-28/changelog
Model Context Protocol, Tools specification (2026-07-28): https://modelcontextprotocol.io/specification/2026-07-28/server/tools
Model Context Protocol, Authorization specification (2026-07-28): https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization
MCP Blog, The 2026-07-28 Specification: https://blog.modelcontextprotocol.io/posts/2026-07-28/
Anthropic Engineering, Writing effective tools for AI agents: https://www.anthropic.com/engineering/writing-tools-for-agents
Anthropic Engineering, Code execution with MCP: https://www.anthropic.com/engineering/code-execution-with-mcp
Anthropic Engineering, Advanced tool use on the Claude Developer Platform: https://www.anthropic.com/engineering/advanced-tool-use
IETF, The Idempotency-Key HTTP Header Field (draft-ietf-httpapi-idempotency-key-header): https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/
Frequently Asked Questions
What is the difference between an API designed for AI agents and one designed for developers?
A developer reads an error once, patches their client and never sees it again. An agent reads the error as input and immediately retries, so names, enumerations and error text are consumed on every single call rather than once at integration time. The practical consequences: tool and parameter names carry semantic weight because the model reads them to choose the call, enumerations replace free-form strings so an invalid value cannot be expressed, error messages must state what failed and what to do next, and idempotency becomes a correctness requirement because the retry loop is autonomous rather than human-driven.
How should an MCP server return errors so an AI agent can recover?
MCP separates two channels. Structural failures such as an unknown tool or a malformed request return as JSON-RPC protocol errors, which the specification notes rarely lead to successful recovery by a model. Execution failures such as validation errors, business rule violations and upstream API problems should be returned inside a normal tool result with isError set to true, and clients are expected to pass that text to the model. Write it as an instruction in three parts: which field failed, what the constraint is and what to do next, including any fact the model was missing such as the current date.
Do tools for AI agents need idempotency keys?
Yes, for anything that writes. MCP's 2026-07-28 revision removed SSE stream resumability from the Streamable HTTP transport, so a broken response stream loses the in-flight request and the client must re-issue it as a new request with a new id. Duplicate execution is now an expected condition rather than an edge case. The IETF Idempotency-Key header draft expired at revision 07 in October 2025 without becoming an RFC, so there is no standard to point at. Accept a caller-supplied key as an ordinary tool parameter, store the result against it and return the stored result on replay.
Read next
China's University Major Cuts Are AI Policy, and Nigeria Should Read the Fine Print
The latest analysis essay.
Working on something in this space?
If this analysis is close to a problem you're thinking about, say so. I read every message personally.
Start a conversation