August 6, 2026 · 8 min read
If your API has an OpenAPI spec, you're already most of the way to an MCP server. OpenAPI defines your endpoints, parameters, request shapes, and response types — exactly the information MCP tools need. The gap between them is smaller than it looks.
This guide covers three approaches: auto-generating MCP tools from your spec, writing a thin wrapper manually, and using a proxy layer. Each has different trade-offs depending on how much control you need over the tool descriptions that agents will use.
An MCP tool definition has three parts: a name, a description, and an input schema. An OpenAPI operation has the same three things: an operationId, a summary/description, and a requestBody or parameters schema.
The mapping looks like this:
# OpenAPI operation
paths:
/users/{id}:
get:
operationId: getUser
summary: Get a user by ID
parameters:
- name: id
in: path
required: true
schema:
type: string
# Maps directly to this MCP tool definition
{
"name": "getUser",
"description": "Get a user by ID",
"inputSchema": {
"type": "object",
"properties": {
"id": { "type": "string" }
},
"required": ["id"]
}
}The conversion is mechanical — which is why auto-generation works well as a starting point. The part that requires judgment is the tool descriptions: what you write in summary and description in OpenAPI becomes what the agent reads to decide which tool to call. Agent-facing descriptions need to be more explicit than human-facing ones.
openapi-mcp-generator is a community tool that reads an OpenAPI spec and outputs a working MCP server. It handles the schema conversion and HTTP plumbing, leaving you with a server you can run immediately and then tune.
npx openapi-mcp-generator generate \ --spec ./openapi.yaml \ --output ./mcp-server \ --language typescript
The output is a TypeScript MCP server with one tool per OpenAPI operation. Each tool makes the corresponding HTTP call and returns the response. You get a running server in minutes.
The limitation is tool descriptions. Auto-generated descriptions inherit your OpenAPI summaries verbatim, which are written for humans browsing API docs — not for agents deciding which tool to invoke. An agent reading "Get user" has no context about when to call this vs. "List users". You'll want to edit the generated descriptions before putting this in front of agents.
For APIs with a small number of endpoints, writing the MCP server by hand gives you full control over how the tools are described. You import the official MCP SDK, define your tools explicitly, and call your existing API client inside each handler.
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { z } from 'zod'
const server = new McpServer({
name: 'your-api',
version: '1.0.0',
})
// Write descriptions for agents, not humans.
// "Get a user by their unique ID. Use this when you have a specific
// user ID and need their profile. Do NOT use this to search by name
// — use searchUsers instead."
server.tool(
'getUser',
'Get a user by their unique ID. Use when you have a specific user ID and need their profile.',
{ id: z.string().describe('The unique user ID (UUID format)') },
async ({ id }) => {
const response = await fetch(`https://api.example.com/users/${id}`, {
headers: { Authorization: `Bearer ${process.env.API_KEY}` },
})
if (!response.ok) {
return { content: [{ type: 'text', text: `Error: ${response.status} ${response.statusText}` }], isError: true }
}
const user = await response.json()
return { content: [{ type: 'text', text: JSON.stringify(user, null, 2) }] }
}
)
const transport = new StdioServerTransport()
await server.connect(transport)This approach is more work upfront but produces better tool descriptions. It also lets you shape the inputs — you can combine multiple API calls into one MCP tool, omit parameters that agents shouldn't set, or add derived parameters that the API doesn't expose directly.
If your OpenAPI spec has dozens or hundreds of operations, exposing all of them as MCP tools is counterproductive. Agents perform better with fewer, well-scoped tools than with many overlapping ones. A proxy approach lets you selectively expose a curated subset.
// mcp-proxy.ts — expose only the tools agents actually need
import spec from './openapi.json'
const EXPOSED_OPERATIONS = new Set([
'getUser',
'listUsers',
'createOrder',
'getOrderStatus',
'searchProducts',
])
// Build MCP tools from the spec, filtered to what agents need
function buildTools(spec: OpenAPISpec) {
const tools = []
for (const [path, methods] of Object.entries(spec.paths)) {
for (const [method, operation] of Object.entries(methods)) {
if (!EXPOSED_OPERATIONS.has(operation.operationId)) continue
tools.push({
name: operation.operationId,
// Augment the spec description with agent-specific context
description: agentDescription(operation),
inputSchema: buildInputSchema(operation, path, method),
})
}
}
return tools
}
function agentDescription(op: Operation): string {
// Your OpenAPI description + "When to use:" guidance
return `${op.summary}. ${op['x-agent-description'] ?? ''}`.trim()
}This pattern also supports an x-agent-description extension field in your OpenAPI spec — you add agent-specific guidance there, and the proxy picks it up. This keeps your agent-facing documentation close to the API definition without duplicating the spec.
The single biggest quality factor in an OpenAPI-to-MCP conversion is the tool descriptions. Agents use descriptions to decide which tool to call — the logic in the handler doesn't matter if the agent picks the wrong tool.
Good MCP tool descriptions:
// ❌ Human-doc description (what the endpoint does) "Returns user information for the given user identifier." // ✅ Agent-facing description (when and how to use it) "Get a single user's profile by their UUID. Use when you already have a specific user ID from a previous tool call or user input. If you need to find a user by name or email, use searchUsers instead. Returns: id, name, email, role, createdAt, and subscription tier."
OpenAPI security schemes (API keys, Bearer tokens, OAuth) need to translate into how your MCP server makes authenticated HTTP calls. The pattern is: accept credentials at server startup (from environment variables or a config file), and embed them in every outgoing request.
Don't expose auth as tool parameters. Agents shouldn't be passing API keys around in tool calls — that's a prompt injection risk. Set the credentials once at server start and use them internally.
// Auth baked into the HTTP client at startup — not exposed as tool params
const apiClient = {
async get(path: string) {
return fetch(`https://api.example.com${path}`, {
headers: {
'Authorization': `Bearer ${process.env.API_KEY}`,
'Content-Type': 'application/json',
}
})
}
}
// Tool only receives business parameters — no auth
server.tool('getUser', '...', { id: z.string() }, async ({ id }) => {
const res = await apiClient.get(`/users/${id}`)
// ...
})Once your server works locally with stdio transport, switching to HTTP transport for remote deployment is a small change. The MCP 2026-07-28 spec makes this particularly clean — your HTTP MCP server is a stateless request handler that any client can call without initialization state.
// app/api/mcp/route.ts — Next.js App Router
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { buildTools } from '@/lib/mcp-tools'
const server = new McpServer({ name: 'your-api', version: '1.0.0' })
buildTools(server)
export async function POST(req: Request) {
const body = await req.json()
const result = await server.handleRequest(body)
return Response.json(result, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type, Mcp-Method, Mcp-Name',
}
})
}
export async function OPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Mcp-Method, Mcp-Name',
}
})
}Deploy this to Vercel, Railway, or any serverless platform and your OpenAPI-backed MCP server is accessible to any MCP client — Claude Desktop, Cursor, Claude Code, or AgentReady's `ask_site` when it needs to call your API as part of answering a question.
An MCP server built from your OpenAPI spec gives agents the ability to call your API. It doesn't give them knowledge about your product — what your API does, when to use which endpoints, what a typical workflow looks like, or what error responses mean.
That knowledge lives in your documentation. Indexing your docs site alongside your MCP server gives agents both: they can understand the domain before they start calling tools, and retrieve context mid-workflow when they need to look something up.
The pairing is: MCP tools for actions, indexed documentation for understanding. Together they cover everything an agent needs to work with your API effectively.
Index your API docs on AgentReady so AI agents can understand your product before they call your MCP tools. Takes 60 seconds, works on any public docs site.
Index your docs →