Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FastMCP OpenAPI MCP Server

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.


Table of Contents


Prerequisites

  • Python 3.12+
  • uv (recommended) or pip — install uv: pip install uv or see astral.sh/uv
  • An OpenAPI 3.0+ specification file (JSON or YAML)

Quick Start

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

If you don't have an OpenAPI spec handy, the included openapi/spec.yaml (a Petstore API) works for testing.


Project Structure

├── 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 configuration
  • openapi/spec.yaml (or equivalent) — your OpenAPI spec

Configuration Reference

All settings are loaded from environment variables or a .env file. Copy .env.example to .env and fill in your values.

OpenAPI Spec

Variable Default Description
OPENAPI_SPEC_PATH openapi/spec.yaml Path to your OpenAPI spec (.json, .yaml, .yml) or a URL (https://...)

Upstream API

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

MCP Server Identity

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

MCP Transport

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)

MCP Server Auth (HTTP transport 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)

Error Handling & Logging

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)

Authentication

This template handles auth at two independent levels.

Upstream API Auth

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.com

API 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.com

No auth — for public APIs or local development:

UPSTREAM_AUTH_TYPE=none
UPSTREAM_BASE_URL=https://api.example.com

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

MCP Server Auth

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-server

Clients 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.


OpenAPI Spec

Supported formats

  • YAML: .yaml, .yml files
  • JSON: .json files
  • URL: Any https:// URL serving a spec (content-type header or extension determines parser)

Where to put it

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 URL

Only one spec is loaded — this template does not scan directories or merge multiple specs.

Spec requirements

  • 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 servers block or UPSTREAM_BASE_URL must be set so generated tools know where to send requests

What happens at startup

  1. The spec is loaded and validated (malformed YAML/JSON raises a clear error)
  2. Each path + method combination is mapped to an MCP component based on your route maps
  3. Request parameters, bodies, and response schemas are auto-generated from the spec
  4. The server is ready — tools and resources appear in your MCP client

Route Maps

Route maps control which OpenAPI endpoints become Tools (invocable), Resources (browsable data), or are excluded entirely.

Default behavior

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 /petsresource://listPets
  • GET /pets/{petId} → resource template getPet
  • POST /pets → tool createPet
  • DELETE /pets/{petId} → tool deletePet

Customizing route maps

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 path
  • tags: set of OpenAPI tags that must all be present on the operation

Running the Server

STDIO mode (local MCP clients)

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 src

No network port is exposed. The server inherits the security context of the user running it.

Set in .env:

MCP_TRANSPORT=stdio

HTTP mode (remote / network)

Run 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 src

The 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=8000

MCP Client Setup

Claude Code

Add 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"
    }
  }
}

Cursor / VS Code

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"
    }
  }
}

Generic HTTP Client

{
  "mcpServers": {
    "my-api": {
      "url": "http://localhost:8000/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_JWT_TOKEN"
      }
    }
  }
}

Verifying it works

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.


Development

Installing dependencies

# Install main dependencies
uv sync

# Install with dev dependencies (pytest, etc.)
uv sync --group dev

Running tests

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

17 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.

Adding custom tools

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.

Customizing route maps

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_fn for dynamic per-endpoint decisions

Adding custom lifespan logic

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

Building the image

docker build -t fastmcp-openapi-mcp .

The Dockerfile is a multi-stage build:

  1. Builder stage: Installs dependencies via uv into a virtual environment
  2. Runtime stage: Copies only the venv and source, runs as non-root app user

Running with Docker Compose

# 1. Configure .env with MCP_TRANSPORT=http
# 2. Start
docker-compose up -d

# 3. Check logs
docker-compose logs -f

# 4. Stop
docker-compose down

The 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

Production Docker

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-mcp

Never bake secrets into the image. Always inject them at runtime via env vars or a secrets manager.


Production Checklist

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=INFO or WARNING (DEBUG leaks sensitive data)
  • Enable MCP_AUTH_ENABLED=true with valid JWKS config (HTTP mode)
  • Pin FastMCP: fastmcp>=3.0.0,<4.0.0 in pyproject.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_TIMEOUT appropriate to your API's SLA
  • Run uv run pytest -v and verify all tests pass
  • Verify the server starts and tools appear in your MCP client

Troubleshooting

"OpenAPI spec not found"

The file at OPENAPI_SPEC_PATH doesn't exist. Either:

  • Drop your spec at the default location (openapi/spec.yaml)
  • Set OPENAPI_SPEC_PATH to the correct path
  • Use an absolute path

"UPSTREAM_AUTH_TYPE is 'bearer' but UPSTREAM_AUTH_TOKEN is not set"

You enabled bearer auth but didn't provide a token. Set UPSTREAM_AUTH_TOKEN in .env.

"UPSTREAM_BASE_URL is required when UPSTREAM_AUTH_TYPE is..."

You're using auth but didn't set a base URL. Add UPSTREAM_BASE_URL=https://your-api.com.

"MCP_AUTH_ENABLED is true but required fields are missing"

All three JWT fields (MCP_JWT_JWKS_URI, MCP_JWT_ISSUER, MCP_JWT_AUDIENCE) are required when auth is enabled.

Tools don't appear in my MCP client

Check:

  1. The spec loaded successfully (check startup logs for openapi_spec_loaded)
  2. Your route maps aren't excluding everything (set LOG_LEVEL=DEBUG to see route mapping decisions)
  3. The spec has valid operationId values on each endpoint
  4. For STDIO mode: the client is correctly configured with the absolute path to this project

"route_map_import_failed_using_defaults" warning

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.

Upstream API calls fail

  • Verify UPSTREAM_BASE_URL is 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=DEBUG to see request/response details

Port already in use (HTTP mode)

Change MCP_PORT to a different port, or find and stop the process:

lsof -i :8000   # macOS/Linux
netstat -ano | findstr :8000   # Windows

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages