MCP
Optimize your OpenAPI for MCP

Optimizing your OpenAPI document for MCP servers

If you’re building an AI-driven application, you’re probably exploring ways to connect it to your APIs.

Model Context Protocol (MCP) (opens in a new tab) servers are a powerful way to bridge AI models like Claude directly with your API endpoints. Using your OpenAPI document, Speakeasy automatically generates MCP servers alongside your SDK, allowing AI agents to refer to and use your API in a natural, conversational way.

In this guide, we’ll use an example mock racing lap API to demonstrate how to optimize an OpenAPI document for MCP server generation. The example API tracks drivers’ lap times across various racing circuits, allowing you to monitor and analyze driver performance. You can follow along with this guide using the example repository on GitHub (opens in a new tab).

We’ll look at how to improve your OpenAPI document – including optimizing descriptions, parameters, and naming conventions and using Speakeasy’s extensions – so that AI tools like Claude and Cursor can effortlessly interact with your API using the Speakeasy-generated MCP server.

What is MCP?

MCP is an open standard introduced by Anthropic (opens in a new tab) that enables AI agents to interact with APIs. Think of it as a translator for APIs, helping AI models interpret your API’s capabilities and interact with them in a consistent way. If SDKs are the bridge between your API and developers, MCP is the bridge between your API and AI agents.

Instead of building multiple custom integrations, developers can use a single MCP server to allow AI agents to interact with their API.

Where does OpenAPI fit in?

Since MCP is built around APIs, Speakeasy can generate a fully functional MCP server from your OpenAPI document. This means that your API’s capabilities are immediately accessible to LLMs that support MCP.

How does Speakeasy generate an MCP server using OpenAPI?

Info Icon

Note

At the time of writing, MCP server generation is only available in TypeScript SDKs. If you’re using a different language, you can create a new TypeScript SDK target and then generate the MCP server from there.

Speakeasy extracts every detail from your OpenAPI document: endpoints, methods, parameters, responses, and descriptions. It then generates a set of MCP tools that represent each API operation, complete with type-safe arguments and return values.

These tools are designed to be easily consumed by AI models, providing them with the necessary context to understand how to interact with your API.

You can then give your AI tool access to the MCP server, and the available tools and their parameters.

When Speakeasy generates your TypeScript SDK from an OpenAPI document, it also creates a corresponding MCP server. This MCP server is a lightweight wrapper around the generated SDK, specifically optimized for AI agent interactions.

The MCP server directory structure looks like this:

mcp-server/
├── build.mts
├── cli/
└── start/
├── command.ts
└── impl.ts
├── cli.ts
├── console-logger.ts
├── extensions.ts
├── mcp-server.ts
├── prompts.ts
├── resources.ts
├── scopes.ts
├── server.ts
├── shared.ts
├── tools/
├── driversCreateDriver.ts
├── driversDeleteDriver.ts
├── driversGetDriver.ts
├── driversListDrivers.ts
├── driversUpdateDriver.ts
├── lapsCreateLap.ts
├── lapsDeleteLap.ts
├── lapsGetLap.ts
├── lapsListLaps.ts
├── lapsUpdateLap.ts
└── rootGetWelcomeMessage.ts
└── tools.ts

Each file in the tools/ directory corresponds directly to an SDK method, providing AI agents with clearly defined and type-safe tools. These tools describe the API operation, specify required arguments, handle execution through the SDK, and format the responses appropriately.

Here’s an example tool generated from the OpenAPI document for creating a new lap record. Notice how it implements the lapsCreateLap function from the SDK:

lapsCreateLap.ts
/*
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
*/
import { lapsCreateLap } from "../../funcs/lapsCreateLap.js";
import * as operations from "../../models/operations/index.js";
import { formatResult, ToolDefinition } from "../tools.js";
const args = {
request: operations.CreateLapRequest$inboundSchema,
};
export const tool$lapsCreateLap: ToolDefinition<typeof args> = {
name: "laps-create-lap",
description: `Create a new lap record
Create a new lap record for a specific driver`,
args,
tool: async (client, args, ctx) => {
const [result, apiCall] = await lapsCreateLap(
client,
args.request,
{ fetchOptions: { signal: ctx.signal } },
).$inspect();
if (!result.ok) {
return {
content: [{ type: "text", text: result.error.message }],
isError: true,
};
}
const value = result.value;
return formatResult(value, apiCall);
},
};

Since each MCP tool mirrors your SDK’s Zod schemas and method signatures, AI models like Claude can interact intuitively and accurately with your API as if they were using a native SDK. This means that the AI can understand the expected input and output types, making it easier to generate valid API calls.

To learn more about how Speakeasy generates MCP servers, check out our MCP release article.

See it in action: Racing-lap counter API

For example, here’s a snippet of the OpenAPI document for the /drivers/{driver_id}/laps/{lap_id} endpoint, along with the generated MCP tool:

openapi.yaml
"/drivers/{driver_id}/laps/{lap_id}":
get:
tags:
- Laps
summary: Get a specific lap record
description: Retrieve a specific lap record for a driver
operationId: getLap
parameters:
- name: driver_id
in: path
required: true
schema:
type: string
description: The ID of the driver
examples:
example1:
value: 3fa85f64-5717-4562-b3fc-2c963f66afa6
title: Driver Id
description: The ID of the driver
- name: lap_id
in: path
required: true
schema:
type: string
description: The ID of the lap record
examples:
example1:
value: 3fa85f64-5717-4562-b3fc-2c963f66afa7
title: Lap Id
description: The ID of the lap record
responses:
'200':
description: Lap record retrieved successfully
content:
application/json:
schema:
"$ref": "#/components/schemas/Lap"
lapsGetLap.ts
/*
* Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.
*/
import { lapsGetLap } from "../../funcs/lapsGetLap.js";
import * as operations from "../../models/operations/index.js";
import { formatResult, ToolDefinition } from "../tools.js";
const args = {
request: operations.GetLapRequest$inboundSchema,
};
export const tool$lapsGetLap: ToolDefinition<typeof args> = {
name: "laps-get-lap",
description: `Get a specific lap record Retrieve a specific lap record for a driver`,
args,
tool: async (client, args, ctx) => {
const [result, apiCall] = await lapsGetLap(
client,
args.request,
{ fetchOptions: { signal: ctx.signal } },
).$inspect();
if (!result.ok) {
return {
content: [{ type: "text", text: result.error.message }],
isError: true,
};
}
const value = result.value;
return formatResult(value, apiCall);
},
};

This MCP tool is generated directly from the OpenAPI document, and it provides a clear description of what the endpoint does and detailed parameter information. An MCP-compatible LLM can use this information to understand how to call the API and what to expect in the response.

Natural language becomes API calls

Let’s take a look at how this works in practice. Say a user asks, “What’s Lewis Hamilton’s fastest lap at Silverstone?” using Claude desktop.

Claude fetching a lap time

Claude first uses the lapsListLaps tool to get a list of all laps for Lewis Hamilton. It then uses the lapsGetLap tool to get the fastest lap time at Silverstone.

This works well because the tools and questions are fairly straightforward, and Claude can figure out a step-by-step approach to get the answer. However, this is not always the case.

The problem: When LLMs hallucinate with poor OpenAPI documentation

Even the best AI models can confidently make things up when working with poorly documented APIs. This hallucination problem gets especially bad with MCP servers built from thin OpenAPI documents.

Suppose we ask a seemingly straightforward question like, “What was Lance Stroll’s fastest lap at Silverstone?”. Here’s what can happen with insufficient API documentation, especially problematic since Lance Stroll isn’t even in the database.

Claude generating a fake lap time

In this example, rather than returning an error or saying it doesn’t know, Claude uses the lapsPostLap tool to create a completely new (and fictional) lap record for Lance Stroll at Silverstone.

This happens because:

  1. Endpoint purpose is unclear: Without explicit documentation about the purpose of each endpoint, the LLM can’t determine which tool to use and when.

  2. Parameter documentation has gaps: Vague parameter descriptions may lead the LLM to misjudge expected formats and values, resulting in incorrect assumptions about a tool’s intended purpose.

  3. Context is missing: Without examples of real response patterns, the AI can’t infer what expected data looks like, resulting in incorrect assumptions about the API’s behavior.

  4. Error guidance is insufficient: Poor error documentation prevents the AI from recognizing when additional context or clarification is needed, leading to more hallucinations.

The good news? These problems can mostly be avoided by structuring your MCP tools well and following a few simple guidelines when writing your OpenAPI document.

How to optimize your OpenAPI document

Prompt engineering is most of the work when it comes to getting good results from AI models. Similarly, the clearer and more structured your OpenAPI documentation is, the more effectively AI tools will be able to understand and use your API.

Try these simple strategies to enhance your OpenAPI document and make it easier for AI models to interact with your API through MCP.

Optimize MCP tools with x-speakeasy-mcp

Speakeasy provides a dedicated OpenAPI extension specifically for customizing your MCP tools. The x-speakeasy-mcp extension gives you fine-grained control over how your API operations are presented to AI agents, allowing you to:

  • Override tool names and descriptions
  • Group related tools with scopes
  • Control which tools are available in different contexts

Here’s how to use this extension:

"/drivers/{driver_id}/laps/fastest":
get:
operationId: getDriverFastestLap
summary: Get driver's fastest lap
description: Retrieve a specific driver's fastest lap time at any track
x-speakeasy-mcp:
name: "driver_fastest_lap_tool"
description: "Find the fastest lap time a driver has ever recorded at any track"
scope: [read, performance-stats]

Customize tool names and descriptions

While a good operationId is great for SDK generation, MCP tools sometimes benefit from more descriptive names:

openapi.yaml
"/race-results/summary":
get:
operationId: getRaceSummary
summary: Get race summary
description: Retrieve a summary of race results
# Default MCP tool will use these values
openapi.yaml
"/race-results/summary":
get:
operationId: getRaceSummary
summary: Get race summary
description: Retrieve a summary of race results
x-speakeasy-mcp:
name: "get_race_finish_positions"
description: |
Get the final positions of all drivers in a specific race.
Returns each driver's finishing position, total time, and points earned.
Use this tool when you need to know who won a race or how drivers performed.

The improved MCP tool name and description provide clearer guidance to AI agents about what the endpoint does and when to use it while preserving your API’s original structure.

Organize tools with scopes

Scopes allow you to tag operations and control which tools are available in different contexts. This is particularly valuable when you want to:

  • Separate read and write operations
  • Protect destructive operations from accidental use
  • Group related operations by feature or sensitivity

Here’s how to implement scopes:

paths:
"/drivers":
get:
summary: List all drivers
x-speakeasy-mcp:
scope: [read]
# ...
post:
summary: Create a new driver
x-speakeasy-mcp:
scope: [write]
# ...
"/drivers/{driver_id}":
delete:
summary: Delete a driver
x-speakeasy-mcp:
scope: [write, destructive]
# ...

When starting your MCP server, you can specify which scopes to include:

# Only enable read operations
npx mcp start --scope read
# Enable both read and write (but not destructive)
npx mcp start --scope read --scope write

If you’re using the Claude desktop app, you can also specify scopes in the config:

{
"mcpServers": {
"RacingLapCounter": {
"command": "npx",
"args": [
"-y", "--package", "racing-lap-counter",
"--",
"mcp", "start",
"--scope", "read",
"--scope", "write"
]
}
}
}

Apply scopes across multiple operations

To apply scopes consistently across many operations, you can use the global x-speakeasy-mcp extension:

x-speakeasy-mcp:
scope-mapping:
- pattern: "^get|^list"
scope: [read]
- pattern: "^create|^update"
scope: [write]
- pattern: "^delete"
scope: [write, destructive]

This automatically applies scopes based on operation name patterns, saving you from manually tagging each endpoint.

Use descriptive operationId values

Every API operation should have a unique, descriptive operationId that clearly indicates its purpose:

"/drivers/{driver_id}/laps/fastest":
get:
operationId: getDriverFastestLap
# Rather than generic names like "getLapData" or "fetchInfo"

This naming convention helps AI models accurately map natural language requests like, “What’s Hamilton’s fastest lap?” to the correct tool, since the operationId is used as the default tool name.

Explain the purpose of each operation

When using MCP, it’s essential to provide clear descriptions for each operation. This is especially important for AI models, as they rely on these descriptions to understand the purpose and expected behavior of each endpoint.

openapi.yaml
"/drivers/{driver_id}":
get:
tags:
- Drivers
summary: Get a specific driver
description: |
Retrieves complete profile information for a racing driver,
including their name and full history of lap times across
all tracks. Use this endpoint when you need comprehensive
driver details rather than just basic information.
operationId: getDriver
x-speakeasy-mcp:
name: "get_driver_profile"
description: |
Fetch complete details about a specific racing driver including their
name, team, career statistics, and full history of lap times across all tracks.
Use this tool when you need comprehensive information about a driver.
scope: [read, driver-info]
parameters:
- name: driver_id
in: path
required: true
schema:
type: string
description: The ID of the driver to retrieve
title: Driver Id
description: The ID of the driver to retrieve
responses:
"200":
description: "Driver details retrieved successfully"
content:
application/json:
schema:
$ref: "#/components/schemas/Driver"

Add detailed parameter descriptions and examples

To make it easier for AI models to understand your MCP tools, provide detailed descriptions for each parameter. This includes specifying the expected format, constraints, and examples. This helps the LLM choose the right tools when it needs to follow a step-by-step approach to accomplish a task.

openapi.yaml
parameters:
- name: driver_id
in: path
required: true
schema:
type: string
description: |
The UUID of the driver to retrieve. Must be a valid UUID v4 format.
format: uuid
examples:
hamiltonExample:
summary: Lewis Hamilton's ID
value: f1a52136-5717-4562-b3fc-2c963f66afa6
verstappenExample:
summary: Max Verstappen's ID
value: c4d85b23-9fe2-4219-8a30-72ef172e327b
title: Driver Id
description: The UUID of the driver to retrieve
x-speakeasy-mcp:
description: |
The unique identifier for the driver. You can find the IDs of current F1 drivers
by first using the listDrivers tool. Common drivers include Lewis Hamilton,
Max Verstappen, and Charles Leclerc.

Add examples to responses

To improve the LLM’s understanding of your API’s responses, provide detailed descriptions, examples, and expected structures. This helps the LLM accurately interpret the data returned by your API.

openapi.yaml
responses:
'200':
description: |
Lap records retrieved successfully, sorted from fastest to slowest lap time.
Returns an empty array if the driver exists but has no recorded laps.
content:
application/json:
schema:
type: array
items:
"$ref": "#/components/schemas/Lap"
title: DriverLapsResponse
examples:
multipleLaps:
summary: Multiple lap records
value:
- id: "3fa85f64-5717-4562-b3fc-2c963f66afa7"
lap_time: 85.4
track: "Silverstone"
- id: "3fa85f64-5717-4562-b3fc-2c963f66afa8"
lap_time: 86.2
track: "Monza"
emptyLaps:
summary: No lap records
value: []
x-speakeasy-mcp:
name: "get_driver_lap_records"
description: |
Returns an array of lap records for the requested driver. Each record contains:
- id: A unique identifier for the lap record
- lap_time: Time in seconds (lower is better)
- track: Name of the circuit where the lap was recorded
The records are sorted from fastest to slowest lap time. If the driver exists
but hasn't completed any laps, this will return an empty array.

Final thoughts

While this post focuses on optimizing your OpenAPI document for MCP servers, these techniques are also good practice for writing well-structured, high-quality, comprehensive OpenAPI documents with great developer experience, whether or not you’re using Speakeasy or MCP.

For more information on how to improve your OpenAPI document, check out our OpenAPI best practices guide.