Skip to content

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 Updated

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#

bash
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/v3

Output (trimmed):

text
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: ./petstore

The 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#

bash
cd petstore && npm install && npm run build

The project depends on @modelcontextprotocol/server, @cfworker/json-schema and axios, and builds to build/index.js.

3. List the tools#

bash
npx -y @modelcontextprotocol/inspector@latest --cli node build/index.js --method tools/list

The server exposes 19 tools, one per Petstore operation. Here is getPetById as a client sees it:

json
{
  "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:

text
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):

bash
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
json
{
  "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:

text
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:

python
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):

bash
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 --cli mode shown above for scripts and CI.
  • HTTP servers: web, streamable-http and 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. Set MCP_PLAYGROUND=off to 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|false at root, path or operation level (operation wins). In our test, x-mcp: false on a path removed its operation.
  • --default-include false, then opt operations in with x-mcp: true.
  • --include-tags, --exclude-tags, --include-operations, --exclude-operations. Exclusions win. --include-tags pet --exclude-operations addPet,updatePet,deletePet,uploadFile,updatePetWithForm left three read-only tools.
  • Tool names are capped at 64 characters by default (--max-tool-name-length). A 79-character operationId became FDA_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 servers is /api/v3, so pass --base-url or set API_BASE_URL.
  • Placeholder credentials in .env.example. Every credential line and API_BASE_URL are commented out, so copying the file to .env unchanged 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 GetItems and Executes POST /items. Write summaries for the model.
  • Schema fidelity. integer stays integer, oneOf stays oneOf, and an OpenAPI 3.0 boolean exclusiveMinimum: true with minimum: 0 became exclusiveMinimum: 0. Check how your clients handle oneOf.
  • Multipart and binary. A multipart/form-data body and Petstore's application/octet-stream upload both became a single string requestBody. Do not expect file uploads to work without editing the generated code.
  • Query arrays. Array query parameters follow the spec's style and explode. With neither set, 5.0.1 repeats the key (tag=red&tag=blue), as OpenAPI specifies. If your API expects comma-separated values, declare explode: false on that parameter and regenerate; our test then sent ids=1%2C2.
  • Pagination. Cursor and limit parameters are ordinary arguments; the server does not follow pages for the model.
  • Array responses. structuredContent is only added for JSON object responses. findPetsByStatus returns 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)#

Last verified 24 October 2026.