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?
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:
/** 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 recordCreate 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:
"/drivers/{driver_id}/laps/{lap_id}":get:tags:- Lapssummary: Get a specific lap recorddescription: Retrieve a specific lap record for a driveroperationId: getLapparameters:- name: driver_idin: pathrequired: trueschema:type: stringdescription: The ID of the driverexamples:example1:value: 3fa85f64-5717-4562-b3fc-2c963f66afa6title: Driver Iddescription: The ID of the driver- name: lap_idin: pathrequired: trueschema:type: stringdescription: The ID of the lap recordexamples:example1:value: 3fa85f64-5717-4562-b3fc-2c963f66afa7title: Lap Iddescription: The ID of the lap recordresponses:'200':description: Lap record retrieved successfullycontent:application/json:schema:"$ref": "#/components/schemas/Lap"
/** 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 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.
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:
-
Endpoint purpose is unclear: Without explicit documentation about the purpose of each endpoint, the LLM can’t determine which tool to use and when.
-
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.
-
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.
-
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: getDriverFastestLapsummary: Get driver's fastest lapdescription: Retrieve a specific driver's fastest lap time at any trackx-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:
"/race-results/summary":get:operationId: getRaceSummarysummary: Get race summarydescription: Retrieve a summary of race results# Default MCP tool will use these values
"/race-results/summary":get:operationId: getRaceSummarysummary: Get race summarydescription: Retrieve a summary of race resultsx-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 driversx-speakeasy-mcp:scope: [read]# ...post:summary: Create a new driverx-speakeasy-mcp:scope: [write]# ..."/drivers/{driver_id}":delete:summary: Delete a driverx-speakeasy-mcp:scope: [write, destructive]# ...
When starting your MCP server, you can specify which scopes to include:
# Only enable read operationsnpx 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.
"/drivers/{driver_id}":get:tags:- Driverssummary: Get a specific driverdescription: |Retrieves complete profile information for a racing driver,including their name and full history of lap times acrossall tracks. Use this endpoint when you need comprehensivedriver details rather than just basic information.operationId: getDriverx-speakeasy-mcp:name: "get_driver_profile"description: |Fetch complete details about a specific racing driver including theirname, 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_idin: pathrequired: trueschema:type: stringdescription: The ID of the driver to retrievetitle: Driver Iddescription: The ID of the driver to retrieveresponses:"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.
parameters:- name: driver_idin: pathrequired: trueschema:type: stringdescription: |The UUID of the driver to retrieve. Must be a valid UUID v4 format.format: uuidexamples:hamiltonExample:summary: Lewis Hamilton's IDvalue: f1a52136-5717-4562-b3fc-2c963f66afa6verstappenExample:summary: Max Verstappen's IDvalue: c4d85b23-9fe2-4219-8a30-72ef172e327btitle: Driver Iddescription: The UUID of the driver to retrievex-speakeasy-mcp:description: |The unique identifier for the driver. You can find the IDs of current F1 driversby 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.
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: arrayitems:"$ref": "#/components/schemas/Lap"title: DriverLapsResponseexamples:multipleLaps:summary: Multiple lap recordsvalue:- id: "3fa85f64-5717-4562-b3fc-2c963f66afa7"lap_time: 85.4track: "Silverstone"- id: "3fa85f64-5717-4562-b3fc-2c963f66afa8"lap_time: 86.2track: "Monza"emptyLaps:summary: No lap recordsvalue: []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 recordedThe records are sorted from fastest to slowest lap time. If the driver existsbut 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.