Write API descriptions for a reader who never sees your guides

The description fields in an OpenAPI document were written for a developer who also had the getting-started guide, the tutorials and someone to ask. More and more often the reader is an agent, and the description is all it gets: tooling copies it into a tool definition, and the model decides from that text alone whether to call the operation, with what, and what to do next. So the description has to stand on its own. That isn’t a new genre of documentation for machines. It is reference documentation doing the job we always said it should, with the safety net taken away.

How the description reaches the model

OpenAPI gives each operation a summary, “a short summary of what the operation does”, and a description, “a verbose explanation of the operation behavior”, which may use CommonMark (Operation Object). Parameters get a description too. None of summary, description or operationId is required, so a document can be perfectly valid with all three missing.

An MCP tool is a name, a title, a description and an input schema (MCP tools specification). The quickest way to get an MCP server in front of an existing API is to generate one from the OpenAPI document. FastMCP, for example, converts every endpoint into a tool by default, incorporating the OpenAPI description into the tool’s description. Whatever was written in that field for a docs portal some years ago is now, word for word, part of a prompt.

The people who build models are clear about how much that text matters. Anthropic’s guidance on defining tools calls detailed descriptions “by far the most important factor in tool performance”, and asks for at least three or four sentences covering what the tool does, when it should and shouldn’t be used, what each parameter means, and its caveats. FastMCP’s own documentation says that models do significantly better with curated servers than with auto-converted ones, and recommends conversion for bootstrapping and prototyping, not for mirroring an API to models.

What the guides used to carry

Here is a thin operation description. I’ve invented it, so that no team is singled out:

/orders/{orderId}/cancel:
  post:
    operationId: cancelOrder
    summary: Cancel order
    description: Cancels an order.

A developer integrating this would learn the rest elsewhere: that only orders that haven’t been dispatched can be cancelled, that the refund happens later, that a shipped order goes through a separate returns flow, and what the 409 means. An agent learns none of it. Ask it to “send this parcel back” and cancelOrder is a plausible choice. It may even look as if it worked, if the error that comes back is vague enough.

The same operation, described for a reader with nothing else to go on:

    description: |
      Cancels an order that has not been dispatched, and refunds the
      original payment method. The refund completes asynchronously: read
      `refund.status` from GET /orders/{orderId} to see when it is done.

      Only orders with `status` of `pending` or `paid` can be cancelled.
      For an order that has shipped, use createReturn instead; this
      operation returns 409 with type `order-dispatched`.

      Calling it again on a cancelled order returns 200 and changes nothing.

That adds four things, and each of them used to live in a guide. When to use the operation and when not to, naming the alternative by its operationId. The preconditions, stated as values the agent can check. The side effects and their timing. And what repeating the call does, along with what the likely error means. None of it is specific to agents. A developer reading the second version would also be glad of it.

“Good documentation is good documentation”

The strongest objection is that there’s nothing here but the old advice: write proper reference docs. Keep one description, write it well, and don’t invent an agent dialect that drifts away from the human one.

I agree with most of that, and I’d resist any proposal for a separate “agent description” written by a different team. What agents change is the cost of getting it wrong, not the kind of writing. A developer who meets a thin description asks a colleague, once, while building the integration, and the answer ends up in code. A model reads the description on every task, at runtime, and acts on its own reading of it each time. A human choosing an operation has usually already picked it from the guide. A model is choosing among every tool it has been given at once, so the text that tells cancelOrder apart from createReturn carries more weight than it ever did in a portal.

There’s a second change, and it is less comfortable. Once descriptions feed a model, they are instructions, and a channel for them. I wrote about tool poisoning and changed descriptions as a versioning problem, and about text that reaches a model through tools as a security one. The upshot is the same: an edit to a description changes how agents behave, so it deserves the review an edit to code gets.

Where the words should live

The OpenAPI Initiative’s Overlay Specification, published in October 2024, offers another route. An overlay is a list of actions, each with a JSONPath target and an update or remove, that can be applied repeatably to an OpenAPI description (Action Object). You could keep your agent-facing descriptions in an overlay and apply it when you generate the MCP server.

If you own the API, I wouldn’t. An overlay of descriptions is a second copy of the documentation, and second copies drift. Put the words in the source, where the API’s owner reviews them. Overlays earn their place when you don’t own the source: a team wrapping a vendor’s API as tools can add the missing “when not to use this” without forking the vendor’s document, and reapply it when a new version arrives.

Some guidance doesn’t belong in any single operation. “Cancel, then poll until the refund completes” is a sequence, and Arazzo exists to describe “sequences of calls and their dependencies” across API descriptions. I don’t know how many agent hosts read Arazzo documents directly today, so treat this as my own suggestion: if three operation descriptions each explain part of one workflow, write the workflow down once, as a workflow, and let the tool layer build from it.

What to do this week

A sample lint rule

Here is a ruleset for OpenLint, the community fork of Spectral. It uses OpenLint’s built-in OpenAPI rules, which already warn on an operation with no description, and adds two rules of its own:

# .openlint.yaml
extends: openlint:openapi
functions: [describesMoreThanSummary]
rules:
  parameter-description:
    description: Every parameter needs a description.
    message: Parameter has no description.
    severity: error
    given:
      - $.paths[*].parameters[*]
      - $.paths[*][get,put,post,delete,options,head,patch,trace].parameters[*]
    then:
      field: description
      function: truthy
  operation-description-adds-to-summary:
    description: An operation's description must say more than its summary.
    message: "{{error}}"
    severity: error
    given: $.paths[*][get,put,post,delete,options,head,patch,trace]
    then:
      function: describesMoreThanSummary

None of OpenLint’s core functions compares one field with another, so the second rule needs a custom function. This one counts the words in the description that aren’t already in the summary:

// functions/describesMoreThanSummary.js
const words = (s) => (s ?? '').toLowerCase().match(/[a-z0-9]+/g) ?? [];

export default (operation) => {
  const summary = new Set(words(operation.summary));
  const extra = words(operation.description).filter((w) => !summary.has(w));
  if (extra.length < 10) {
    return [{
      message: 'The description only restates the summary. Say when to use this ' +
        'operation, when not to, and what it changes.',
    }];
  }
};

I ran it with OpenLint 1.1.3 (openlint lint openapi.yaml --ruleset .openlint.yaml) against both versions of cancelOrder above. The thin one fails the second rule and the rewritten one passes it. Declare the orderId path parameter without a description and the first rule fails too. The threshold of ten new words is arbitrary, and a description can clear it while still saying nothing useful, which is why the reviewer stays.

The question I haven’t settled is length. Three or four sentences per operation is a floor, but an agent given eighty tools pays for every word of every description in its context before it does any work. Somewhere a description stops helping the model choose and starts crowding out the task. I’d like to know where that line is, and whether it should be the API provider or the agent host who decides how much of the description to send.

Comments

Comments need a GitHub account and are stored as issues on theRookery repository.