← Blog

August 6, 2026 · 8 min read

How to Convert an OpenAPI Spec to an MCP Server

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.

Why OpenAPI maps cleanly to MCP

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.

Approach 1: Auto-generate with openapi-mcp-generator

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.

Approach 2: Write a thin wrapper manually

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.

Approach 3: OpenAPI proxy with tool filtering

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.

Writing good MCP tool descriptions

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:

  • State when to use this tool — not just what it does
  • Call out when NOT to use it — distinguish from similar tools
  • Name the identifiers required — "requires a user ID, not a username"
  • Describe the return shape — "returns a list of order objects with status, total, and line items"
// ❌ 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."

Handling authentication

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}`)
  // ...
})

Deploying as a remote MCP server

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.

The knowledge gap: what MCP tools don't give agents

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 →