
This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.
You can turn an OpenAPI spec into an MCP server in about 15 lines of Python with FastMCP's from_openapi method. It reads the spec, makes one MCP tool per endpoint, and calls your API through an HTTP client you supply. Add route maps to keep only the endpoints you want, and you have a working prototype in minutes.
The catch: a generated server mirrors your API, and APIs are designed for programmers, not for AI models. FastMCP's own docs say generated servers are a starting point. This guide shows the generator, a real failure it exposed, and how to move from generated to curated tools.
Checked against: FastMCP's OpenAPI integration docs on 2 November 2026, with fastmcp 4.1.5 (the latest PyPI release, which installed mcp 2.3.0 and httpx2 2.16.0). The code ran locally on 2 November 2026 against the public Swagger Petstore demo API.
Key Takeaways
FastMCP.from_openapi(openapi_spec=..., client=...)builds a server where, by default, every endpoint becomes a tool.- Use route maps with a final catch-all
MCPType.EXCLUDErule to allow-list only the endpoints you trust. - Put API credentials on the HTTP client you pass in. FastMCP does not forward the
Authorizationheader from incoming MCP requests. - If the API's real responses do not match its spec, calls can fail validation. Fix the spec before you rely on the server.
- FastMCP's docs say models perform better with curated servers. Use generation to learn which tools you need, then hand-write the important ones.
Your options in November 2026
| Tool | Language | Status checked 2 Nov 2026 |
|---|---|---|
FastMCP from_openapi | Python | Documented, open source |
Speakeasy (speakeasy quickstart --mcp) | Generates TypeScript | Documented in Speakeasy's MCP guide |
| Stainless | Was TypeScript | Winding down hosted products after joining Anthropic, per its May 2026 announcement |
| Hand-written wrapper | Any | See building an MCP server for a REST API |
Speakeasy's guide says its generator creates "a TypeScript-based MCP server." It lets you tag tools with scopes through an x-speakeasy-mcp extension, and start the server with --scope read to mount only read tools. That is a useful pattern whichever tool you pick.
This tutorial uses FastMCP because it runs from a spec with no build step. Our MCP vs OpenAPI page explains why the two formats are complements, not rivals.
Prerequisites
- Python 3.10 or newer (PyPI lists
fastmcp4.1.5 as requiring 3.10+). - An OpenAPI 3 document for your API, as a URL or a file.
- API credentials with the narrowest rights you can get.
- Node.js 22.19+ if you want to test with the MCP Inspector.
Install
uv init openapi-mcp --python 3.12
cd openapi-mcp
uv add fastmcp
Note the HTTP library. FastMCP's OpenAPI page now uses httpx2, a separate package from httpx, and installing fastmcp 4.1.5 pulled in httpx2 2.16.0. Older tutorials import httpx. Follow the current docs.
FastMCP is a separate project from the official MCP Python SDK. Its upgrade guide says "FastMCP 4 builds on the MCP Python SDK v2." Our FastMCP vs the official Python SDK comparison covers the differences.
Minimal working server
Create server.py:
import httpx2
from fastmcp import FastMCP
from fastmcp.server.providers.openapi import MCPType, RouteMap
BASE_URL = "https://petstore3.swagger.io/api/v3"
client = httpx2.AsyncClient(base_url=BASE_URL, timeout=20)
spec = httpx2.get(f"{BASE_URL}/openapi.json").json()
mcp = FastMCP.from_openapi(
openapi_spec=spec,
client=client,
name="Petstore (read-only)",
route_maps=[
RouteMap(methods=["GET"], pattern=r"^/(pet|store)/.*", mcp_type=MCPType.TOOL),
RouteMap(mcp_type=MCPType.EXCLUDE), # everything else is dropped
],
)
if __name__ == "__main__":
mcp.run()
What each part does, per FastMCP's docs:
from_openapireads the spec and creates components. "By default, every endpoint becomes an MCP Tool."clientis the HTTP client that makes the real calls. Itsbase_urldecides where requests go.route_mapsare checked in order, and the first match wins. Each can match onmethods, a pathpatternortags, and setmcp_typetoTOOL,RESOURCE,RESOURCE_TEMPLATEorEXCLUDE.- The final catch-all
EXCLUDEturns the list into an allow-list. FastMCP's docs describe this pattern to stop default mappings from applying to the remaining routes. Here, only GET endpoints under/petand/storesurvive. Creating, updating and deleting pets are gone.
Check what the generator made
Create check.py using FastMCP's client:
import asyncio
from fastmcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as c:
for t in await c.list_tools():
print(t.name, "|", (t.description or "").splitlines()[0][:70])
r = await c.call_tool("getPetById", {"petId": 999999999}, raise_on_error=False)
print(r.is_error, r.content[0].text)
asyncio.run(main())
On 2 November 2026 the five generated tools were:
findPetsByStatus | Multiple status values can be provided with comma separated strings.
findPetsByTags | Multiple tags can be provided with comma separated strings. Use tag1,
getPetById | Returns a single pet.
getInventory | Returns a map of status codes to quantities.
getOrderById | For valid response try integer IDs with value <= 5 or > 10. Other valu
And the missing pet came back as a tool error:
True Error calling tool 'getPetById': HTTP error 404: Not Found - Pet not found
Look at the tool names and descriptions. They come straight from the spec's operationId and description fields. "For valid response try integer IDs with value <= 5 or > 10" is a note for human testers, and the model will read it too. Your spec's wording is now your prompt.
A real failure: the spec and the API disagree
Calling findPetsByStatus with {"status": "available"} failed on 2 November 2026. The API returned pets without a photoUrls field, which the spec marks as required. FastMCP had built an output schema from the spec, and the client rejected the result:
RuntimeError: Invalid structured content returned by tool findPetsByStatus: 'photoUrls' is a required property
The MCP tools specification says that when a tool declares an output schema, servers must provide structured results that conform to it. A spec that drifts from the real API therefore produces tool calls that fail, even though the HTTP call worked. A call to getInventory in the same run failed differently, with HTTP error 500 from the demo server.
The lesson for your own API: validate the spec against real responses before generating a server. Contract tests that compare responses to the spec pay for themselves here.
Add credentials
Put them on the client. FastMCP's docs show:
import os
client = httpx2.AsyncClient(
base_url="https://api.example.com",
headers={"Authorization": f"Bearer {os.environ['API_TOKEN']}"},
)
The docs also note that when the MCP server runs over HTTP, FastMCP does not forward credential headers such as Authorization from incoming MCP requests. That is the right default. The MCP security best practices say servers "MUST NOT accept any tokens that were not explicitly issued for the MCP server." See our page on token passthrough.
Test with the MCP Inspector
npx @modelcontextprotocol/inspector --cli \
uv --directory /ABSOLUTE/PATH/openapi-mcp run server.py \
--method tools/list --format json
Review every generated input schema. Look for vague parameter names, missing descriptions and enums that should exist but do not. Our Inspector lesson covers the flags.
From generated to curated
Use the generated server as a map, then improve it:
- Cut the list. Keep the 5 to 15 operations people ask for. Exclude admin, bulk and internal routes with
RouteMappatterns or tags. - Fix names and descriptions in the spec. Clear
operationIdvalues likegetOrderStatusbeatorders_get_v2. Write descriptions for the model, not for testers. - Hand-write the important tools. Combine two calls into one task, trim responses, and add limits. FastMCP lets you add normal
@mcp.toolfunctions to the same server. - Separate reads and writes. Expose writes only where a person approves them. Speakeasy's scope tags show one way to do this.
- Fix spec drift so output schemas match reality.
Connect it to a host
claude mcp add -e API_TOKEN=your-token --transport stdio petstore \
-- uv run --directory /ABSOLUTE/PATH/openapi-mcp server.py
For other clients, generate the JSON with our MCP config generator. Use absolute paths, and restart the host after config changes.
Deploy notes
The spec is fetched at startup in this example. In production, load it from a pinned file in your repo, so an API change cannot silently add new tools. Run the server remotely over Streamable HTTP only behind sign-in, and keep the API token in a secret store.
Pitfalls
- Exposing every endpoint. Without an allow-list,
DELETEand admin routes become tools. - Test-only notes in descriptions. The model reads them as instructions.
- Huge responses. Generated tools return whatever the API returns. Add a curated tool for list endpoints.
- Outdated imports. Older FastMCP examples import
httpxandfastmcp.server.openapi. The current page importshttpx2andfastmcp.server.providers.openapi. - Mixing up FastMCP and the SDK.
mcp.server.fastmcpwas the v1 SDK's built-in copy, and it is gone in SDK v2. The standalonefastmcppackage is different.
Next step
Learn tool design and error handling in the free Learn MCP course, especially designing tools an AI uses well. If you want an MCP server for your product's API that customers can connect to, eSEOspace MCP development can help you curate the tool set and set up sign-in.
More in the MCP Guide
Free tools: server.json Validator · MCP Error Code Lookup
Latest research: State of MCP: October 2026
Frequently asked questions
Does this work with Swagger 2.0 specs?
FastMCP's page refers to OpenAPI documents and notes that its provider requires FastMCP v3.0.0 or later. If you only have a Swagger 2.0 file, convert it to OpenAPI 3 first with a standard converter, then check the result.
Can I generate a TypeScript server instead?
Yes. Speakeasy's guide generates a TypeScript MCP server from an OpenAPI document with speakeasy quickstart --mcp. Check its current pricing and terms on Speakeasy's site before you commit.
Why are my tool names ugly?
They come from operationId. Rename operations in the spec, or replace the most-used ones with hand-written tools. Clear names help the model choose correctly.
Should GET endpoints be resources instead of tools?
FastMCP lets you map them to resources or resource templates with route maps. Tools are simpler for most hosts, because the model can call them on its own.
Is a generated server safe for production?
Treat it as a prototype. Review every tool, allow-list routes, use a scoped API key, and add sign-in before anyone else can reach it.











