Back to blog
Spec Design • 12 min read

Best Practices for Designing OpenAPI Specs for AI Agents

An MCP server is only as good as the specification behind it. These practices improve the tool names, descriptions, input schemas, safety signals, and response shapes that an AI agent actually sees.

OpenAPI was designed for humans writing clients. An MCP server uses the same document to build tools for a language model, and the model has no ability to read your documentation site, inspect the code, or ask a colleague what an endpoint does. It sees the tool name, the description, the argument schema, and the text your API returns.

That makes spec quality unusually direct: weak descriptions and vague schemas become weak tools, while explicit names, types, and constraints produce a server that agents can use reliably.

Start with what the generator actually uses

MCP Studio maps a small set of OpenAPI fields into the generated MCP server. Optimizing those fields has the largest effect on tool quality.

OpenAPI field Generated result
operationId Tool name. Must be unique, valid, and stable.
summary Tool title; some generators also use it as the description.
description The text the model uses for tool selection and usage. Falls back to operationId when absent.
parameter description Description attached to that tool argument.
requestBody schema Typed input schema when the body uses components and $ref.
schema property description Description attached to that input field.
first path segment Resource grouping in the generated source, after stripping a version prefix.
deprecated: true Operation is excluded from generated tools by default.
tags Not used for tool grouping. The path structure drives grouping.

1. Give every operation a stable, valid ID

The operationId becomes the tool name, so treat it as a public API rather than an internal label. Use a verb-first, snake_case identifier that describes the intent:

  • list_invoices, get_invoice, create_invoice
  • send_invoice, void_invoice, delete_invoice

IDs must be unique and match [A-Za-z0-9_-], up to 255 characters. Missing, duplicate, invalid, or deprecated operations are imported but disabled by default, so a careless naming scheme can silently remove tools from the server.

Avoid renaming later: tool names appear in prompts, client allowlists, logs, and team workflows. Choose an ID you can keep stable when the API evolves.

2. Write the description for tool selection

The model chooses tools by reading names and descriptions. A description such as "Gets invoices" is technically true and practically useless. State what the operation does, when to use it, what it returns, and whether it changes anything.

A good description answers four questions in one or two sentences:

  • What does this operation do, in concrete terms?
  • When should the model choose this tool instead of another one?
  • What are the key inputs, units, or prerequisites?
  • Is it read-only, destructive, or idempotent?

Gets invoices.

Returns a paginated list of invoices, newest first. Use when the user asks for recent invoices, invoices for a customer, or invoices in a specific status. Read-only; does not modify billing state.

Put the summary in summary and the detailed guidance in description. Some generated runtimes use the summary as the tool description, so both fields should be meaningful on their own.

3. Define request bodies in components and reference them

The most consequential schema advice is simple: put request and response models under components.schemas and reference them with $ref. A typed, reusable model produces a much better tool schema than an inline object, and some generators fall back to a free-form dictionary when an inline body has no component schema to resolve.

  • List every genuinely required property in the schema's required array.
  • Describe each property with units, formats, allowed values, and an example.
  • Avoid additionalProperties as a catch-all. If the shape is unknown, the model can only guess.
  • Avoid oneOf and anyOf for request bodies. Separate operations or a flat object with clearly optional fields are easier for a model to fill.
  • Flatten deep allOf inheritance chains; repeated layers obscure which fields are required.
  • Do not mark a required field as nullable. Make it required and give it a real value, or make it optional.

Body requiredness: some generators expose the top-level body argument as optional even when requestBody.required is true. State "the request body is required" in the description so the model does not omit it.

4. Keep tool inputs flat, typed, and explicit

Models handle a small number of well-named scalar arguments far better than deeply nested objects. Keep the input surface close to the user's intent and push incidental structure into the API contract.

  • Prefer string, integer, number, and boolean for path, query, and header parameters.
  • Avoid array-valued query parameters; item types are often lost. Use a comma-separated string with a documented format, or move the operation to a JSON body.
  • Keep path parameters short and opaque, and never embed slashes in them.
  • If an operation needs a dozen arguments, it probably represents several tools. Split it.
  • Do not ask the model for values that are fixed for the deployment. Parameters that are constant across every operation can sometimes be hoisted into server configuration, but do not rely on that for endpoint-specific constants.

Name arguments the way a user would describe them. customer_id is better than cust, and created_after is better than ts_from.

5. Use enums, and restate constraints in prose

String enums are one of the most effective constraints you can give a model. They become literal unions in the generated input schema, which sharply reduces invalid values. Use enums for status, type, currency, channel, and other closed sets.

Other JSON Schema constraints are not always preserved in the generated tool schema. Formats, patterns, minimums, maximums, and defaults may survive only as prose. Put the important ones in the parameter and property descriptions:

Instead of relying on: format: date-time alone

Write: "ISO 8601 timestamp in UTC, for example 2026-10-19T14:30:00Z."

For query parameters, list allowed values explicitly in the description even when the schema has an enum. The constraint may not be enforced by the generated argument schema, but the model will still follow a clear description.

6. Design for bounded, useful outputs

Tool output is read by the model, not rendered to a human. A response that returns every nested relation is expensive in context and makes the next decision harder. Design list and detail operations to return just enough information to act.

  • Paginate every list operation. Use a small default page size and a hard maximum, with cursor, offset, or page parameters.
  • Return compact summaries with stable IDs, then provide a get-by-ID tool for details.
  • Expose targeted filters such as status, date range, and customer ID instead of making the agent fetch everything and filter locally.
  • Avoid returning HTML, XML, binary data, or base64 blobs. Return a URL or a structured reference instead.
  • Return consistent JSON error bodies with a code, message, and retryable flag so the model can recover.

7. Make side effects impossible to miss

Agents are cautious about destructive actions only when the tool tells them to be. Encode risk in the operation name and description, not only in documentation.

  • Use explicit verbs: delete_invoice, refund_payment, archive_project.
  • Say when an action is irreversible: "Permanently deletes the record. Only call after explicit user confirmation."
  • Support idempotency keys for creates and payments, and describe the header and its expected format.
  • Avoid unbounded bulk operations. Require explicit IDs and a maximum batch size.
  • Do not hide multiple actions behind one action parameter. Split them into distinct operations with distinct descriptions.

8. Organize paths for predictable grouping

MCP Studio groups generated tools by the first path segment, after stripping a leading version prefix. The path structure therefore shapes the generated source, even though tool names come from operationId.

  • Prefer resource-first paths: /v1/invoices, /v1/customers/{customer_id}/invoices.
  • Avoid a generic prefix such as /api/v1/...; the first segment becomes the group name, so the tools end up grouped under "Api".
  • Keep resource names consistent and plural. A mix of /invoice and /customers makes the generated structure harder to follow.
  • One operation per intent. Do not overload a single endpoint with an action query parameter that selects between unrelated behaviors.

Tags remain useful for human documentation, but they do not drive tool grouping in the generated server.

9. Keep auth and formats out of the prompt

Credentials belong in securitySchemes, not in tool arguments. The generated server reads them from environment variables, so the model never has to supply or see a token.

  • Declare authentication at the operation or global level with a standard scheme.
  • Do not add an api_key parameter to every operation; the security scheme handles it consistently.
  • Use one stable server URL. Templated server variables are a poor fit for a generated tool's fixed base URL.
  • State formats and units in prose when they matter: currency codes, time zones, date formats, measurement units, and enumerations.
  • Remove deprecated operations from the spec rather than marking them deprecated; they are excluded from generation anyway.

Before and after

A single operation can demonstrate most of the advice above. The first version produces a vague tool with an unbounded input; the second produces a named, typed tool with a clear contract.

before.yaml
paths:
  /api/v1/doStuff:
    post:
      summary: Do stuff
      requestBody:
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
      responses:
        '200':
          description: OK
after.yaml
paths:
  /v1/invoices:
    post:
      operationId: create_invoice
      summary: Create an invoice
      description: Creates a draft invoice for an existing customer. Use after the user has confirmed the customer, amount, and currency. The request body is required. Returns the new invoice ID and status. Not idempotent unless an Idempotency-Key header is supplied.
      parameters:
        - name: Idempotency-Key
          in: header
          required: false
          description: Unique key that prevents duplicate invoices on retry. Use a UUID generated per user request.
          schema:
            type: string
      requestBody:
        required: true
        description: Invoice fields. The request body is required.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvoiceCreate'
      responses:
        '201':
          description: Invoice created

components:
  schemas:
    InvoiceCreate:
      type: object
      required: [customer_id, amount_cents, currency]
      properties:
        customer_id:
          type: string
          description: ID of an existing customer, prefixed cus_.
        amount_cents:
          type: integer
          description: Total in the smallest currency unit, for example 1999 for 19.99 EUR. Must be greater than zero.
        currency:
          type: string
          enum: [eur, usd, gbp]
          description: ISO 4217 currency code.
        memo:
          type: string
          description: Optional internal note, up to 500 characters.

A practical checklist

Before uploading a spec, confirm that:

  • Every operation has a unique operationId matching [A-Za-z0-9_-] and describing a single intent.
  • Every operation has a meaningful summary and a description covering when to use it, what it returns, and any side effects.
  • Every parameter, request body property, and component field has a description with units, formats, and allowed values where relevant.
  • Request bodies use components.schemas and $ref, with a correct required list.
  • There are no free-form objects, deep unions, or unnecessary nullable fields in inputs.
  • List operations are paginated and return compact summaries rather than full object graphs.
  • Destructive operations are named explicitly and warn about irreversibility.
  • Credentials use securitySchemes, not tool parameters.
  • Deprecated operations are removed, and paths are organized around stable resource names.

Upload the revised spec, review the enabled tool list, and test the generated descriptions with a real client. Tool quality is iterative: the first pass usually reveals one or two operations that need a sharper name or a smaller response.

Turn your spec into a server

Import an OpenAPI or Swagger document and generate a typed MCP server.