How to convert an OpenAPI spec into an MCP server (2026 guide)
Turn an OpenAPI 3.x spec into a working MCP server: how fields map to tools, codegen vs runtime vs hosted, a tested Petstore walkthrough, and pitfalls.
By Harsha VardhanUpdated
Short answer: To convert an OpenAPI spec into an MCP server, map each API operation to an MCP tool: the operationId becomes the tool name, parameters and request body become the input schema, and servers and security schemes become the base URL and credentials. A generator, runtime proxy or hosted platform does this for you.
Disclosure: I maintain openapi-mcp-generator, one of the tools in this guide. Every command and output below was run on 24 October 2026 with openapi-mcp-generator 5.0.1, FastMCP 4.1.3 and Node 22.20.0.
What MCP and OpenAPI are#
MCP (Model Context Protocol) is an open protocol that lets AI clients such as Claude Code, Cursor and VS Code discover and call tools exposed by a server. A tool has a name, a description, a JSON Schema for its input and, optionally, an output schema and behavior hints. The current specification version is 2026-07-28.
OpenAPI is the standard machine-readable description of an HTTP API: paths, methods, parameters, request and response schemas, servers and authentication. Many REST APIs already publish one, which is why it is the natural starting point for an MCP server. The latest published version is 3.2.1; most tooling today targets 3.0 and 3.1.
How OpenAPI fields map to MCP tools#
An OpenAPI operation already contains almost everything an MCP tool needs. The table shows the mapping and what openapi-mcp-generator 5.0.1 did with the Swagger Petstore.
| OpenAPI field | MCP tool field | What 5.0.1 produced on Petstore |
|---|---|---|
operationId |
name |
getPetById. With no operationId, a name is derived from method and path (GetItems in our test spec). |
summary |
title |
"Find pet by ID." (override with x-mcp-title) |
description (+ tags) |
description |
"Returns a single pet.\n(Tags: pet)". With neither field, only "Executes POST /items". |
path, query and header parameters + requestBody |
inputSchema |
petId as {"type":"integer","format":"int64"}; JSON bodies under a requestBody property |
lowest 2xx application/json object schema |
outputSchema |
The Pet schema, only with --output-schema |
servers |
base URL | Petstore declares a relative /api/v3, so we passed --base-url. API_BASE_URL overrides it at runtime. |
securitySchemes + security |
credentials | Environment variables such as API_KEY_API_KEY and OAUTH_TOKEN_PETSTORE_AUTH |
| HTTP method | annotations |
GET: readOnlyHint: true. PUT and DELETE: destructive and idempotent. POST: neither. All openWorldHint: true. Override with x-mcp-annotations. |
info.description |
server instructions |
Sent to clients on initialize |
The quality of the result depends on the spec. A model only sees names, descriptions and schemas, so a vague summary becomes a vague tool.
Three ways to do it#
There are three approaches. They differ in where the conversion happens and who runs the server.
| Approach | How it works | Examples | Strengths | Trade-offs |
|---|---|---|---|---|
| Code generation | Reads the spec once and writes a server project you own | openapi-mcp-generator (TypeScript), Speakeasy standalone MCP (TypeScript) | Reviewable code, no runtime spec parsing, edit anything, deploy anywhere | Regenerate when the spec changes; --force overwrites your edits |
| Runtime proxy | Reads the spec at startup and builds tools in memory | FastMCP from_openapi (Python), AWS Labs awslabs.openapi-mcp-server (Python) |
Always matches the current spec, very little code | Behavior is library code you configure rather than edit; tied to that runtime |
| Hosted platform | Upload the spec; the vendor runs the server | Speakeasy AI control plane (formerly Gram), Prefect Horizon (hosts FastMCP servers) | No infrastructure, managed OAuth, logs | Usage pricing or vendor terms, less control over code |
My recommendation: choose the runtime of your team first. Python teams should start with FastMCP. Teams that want a TypeScript project they can read, commit and deploy to Node or Cloudflare Workers should use a generator. Teams that want governance and hosting more than code should look at a platform. For measured results from six open-source tools on the same specs, see OpenAPI to MCP generators compared.
Step by step: Petstore with openapi-mcp-generator 5.0.1#
The generator needs Node 22.19 or later. It accepts OpenAPI 3.0.x and 3.1.x as JSON or YAML, from a path or URL.
1. Generate the project#
npx -y openapi-mcp-generator@5 \
-i https://petstore3.swagger.io/api/v3/openapi.json \
-o ./petstore -n petstore-mcp \
-b https://petstore3.swagger.io/api/v3Output (trimmed):
OpenAPI spec parsed successfully.
-> Created petstore/src/index.ts
-> Created petstore/package.json
-> Created petstore/.env.example
-> Created petstore/docs/oauth2-configuration.md
MCP server project 'petstore-mcp' successfully generated at: ./petstoreThe default transport is stdio. Add -t streamable-http, -t web (HTTP+SSE for older clients) or -t cloudflare-worker for other targets.
2. Build it#
cd petstore && npm install && npm run buildThe project depends on @modelcontextprotocol/server, @cfworker/json-schema and axios, and builds to build/index.js.
3. List the tools#
npx -y @modelcontextprotocol/inspector@latest --cli node build/index.js --method tools/listThe server exposes 19 tools, one per Petstore operation. Here is getPetById as a client sees it:
{
"name": "getPetById",
"title": "Find pet by ID.",
"description": "Returns a single pet.\n(Tags: pet)",
"inputSchema": {
"type": "object",
"properties": {
"petId": { "type": "integer", "format": "int64", "description": "ID of pet to return" }
},
"required": ["petId"]
},
"annotations": { "readOnlyHint": true, "openWorldHint": true }
}4. Call a tool, and watch it fail closed#
The Petstore spec says getPetById needs an API key or an OAuth token. With neither set, the server refuses to call the API:
Missing credentials for 'getPetById': set API_KEY_API_KEY, or OAUTH_TOKEN_PETSTORE_AUTH.
Required security: [api_key] OR [petstore_auth (scopes: write:pets, read:pets)].With the Petstore demo key it succeeds, and JSON object responses also arrive as structuredContent (output trimmed):
npx -y @modelcontextprotocol/inspector@latest --cli -e API_KEY_API_KEY=special-key \
node build/index.js --method tools/call --tool-name getPetById --tool-arg petId=10{
"content": [{ "type": "text", "text": "API Response (Status: 200):\n{ \"id\": 10, \"name\": \"Rufus\", ... }" }],
"structuredContent": { "id": 10, "name": "Rufus", "photoUrls": ["example.com"], "tags": [] }
}Invalid arguments are rejected before any HTTP request. Sending {"petId": "abc"} over JSON-RPC returned:
Invalid arguments for tool 'getPetById': /petId: Instance type "string" is invalid. Expected "integer".The Python alternative: FastMCP from_openapi#
If your team writes Python, FastMCP (PrefectHQ/fastmcp, 28,190 GitHub stars on 24 October 2026) is the obvious place to start. It builds tools from the spec at startup. This is the server we ran:
import httpx2
from fastmcp import FastMCP
spec = httpx2.get("https://petstore3.swagger.io/api/v3/openapi.json").json()
client = httpx2.AsyncClient(
base_url="https://petstore3.swagger.io/api/v3",
headers={"api_key": "special-key"},
)
mcp = FastMCP.from_openapi(openapi_spec=spec, client=client, name="Petstore")
if __name__ == "__main__":
mcp.run()With FastMCP 4.1.3 it produced the same 19 tools, declared an outputSchema for getPetById by default, and returned the pet as structured content. In our test it set no tool annotations, and the invalid petId: "abc" reached the API, which answered 400. FastMCP's own docs recommend auto-conversion for bootstrapping and note that curated servers perform better for complex APIs. See openapi-mcp-generator vs FastMCP.
Connect a client#
Claude Code (syntax from the Claude Code MCP docs; -- separates Claude's options from the server command):
claude mcp add --env API_KEY_API_KEY=your-key --transport stdio petstore -- node /absolute/path/to/petstore/build/index.js
claude mcp add --transport http petstore https://your-server.example.com/mcp --header "Authorization: Bearer $MCP_AUTH_TOKEN"Cursor reads mcpServers from .cursor/mcp.json in a project or ~/.cursor/mcp.json globally. Use command, args and env for stdio, or url and headers for a remote server.
VS Code now offers several configuration destinations with different formats (servers in VS Code's own format, mcpServers in portable files). The safest path is the MCP: Add Server command. Copy-paste examples for all three clients are in Connect clients.
Test it#
- stdio servers: use the MCP Inspector, either the browser UI (
npx @modelcontextprotocol/inspector node build/index.js, per the Inspector docs) or the--climode shown above for scripts and CI. - HTTP servers:
web,streamable-httpand Worker servers serve a browser playground at/. It lists tools, builds a form for each, and shows every JSON-RPC message. The same page is hosted at mcp.pidata.tech/playground for deployed servers. SetMCP_PLAYGROUND=offto disable it.
When we generated a Streamable HTTP server with --inbound-auth bearer, an initialize request without a token got 401 with WWW-Authenticate: Bearer realm="mcp", and the same request with the token succeeded.
Deploy to Cloudflare Workers#
-t cloudflare-worker writes a Workers project with wrangler.jsonc, .dev.vars.example and a README. Non-secret values such as API_BASE_URL go in wrangler.jsonc; secrets are set with npx wrangler secret put <NAME>; then npx wrangler deploy publishes it at https://<name>.<subdomain>.workers.dev/mcp. Worker secret names differ from the Node variables (PETSTORE_AUTH_TOKEN rather than OAUTH_TOKEN_PETSTORE_AUTH), and the generated README and .dev.vars.example list the ones your spec needs. A Worker fetches OAuth2 tokens only for client credentials flows; for other flows, set a token you already have. Full steps: Deploy to Cloudflare Workers.
Credentials and fail-closed behavior#
Generated servers read upstream credentials from environment variables: API keys, bearer tokens, basic auth (including an empty password), OAuth2 client credentials and password grants (tokens fetched automatically) and OpenID Connect tokens. The password grant reads OAUTH_USERNAME_<SCHEME> and OAUTH_PASSWORD_<SCHEME>. For authorization code and implicit flows, supply a token you already acquired in OAUTH_TOKEN_<SCHEME>; when set, that token is used for any flow instead of fetching one.
If an operation requires credentials that are not configured, the tool returns an error naming the variables and does not call the API. Set MCP_ALLOW_MISSING_CREDENTIALS=true to restore the 4.x behavior of sending the request anyway. Operations with security: [] stay anonymous.
Fail-closed follows the spec, not the live API. The public Petstore accepted unauthenticated reads in our runs even though its spec declares security. If your spec over-declares security, fix the spec or use the escape hatch deliberately.
For HTTP transports, protect the MCP endpoint itself with --inbound-auth bearer (MCP_AUTH_TOKEN), or forward selected client headers upstream with --header-passthrough. There is no built-in inbound OAuth 2.1 mode in 5.0.1; use bearer auth or a gateway.
Large APIs: curate before you ship#
Every tool definition costs context. For a large spec, expose a focused set:
x-mcp: true|falseat root, path or operation level (operation wins). In our test,x-mcp: falseon a path removed its operation.--default-include false, then opt operations in withx-mcp: true.--include-tags,--exclude-tags,--include-operations,--exclude-operations. Exclusions win.--include-tags pet --exclude-operations addPet,updatePet,deletePet,uploadFile,updatePetWithFormleft three read-only tools.- Tool names are capped at 64 characters by default (
--max-tool-name-length). A 79-character operationId becameFDA_get_info_on_cond_for_doct_cons_by_drug_name_and_othe_stuf, which works but reads badly. Shorter operationIds are better.
5.0.1 generates one tool per operation. It has no dynamic search or code-execution mode for very large APIs yet; Speakeasy (dynamic mode) and FastMCP (tool search transforms) already offer this.
Common pitfalls#
- Swagger 2.0 input. 5.0.1 refuses it:
Swagger 2.0 is not supported. Convert it to OpenAPI 3 first, for example: npx swagger2openapi swagger.json -o openapi.json. That conversion worked on the Petstore 2.0 spec. FastMCP 4.1.3 also rejected the 2.0 file. - OpenAPI 3.2. Both openapi-mcp-generator 5.0.1 and FastMCP 4.1.3 rejected a 3.2.0 document in our test. 3.1.0 worked in both.
- Relative servers. Petstore's
serversis/api/v3, so pass--base-urlor setAPI_BASE_URL. - Placeholder credentials in
.env.example. Every credential line andAPI_BASE_URLare commented out, so copying the file to.envunchanged keeps the spec's base URL and leaves secured tools failing closed. Uncomment only the variables you use and set real values: a placeholder left in place is sent as the credential. - Weak operationIds and descriptions. Without them you get
GetItemsandExecutes POST /items. Write summaries for the model. - Schema fidelity.
integerstaysinteger,oneOfstaysoneOf, and an OpenAPI 3.0 booleanexclusiveMinimum: truewithminimum: 0becameexclusiveMinimum: 0. Check how your clients handleoneOf. - Multipart and binary. A
multipart/form-databody and Petstore'sapplication/octet-streamupload both became a single stringrequestBody. Do not expect file uploads to work without editing the generated code. - Query arrays. Array query parameters follow the spec's
styleandexplode. With neither set, 5.0.1 repeats the key (tag=red&tag=blue), as OpenAPI specifies. If your API expects comma-separated values, declareexplode: falseon that parameter and regenerate; our test then sentids=1%2C2. - Pagination. Cursor and limit parameters are ordinary arguments; the server does not follow pages for the model.
- Array responses.
structuredContentis only added for JSON object responses.findPetsByStatusreturns an array, so it came back as text only.
FAQ#
Can I convert an OpenAPI spec to an MCP server without writing code? Yes. A generator writes the project for you, and a hosted platform runs it for you. You still need credentials, a base URL and a decent spec.
Does openapi-mcp-generator support Swagger 2.0?
No. Convert with npx swagger2openapi first. OpenAPI 3.0.x and 3.1.x are supported.
Should every API operation become a tool? Usually not. Expose the operations an agent needs, start with read-only ones, and add writes deliberately. Annotations help clients ask before destructive calls.
Codegen or runtime proxy? Codegen if you want code you can review, edit and deploy to Workers. Runtime proxy if you want tools to follow the spec automatically and you are happy in Python.
How do I keep the server in sync when the API changes?
With a generator, regenerate in CI and review the diff. Keep custom logic outside generated files, since --force overwrites them. Runtime proxies reread the spec at startup.
Which transport should I pick?
stdio for local clients, Streamable HTTP for remote clients, cloudflare-worker for serverless hosting, and web only for older SSE clients.
Is it safe to expose my API to an AI agent? Treat the model as an untrusted caller. Use least-privilege credentials, expose a curated tool set, protect HTTP endpoints with inbound auth, and keep destructive operations out unless you need them.
Sources (checked 24 October 2026)#
- openapi-mcp-generator 5.0.1 (published 24 October 2026): npm registry and https://github.com/harsha-iiiv/openapi-mcp-generator ; docs https://mcp.pidata.tech/docs
- MCP specification 2026-07-28: https://modelcontextprotocol.io/specification/latest
- OpenAPI Specification 3.2.1: https://spec.openapis.org/oas/latest.html
- MCP Inspector: https://modelcontextprotocol.io/docs/tools/inspector ; npm
@modelcontextprotocol/inspector2.12.0 - Swagger Petstore spec: https://petstore3.swagger.io/api/v3/openapi.json (OpenAPI 3.0.4, version 1.0.27)
- FastMCP OpenAPI integration: https://gofastmcp.com/integrations/openapi ; PyPI
fastmcp4.1.3 (20 October 2026) - Claude Code MCP docs: https://code.claude.com/docs/en/mcp
- Cursor MCP docs: https://cursor.com/docs/mcp
- VS Code MCP servers: https://code.visualstudio.com/docs/agent-customization/mcp-servers
- Speakeasy standalone MCP generation: https://www.speakeasy.com/docs/standalone-mcp/build-server
- AWS Labs OpenAPI MCP Server: https://github.com/awslabs/mcp/tree/main/src/openapi-mcp-server ; PyPI 1.1.6
- Prefect Horizon: https://gofastmcp.com/deployment/prefect-horizon
Last verified 24 October 2026.