When you generate an MCP server from an OpenAPI spec, every operationId becomes a tool name, every summary becomes a tool description, and every parameter becomes a tool input. The AI agent sees only what's in the spec. If the spec is vague, the agent guesses. If the spec is precise, the agent acts correctly.
This post covers the concrete properties in your OpenAPI spec that affect MCP tool quality—and how to set them in the frameworks you're already using.
Why This Matters
MCP tool selection works by matching a user's intent to the best tool. Whether the selection uses an LLM reading tool definitions or semantic vector search, the same inputs matter:
- Tool name (from
operationId) — must be unique, descriptive, and action-oriented - Tool description (from
summary+description) — must explain what the tool does and when to use it - Parameter descriptions — must tell the agent what values are expected
- Response schema — helps the agent understand what it will get back
Get these right and your MCP server works out of the box. Get them wrong and the agent calls the wrong tools, passes bad parameters, and hallucinates missing context.
1. Use Descriptive operationId Values
The operationId becomes the MCP tool name. AI agents use it as a primary signal for tool selection. A good operation ID reads like a function name—verb first, noun second, specific enough to distinguish from siblings.
Bad:
paths:
/users:
get:
operationId: getUsers
/users/{id}:
get:
operationId: getUser
put:
operationId: updateUser
delete:
operationId: deleteUser
This is fine for a REST API, but getUser and getUsers are almost identical to an embedding model. The agent may pick the wrong one.
Good:
paths:
/users:
get:
operationId: list_all_users
/users/{id}:
get:
operationId: get_user_by_id
put:
operationId: update_user_profile
delete:
operationId: deactivate_user_account
Use underscores, not camelCase—most MCP clients normalize to snake_case anyway. Be specific: update_user_profile is better than updateUser because it tells the agent what gets updated.
Here's how to set the operationId in your framework:
from fastapi import FastAPI app = FastAPI() @app.get("/users", operation_id="list_all_users") async def list_users(): """List every user in the agent.""" ... @app.get("/users/{user_id}", operation_id="get_user_by_id") async def get_user(user_id: str): """Retrieve a single user by their unique ID.""" ...
2. Write Summary and Description for the AI, Not Just Humans
The summary field is short (one line) and becomes the primary tool description. The description field can be longer and should explain when to use this endpoint and what it returns. Think of it as writing a docstring for an AI coworker.
paths:
/invoices:
get:
operationId: search_invoices
summary: Search invoices by date range, status, or customer
description: >
Returns a paginated list of invoices matching the given filters.
Use this when the user asks about billing, payments, or outstanding
balances. Supports filtering by date_from, date_to, status
(draft, sent, paid, overdue), and customer_id. Returns at most
50 results per page.
Tips for writing MCP-friendly descriptions:
- Start the summary with a verb: "Search," "Create," "Delete," "List"
- In the description, mention user intents that should trigger this tool ("Use this when the user asks about...")
- Mention important constraints: pagination limits, required permissions, side effects
- Avoid internal jargon the model won't understand
Here's how to write rich descriptions in each framework:
from fastapi import FastAPI, Query app = FastAPI() @app.get( "/invoices", operation_id="search_invoices", summary="Search invoices by date range, status, or customer", description=( "Returns a paginated list of invoices matching the given filters. " "Use this when the user asks about billing, payments, or outstanding " "balances. Supports filtering by date_from, date_to, status " "(draft, sent, paid, overdue), and customer_id." ), ) async def search_invoices( status: str = Query( None, description="Filter by invoice status: draft, sent, paid, or overdue" ), customer_id: str = Query( None, description="Filter invoices to a specific customer by their ID" ), ): ...
3. Describe Every Parameter
Every parameter without a description is a guessing game for the AI. The agent will try to infer the meaning from the name alone—and names like q, type, or filter are ambiguous.
Bad:
parameters:
- name: q
in: query
schema:
type: string
- name: type
in: query
schema:
type: string
Good:
parameters:
- name: q
in: query
description: >
Full-text search query. Matches against invoice number,
customer name, and line item descriptions.
schema:
type: string
- name: type
in: query
description: >
Invoice type filter. Use "recurring" for subscription invoices,
"one-time" for single charges.
schema:
type: string
enum: [recurring, one-time]
Key rules:
- Always add
descriptionto every parameter, even if the name seems self-explanatory - Use
enumwhen there's a fixed set of values—the agent will use exactly these values instead of guessing - Specify
formatfor strings:date,date-time,email,uuid. This helps the agent format values correctly - Add
examplevalues—some MCP clients surface these as hints
4. Use enum Aggressively
Enums are the single most impactful thing you can add for MCP tool quality. When a parameter has an enum, the agent picks from the list instead of inventing values.
parameters:
- name: status
in: query
description: Filter by invoice status
schema:
type: string
enum:
- draft
- sent
- paid
- overdue
- name: sort_by
in: query
description: Field to sort results by
schema:
type: string
enum:
- created_at
- due_date
- amount
- customer_name
default: created_at
- name: sort_order
in: query
description: Sort direction
schema:
type: string
enum:
- asc
- desc
default: desc
Without enums, the agent might pass status=active (which doesn't exist) or sort=date (wrong field name). With enums, it can only pass valid values.
5. Define Response Schemas
Response schemas help the agent understand what data it will get back and how to present it to the user. Without a response schema, the agent has to parse the raw JSON and guess what each field means.
responses:
"200":
description: List of matching invoices
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
description: Unique invoice identifier
invoice_number:
type: string
description: Human-readable invoice number (e.g., INV-2026-0042)
status:
type: string
enum: [draft, sent, paid, overdue]
description: Current invoice status
total_amount:
type: number
description: Total amount in cents
currency:
type: string
description: ISO 4217 currency code
total_count:
type: integer
description: Total number of invoices matching the filters
has_more:
type: boolean
description: Whether there are more results beyond this page
The description on each response field matters. total_amount described as "Total amount in cents" prevents the agent from displaying $15000 instead of $150.00.
6. Tag and Group Your Operations
Tags control how tools are grouped. When you have dozens of endpoints, meaningful tags help both semantic search and LLM-based selection narrow down the relevant tools.
tags:
- name: invoices
description: Create, search, update, and manage invoices
- name: customers
description: Manage customer records, contacts, and preferences
- name: payments
description: Process payments, refunds, and view transaction history
paths:
/invoices:
get:
tags: [invoices]
operationId: search_invoices
...
/customers:
get:
tags: [customers]
operationId: list_customers
...
Some MCP server generators (including Emcy) use tags to namespace tools or provide filtering. Consistent, meaningful tags make this work well.
7. Mark Dangerous Operations Clearly
If an endpoint has side effects—deletes data, sends emails, charges money—say so in the description. MCP clients like Claude will be more cautious with tools that are clearly marked as destructive.
/invoices/{id}:
delete:
operationId: permanently_delete_invoice
summary: Permanently delete an invoice and all associated data
description: >
WARNING: This permanently deletes the invoice, its line items,
and all payment records. This action cannot be undone. Only use
this when the user explicitly confirms deletion. Consider using
archive_invoice instead for soft deletion.
The word "permanently" in the operation ID and "cannot be undone" in the description signal to the AI that it should confirm with the user before calling this tool.
Checklist
Before importing your OpenAPI spec into an MCP server generator, verify:
| Property | Check |
|---|---|
operationId | Unique, snake_case, verb-first, specific |
summary | One-line, starts with a verb, explains the action |
description | Mentions user intents, constraints, and side effects |
| Parameters | Every parameter has a description |
| Enums | Fixed-value parameters use enum |
| Response schema | Defined with field-level descriptions |
| Tags | Meaningful grouping with tag descriptions |
| Destructive ops | Clearly marked in name and description |
A Complete Example
Putting it all together—here's what a well-documented endpoint looks like:
openapi: "3.1.0"
info:
title: Acme Billing API
version: "1.0.0"
description: >
API for managing invoices, customers, and payments.
Used by the billing team and automated systems.
paths:
/invoices:
get:
operationId: search_invoices
tags: [invoices]
summary: Search invoices by date range, status, or customer
description: >
Returns a paginated list of invoices matching the given filters.
Use this when the user asks about billing, payments, or outstanding
balances. Returns at most 50 results per page. Results are sorted
by creation date descending by default.
parameters:
- name: status
in: query
description: "Filter by invoice status: draft, sent, paid, or overdue"
schema:
type: string
enum: [draft, sent, paid, overdue]
- name: customer_id
in: query
description: Filter invoices for a specific customer
schema:
type: string
format: uuid
- name: date_from
in: query
description: Start of date range filter (inclusive, ISO 8601)
schema:
type: string
format: date
example: "2026-01-01"
- name: date_to
in: query
description: End of date range filter (inclusive, ISO 8601)
schema:
type: string
format: date
- name: page
in: query
description: Page number for pagination (starts at 1)
schema:
type: integer
default: 1
minimum: 1
responses:
"200":
description: Paginated list of matching invoices
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: "#/components/schemas/Invoice"
total_count:
type: integer
description: Total matching invoices
has_more:
type: boolean
Every field has a purpose. The agent knows what the tool does, when to use it, what parameters to pass, and what it will get back.
Building MCP servers from OpenAPI specs? Try Emcy—we generate production-ready MCP servers with OAuth, tool selection, and telemetry built in.
