Production-ready MCP server generated from an OpenAPI specification, built with FastMCP (v3.2+).
Give an LLM access to any REST API by dropping in an OpenAPI spec.
- Prerequisites
- Quick Start
- Project Structure
- Configuration Reference
- Authentication
- OpenAPI Spec
- Route Maps
- Running the Server
- MCP Client Setup
- Development
- Docker
- Production Checklist
- Troubleshooting
- License
- Python 3.12+
- uv (recommended) or pip — install uv:
pip install uvor see astral.sh/uv - An OpenAPI 3.0+ specification file (JSON or YAML)
# 1. Clone or copy this template
cd fastmcp-openapi-template
# 2. Install dependencies
uv sync
# 3. Create your config
cp .env.example .env
# 4. Edit .env — at minimum set UPSTREAM_BASE_URL
# and point OPENAPI_SPEC_PATH to your spec
# 5. Drop your OpenAPI spec
cp /path/to/your-api.yaml openapi/spec.yaml
# 6. Run (stdio mode — for Claude Code, Cursor, etc.)
uv run python -m srcIf you don't have an OpenAPI spec handy, the included openapi/spec.yaml (a Petstore API) works for testing.
├── src/
│ ├── __init__.py
│ ├── __main__.py # Entry point: python -m src
│ ├── server.py # Core: FastMCP.from_openapi() + route maps
│ ├── config.py # Pydantic-settings — all config from env vars
│ ├── auth.py # MCP server JWT auth provider
│ ├── client.py # Upstream HTTP client factory (Bearer / API-key)
│ ├── lifespan.py # Startup/shutdown lifecycle hooks
│ └── middleware.py # Structured logging + error handling middleware
├── openapi/
│ └── spec.yaml # Drop your OpenAPI spec here (or anywhere)
├── tests/
│ ├── conftest.py # Shared fixtures (sample spec, mocks)
│ ├── test_config.py # Config validation tests
│ └── test_server.py # Server creation, auth, client tests
├── scripts/
│ └── dev.sh # Dev launcher helper
├── pyproject.toml # Dependencies + project metadata
├── Dockerfile # Multi-stage production build
├── docker-compose.yml # Local Docker dev setup
├── .env.example # Full config template with docs
└── README.md # This file
You don't need to touch most of these files. For basic use, you only need:
.env— your configurationopenapi/spec.yaml(or equivalent) — your OpenAPI spec
All settings are loaded from environment variables or a .env file. Copy .env.example to .env and fill in your values.
| Variable | Default | Description |
|---|---|---|
OPENAPI_SPEC_PATH |
openapi/spec.yaml |
Path to your OpenAPI spec (.json, .yaml, .yml) or a URL (https://...) |
| Variable | Default | Description |
|---|---|---|
UPSTREAM_BASE_URL |
(empty) | Base URL of the API described by your spec (e.g. https://api.example.com/v1) |
UPSTREAM_AUTH_TYPE |
none |
bearer, api-key, or none |
UPSTREAM_AUTH_TOKEN |
— | Bearer token (required when UPSTREAM_AUTH_TYPE=bearer) |
UPSTREAM_API_KEY_HEADER |
X-API-Key |
Header name for API key auth |
UPSTREAM_API_KEY_VALUE |
— | API key value (required when UPSTREAM_AUTH_TYPE=api-key) |
UPSTREAM_TIMEOUT |
30.0 |
Request timeout in seconds |
| Variable | Default | Description |
|---|---|---|
MCP_NAME |
OpenAPI MCP Server |
Name shown to MCP clients |
MCP_INSTRUCTIONS |
(empty) | Instructions shown to LLMs about how to use this server |
| Variable | Default | Description |
|---|---|---|
MCP_TRANSPORT |
stdio |
stdio (local) or http (remote) |
MCP_HOST |
127.0.0.1 |
Bind address (HTTP mode only) |
MCP_PORT |
8000 |
Bind port (HTTP mode only) |
| Variable | Default | Description |
|---|---|---|
MCP_AUTH_ENABLED |
false |
Enable JWT auth for the MCP server |
MCP_JWT_JWKS_URI |
— | JWKS endpoint URL (required if auth enabled) |
MCP_JWT_ISSUER |
— | Expected JWT issuer (required if auth enabled) |
MCP_JWT_AUDIENCE |
— | Expected JWT audience (required if auth enabled) |
| Variable | Default | Description |
|---|---|---|
MASK_ERROR_DETAILS |
true |
Hide internal errors from clients (set true in production) |
LOG_LEVEL |
INFO |
DEBUG, INFO, WARNING, or ERROR |
LOG_FORMAT |
json |
json (structured) or text (human-readable) |
This template handles auth at two independent levels.
Auth used when calling the REST API described by your OpenAPI spec. Configured on the httpx.AsyncClient that FastMCP.from_openapi() uses internally.
Bearer token — most common for APIs using OAuth 2.0 / JWT:
UPSTREAM_AUTH_TYPE=bearer
UPSTREAM_AUTH_TOKEN=eyJhbGciOi...
UPSTREAM_BASE_URL=https://api.example.comAPI key — common for SaaS APIs (e.g. Stripe, SendGrid):
UPSTREAM_AUTH_TYPE=api-key
UPSTREAM_API_KEY_HEADER=X-API-Key
UPSTREAM_API_KEY_VALUE=sk_live_abc123...
UPSTREAM_BASE_URL=https://api.example.comNo auth — for public APIs or local development:
UPSTREAM_AUTH_TYPE=none
UPSTREAM_BASE_URL=https://api.example.comThe system validates your config at startup — if you set UPSTREAM_AUTH_TYPE=bearer without a token, or api-key without a key value, the server will refuse to start with a clear error message.
Protects the MCP server itself when running over HTTP transport. This is separate from upstream API auth — it controls who can connect to your MCP server, not who can call the upstream API.
Currently supports JWT validation via JWTVerifier:
MCP_TRANSPORT=http
MCP_AUTH_ENABLED=true
MCP_JWT_JWKS_URI=https://auth.example.com/.well-known/jwks.json
MCP_JWT_ISSUER=https://auth.example.com
MCP_JWT_AUDIENCE=my-mcp-serverClients must present a valid JWT in the Authorization: Bearer <token> header. FastMCP validates the signature against the JWKS endpoint, checks expiration, and verifies the issuer and audience claims.
Auth is not applicable to stdio transport — stdio inherits security from the local process environment.
To extend auth (OAuth2, multi-provider), edit src/auth.py. FastMCP also supports OAuthProxy (GitHub, Google, Azure), RemoteAuthProvider (WorkOS, Descope), MultiAuth (hybrid JWT + OAuth), and custom providers.
- YAML:
.yaml,.ymlfiles - JSON:
.jsonfiles - URL: Any
https://URL serving a spec (content-type header or extension determines parser)
The default path is openapi/spec.yaml, but you can point OPENAPI_SPEC_PATH anywhere:
OPENAPI_SPEC_PATH=./my-api/openapi.json # local file
OPENAPI_SPEC_PATH=https://petstore3.swagger.io/api/v3/openapi.json # remote URLOnly one spec is loaded — this template does not scan directories or merge multiple specs.
- OpenAPI 3.0+ (3.0.x or 3.1.x)
- Each endpoint should have an
operationId— FastMCP uses it as the tool/resource name - A
serversblock orUPSTREAM_BASE_URLmust be set so generated tools know where to send requests
- The spec is loaded and validated (malformed YAML/JSON raises a clear error)
- Each path + method combination is mapped to an MCP component based on your route maps
- Request parameters, bodies, and response schemas are auto-generated from the spec
- The server is ready — tools and resources appear in your MCP client
Route maps control which OpenAPI endpoints become Tools (invocable), Resources (browsable data), or are excluded entirely.
| HTTP Method + Path Pattern | MCP Component |
|---|---|
GET /collection (single segment, no params) |
Resource — browsable, cachable |
GET /collection/{id} (contains path param) |
Resource Template — parameterized |
POST, PUT, PATCH, DELETE (any path) |
Tool — invocable by the LLM |
Example with the Petstore spec:
GET /pets→resource://listPetsGET /pets/{petId}→ resource templategetPetPOST /pets→ toolcreatePetDELETE /pets/{petId}→ tooldeletePet
Edit build_route_maps() in src/server.py:
from fastmcp.server.providers.openapi import RouteMap, MCPType
def build_route_maps():
return [
# Everything under /admin/ is excluded from MCP
RouteMap(
pattern=r"^/admin/.*",
mcp_type=MCPType.EXCLUDE,
),
# GET with path params → resource templates
RouteMap(
methods=["GET"],
pattern=r".*\{.*\}.*",
mcp_type=MCPType.RESOURCE_TEMPLATE,
),
# Everything else → tool (default)
RouteMap(
methods=["POST", "PUT", "PATCH", "DELETE", "GET"],
pattern=r".*",
mcp_type=MCPType.TOOL,
),
]Route maps are evaluated in order — the first match wins. Each RouteMap can filter by:
methods: list of HTTP methods (e.g.["GET", "POST"])pattern: regex against the URL pathtags: set of OpenAPI tags that must all be present on the operation
This is the default. MCP clients like Claude Code, Cursor, and VS Code launch the server as a subprocess and communicate via stdin/stdout.
# Using the dev script
./scripts/dev.sh
# Or directly
uv run python -m srcNo network port is exposed. The server inherits the security context of the user running it.
Set in .env:
MCP_TRANSPORT=stdioRun as a standalone HTTP server with the Streamable HTTP transport:
# Using the dev script
./scripts/dev.sh --http
# Or directly
MCP_TRANSPORT=http uv run python -m srcThe server listens on the configured host:port (default 127.0.0.1:8000). Enable MCP_AUTH_ENABLED=true to require JWT authentication.
Set in .env:
MCP_TRANSPORT=http
MCP_HOST=0.0.0.0 # Listen on all interfaces
MCP_PORT=8000Add to your Claude Code config (~/.claude/claude.json or project .mcp.json):
{
"mcpServers": {
"my-api": {
"command": "uv",
"args": ["run", "python", "-m", "src"],
"cwd": "/absolute/path/to/this/project"
}
}
}Add to .cursor/mcp.json or the VS Code MCP settings:
{
"mcpServers": {
"my-api": {
"command": "uv",
"args": ["run", "python", "-m", "src"],
"cwd": "/absolute/path/to/this/project"
}
}
}{
"mcpServers": {
"my-api": {
"url": "http://localhost:8000/mcp",
"headers": {
"Authorization": "Bearer YOUR_JWT_TOKEN"
}
}
}
}After connecting your MCP client, you should see your API's tools and resources listed. Ask the LLM: "What tools are available?" — it should list all the endpoints from your OpenAPI spec.
# Install main dependencies
uv sync
# Install with dev dependencies (pytest, etc.)
uv sync --group dev# Run all tests
uv run pytest -v
# Run specific test file
uv run pytest tests/test_config.py -v
# Run with coverage
uv pip install pytest-cov
uv run pytest --cov=src --cov-report=term-missing17 tests cover config validation, auth setup, client creation, route map building, and server creation. The test suite uses the sample Petstore spec in openapi/spec.yaml.
You can add hand-written tools alongside the auto-generated ones:
# In src/server.py, after create_server()
@mcp.tool
async def custom_business_logic(param: str, ctx: Context) -> str:
"""A custom tool not in the OpenAPI spec."""
return f"Processed: {param}"Custom tools appear alongside the OpenAPI-generated ones.
Edit build_route_maps() in src/server.py. See the Route Maps section for patterns. Common customizations:
- Exclude admin/internal endpoints
- Make read-only GETs into resources for better caching
- Tag specific endpoints for filtering in clients
- Use
route_map_fnfor dynamic per-endpoint decisions
Edit src/lifespan.py to add startup/shutdown logic:
@lifespan
async def app_lifespan(server):
# Startup: warm caches, check upstream health, load config
logger.info("warming_caches")
...
try:
yield {"my_resource": expensive_object}
finally:
# Shutdown: close connections, flush metrics
logger.info("cleaning_up")
...Tools access lifespan objects via ctx.lifespan_context["my_resource"].
docker build -t fastmcp-openapi-mcp .The Dockerfile is a multi-stage build:
- Builder stage: Installs dependencies via
uvinto a virtual environment - Runtime stage: Copies only the venv and source, runs as non-root
appuser
# 1. Configure .env with MCP_TRANSPORT=http
# 2. Start
docker-compose up -d
# 3. Check logs
docker-compose logs -f
# 4. Stop
docker-compose downThe compose file:
- Mounts
./openapi/as read-only (update specs without rebuilding) - Loads env vars from
.env - Sets
restart: unless-stopped - Includes a healthcheck on
GET /health
For production, don't rely on the .env file — pass env vars explicitly:
docker run -d \
--name mcp-server \
-p 8000:8000 \
-v "$(pwd)/openapi:/app/openapi:ro" \
-e MCP_TRANSPORT=http \
-e MCP_HOST=0.0.0.0 \
-e MCP_PORT=8000 \
-e UPSTREAM_BASE_URL=https://api.example.com \
-e UPSTREAM_AUTH_TYPE=bearer \
-e UPSTREAM_AUTH_TOKEN="$API_TOKEN" \
-e MCP_AUTH_ENABLED=true \
-e MCP_JWT_JWKS_URI="$JWKS_URI" \
-e MCP_JWT_ISSUER="$JWT_ISSUER" \
-e MCP_JWT_AUDIENCE="$JWT_AUDIENCE" \
-e MASK_ERROR_DETAILS=true \
fastmcp-openapi-mcpNever bake secrets into the image. Always inject them at runtime via env vars or a secrets manager.
Before deploying to production:
- Set
MASK_ERROR_DETAILS=true(hides stack traces from clients) - Set
LOG_FORMAT=json(structured logs for aggregation) - Set
LOG_LEVEL=INFOorWARNING(DEBUG leaks sensitive data) - Enable
MCP_AUTH_ENABLED=truewith valid JWKS config (HTTP mode) - Pin FastMCP:
fastmcp>=3.0.0,<4.0.0inpyproject.toml(breaking changes happen in minors) - Don't hardcode secrets in
.env— inject via environment at deploy time - Run as non-root user (Dockerfile already does this)
- Configure healthchecks in your orchestrator against
GET /health - Set resource limits on your container (CPU/memory)
- Use a reverse proxy (nginx, Caddy) with TLS if exposing HTTP publicly
- Review your route maps — exclude sensitive/internal endpoints
- Set
UPSTREAM_TIMEOUTappropriate to your API's SLA - Run
uv run pytest -vand verify all tests pass - Verify the server starts and tools appear in your MCP client
The file at OPENAPI_SPEC_PATH doesn't exist. Either:
- Drop your spec at the default location (
openapi/spec.yaml) - Set
OPENAPI_SPEC_PATHto the correct path - Use an absolute path
You enabled bearer auth but didn't provide a token. Set UPSTREAM_AUTH_TOKEN in .env.
You're using auth but didn't set a base URL. Add UPSTREAM_BASE_URL=https://your-api.com.
All three JWT fields (MCP_JWT_JWKS_URI, MCP_JWT_ISSUER, MCP_JWT_AUDIENCE) are required when auth is enabled.
Check:
- The spec loaded successfully (check startup logs for
openapi_spec_loaded) - Your route maps aren't excluding everything (set
LOG_LEVEL=DEBUGto see route mapping decisions) - The spec has valid
operationIdvalues on each endpoint - For STDIO mode: the client is correctly configured with the absolute path to this project
The FastMCP version installed doesn't match the expected RouteMap import path. The server falls back to default behavior (all endpoints → tools). Update FastMCP: uv sync --reinstall.
- Verify
UPSTREAM_BASE_URLis correct (include the path prefix, e.g./api/v1) - Verify auth credentials are valid
- Check network connectivity from the server's environment
- Set
LOG_LEVEL=DEBUGto see request/response details
Change MCP_PORT to a different port, or find and stop the process:
lsof -i :8000 # macOS/Linux
netstat -ano | findstr :8000 # WindowsMIT