Skip to main content

Turn any OpenAPI specification into a Model Context Protocol (MCP) server with a single command.

Project description

OpenAPI MCP Gateway

CI PyPI version PyPI Downloads Python Version License: MIT

Mount any OpenAPI (Swagger) spec as a Model Context Protocol (MCP) server, or expose your existing FastAPI app the same way. Several APIs in one process, each on its own mount path with its own auth.

uvx openapi-mcp-gateway --spec https://petstore3.swagger.io/api/v3/openapi.json
# Server live at http://127.0.0.1:8000/api/mcp
  • Multi-spec, multi-auth. Mount GitHub, an OAuth2 SaaS, and your internal API in one process. Each (server, user) pair has its own token namespace, no cross-talk. Bearer, API key, OAuth2 authorization_code for end-user delegation, and client_credentials for service flows all coexist.
  • FastAPI native, route-level. Decorate individual routes with @mcp_tool to opt them in one by one, no whole-app exposure. Routes run in-process via httpx.ASGITransport, no extra network hop and no second spec to maintain.
  • Dynamic exposure. For specs with hundreds of operations that blow the LLM context window, flip a server to exposure: dynamic and the agent walks list → get → call meta-tools on demand.
  • Spec-compliant authorization. Audience-bound tokens, no silent passthrough to third-party upstreams [MCP Authorization Spec: Access Token Privilege Restriction].
  • Pluggable token store. Memory by default. Switch to Redis when you need to share state across replicas.

Streamable HTTP, SSE, and stdio all supported on the same binary. Works with Claude Desktop, Cursor, Cline, or any other MCP client.


Installation

Add the gateway to your project with uv:

uv add openapi-mcp-gateway

Optional extras:

uv add "openapi-mcp-gateway[redis]"   # Redis token store, used for auth memoization

Requires Python 3.11+.

Quick Start

1. Public API, No Auth

# `uvx` runs the published package without installing it into your project;
# swap in `uv run` once you've added the gateway as a dependency.
uvx openapi-mcp-gateway --spec https://petstore3.swagger.io/api/v3/openapi.json --name petstore

Connect an MCP client to http://127.0.0.1:8000/petstore/mcp.

2. Bearer Token

export GITHUB_TOKEN="ghp_..."
uv run openapi-mcp-gateway \
    --spec https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json \
    --name github \
    --auth-type bearer \
    --auth-token '${GITHUB_TOKEN}'

3. OAuth2, Per-User Delegation (authorization_code)

The gateway runs its own OAuth server so each MCP client authenticates as its own end-user; tokens are minted per session.

export ASANA_CLIENT_ID="..." ASANA_CLIENT_SECRET="..."
uv run openapi-mcp-gateway \
    --spec https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml \
    --name asana \
    --auth-type oauth2 \
    --auth-client-id '${ASANA_CLIENT_ID}' \
    --auth-client-secret '${ASANA_CLIENT_SECRET}' \
    --auth-scopes "openid,email,profile,users:read,workspaces:read"

4. OAuth2, Service Token (client_credentials)

When the gateway holds its own credentials and shares one upstream token across every MCP client. No per-user OAuth dance:

export SVC_CLIENT_ID="..." SVC_CLIENT_SECRET="..."
uv run openapi-mcp-gateway \
    --spec ./service-api.json \
    --name svc \
    --auth-type oauth2 \
    --auth-flow client_credentials \
    --auth-client-id '${SVC_CLIENT_ID}' \
    --auth-client-secret '${SVC_CLIENT_SECRET}'

5. Multiple APIs at Once

Mix public, bearer, and OAuth2 services in a single config. Each server is mounted at /{name}/mcp:

# servers.yml
host: "127.0.0.1"
port: 8000
url: http://127.0.0.1:8000   # public base URL for OAuth callbacks

servers:
  - name: petstore
    spec: https://petstore3.swagger.io/api/v3/openapi.json

  - name: github
    spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json
    auth:
      type: bearer
      token: ${GITHUB_TOKEN}
    policy:
      allow: ["GET /repos/*", "GET /users/*"]
      deny:  ["GET /repos/*/actions/secrets*"]

  - name: asana
    spec: https://raw.githubusercontent.com/Asana/openapi/master/defs/asana_oas.yaml
    auth:
      type: oauth2
      client_id: ${ASANA_CLIENT_ID}
      client_secret: ${ASANA_CLIENT_SECRET}
      scopes: [openid, email, profile, users:read, workspaces:read]
export GITHUB_TOKEN="ghp_..."
export ASANA_CLIENT_ID="..." ASANA_CLIENT_SECRET="..."
uv run openapi-mcp-gateway --config servers.yml

Runnable variants live in examples/; each YAML lists prerequisites at the top.

${ENV_VAR} and ${ENV_VAR:-default} work in any string field, resolved at request time. For OAuth2, authorizationUrl / tokenUrl / scopes are auto-detected from the spec's securitySchemes; override with auth.authorization_url / auth.token_url / auth.scopes when the spec is incomplete.

6. Local Desktop Client (stdio)

For Claude Desktop, IDE integrations, or any MCP client that prefers stdio:

{
  "mcpServers": {
    "petstore": {
      "command": "uv",
      "args": [
        "run",
        "--project", "/abs/path/to/your/project",
        "openapi-mcp-gateway",
        "--spec", "/abs/path/to/openapi.json",
        "--transport", "stdio"
      ]
    }
  }
}

Configuration

Run uv run openapi-mcp-gateway --help for the CLI reference. The Quick Start examples cover most setups; the full field reference is below.

When values appear in more than one place, the rule is defaults < YAML (--config) < CLI flags < Gateway.run(...) kwargs, and a layer only overrides what it actually sets. Sub-trees (logging, per-server auth) merge field-by-field; the servers list is replaced wholesale.

Top-Level Fields
Field Type Default Description
host string 0.0.0.0 Bind address (0.0.0.0 = all interfaces). Clients on the same machine usually open http://localhost:{port} or http://127.0.0.1:{port}.
port int 8000 Bind port
url string (empty) Public base URL for OAuth redirects and discovery. When unset: http://localhost:{port} if host is 0.0.0.0, otherwise http://{host}:{port}. Override when your registered redirect URI uses another host (tunnel, reverse proxy, etc.).
transport string streamable-http sse, streamable-http, or stdio
store.type string memory memory or redis
store.redis_url string redis://localhost:6379 Redis URL when store.type: redis
logging.level string INFO DEBUG, INFO, WARNING, ERROR, CRITICAL
logging.format string text text or json
logging.file string Mirror logs to this file
servers list required List of per-server config entries
Per-Server Fields
Field Type Default Description
name string required Unique identifier; mount path defaults to /{name}
spec string required Path or URL to OpenAPI document (JSON or YAML)
base_url string from spec Override the upstream base URL
auth.type string none none, bearer, api_key, or oauth2
auth.token string Required for bearer / api_key
auth.api_key_header string X-API-Key Header name for api_key
auth.client_id, auth.client_secret string Required for oauth2
auth.scopes, auth.authorization_url, auth.token_url from spec OAuth2 overrides when securitySchemes is incomplete
policy.allow list Only expose matching operations
policy.deny list Exclude matching operations
timeout float 90 HTTP timeout in seconds
exposure string static static registers one MCP tool per operation. dynamic registers three meta-tools (list_operations, get_operation, call_operation) for the LLM to walk on demand.

Filtering Operations

Use policy.allow and policy.deny with fnmatch syntax against operation IDs (getUsers, create*) or method + path (GET /users/*):

policy:
  allow: ["GET /repos/*"]
  deny:  ["GET /repos/*/actions/secrets*"]

Operations can also be opted in from the spec side with x-mcp-integration: {expose: {tool: {}}} plus policy.marked_only: true. Filters apply in order: marked_only, then allow, then deny.

Dynamic Exposure

For APIs with hundreds of operations (GitHub, Stripe, etc.), registering every operation as its own MCP tool can blow the LLM's context window before the agent does anything. Flip the server to exposure: dynamic and the client sees three meta-tools instead:

servers:
  - name: github
    spec: https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json
    exposure: dynamic   # default is 'static'
    auth:
      type: bearer
      token: ${GITHUB_TOKEN}

The three meta-tools:

  • list_operations() returns [{name, description}, ...] for every operation on this server.
  • get_operation(name) returns one operation's JSON Schema for input arguments.
  • call_operation(name, arguments) invokes that operation against the upstream.

The LLM walks list → get → call to discover and invoke operations on demand. Auth, path templating, and per-operation request shape are identical to static mode; the only thing that changes is how the operations are surfaced to the client.

exposure is per-server, so /github/mcp can run dynamic while /petstore/mcp runs static in the same process.

Logging

Configure via the logging.* YAML keys or via CLI flags (--log-level, --log-format, --log-file); -v and -q are shortcuts for DEBUG and WARNING. CLI flags override YAML field-by-field, following the precedence rule above.

Python API

Use the gateway as a library inside your own Python application:

from openapi_mcp_gateway import Gateway

gateway = Gateway()
gateway.add_server(
    name="petstore",
    spec="https://petstore3.swagger.io/api/v3/openapi.json",
)
gateway.add_server(
    name="github",
    spec="./github-openapi.json",
    auth={"type": "bearer", "token": "${GITHUB_TOKEN}"},
    policy={"allow": ["GET /repos/*"]},
)
gateway.run(port=8000)

Expose Your FastAPI App as MCP Tools

Already running FastAPI? Decorate the routes you want to expose with @mcp_tool and the gateway picks them up. No second spec, no separate process. Routes run in-process via httpx.ASGITransport, so there is no extra network hop:

from fastapi import FastAPI
from openapi_mcp_gateway import Gateway, mcp_tool

app = FastAPI()

@app.get("/items/{item_id}")
@mcp_tool()
def read_item(item_id: int):
    return {"id": item_id}

@app.get("/internal/health")  # not decorated → not exposed
def health():
    return {"ok": True}

Gateway.from_fastapi(app, name="myapp").run()

Auth is auto-detected from the app's securitySchemes. Override by passing an explicit auth=AuthConfig(...) to Gateway.from_fastapi.

How auth works for the FastAPI integration

Because the gateway runs in-process and routes through httpx.ASGITransport, gateway and upstream share the same OAuth audience, so the MCP client's Authorization header passes through verbatim (auth.flow: passthrough, set automatically for this integration only). For client_credentials schemes the gateway mints upstream tokens from its own credentials instead.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

openapi_mcp_gateway-0.3.1.tar.gz (154.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

openapi_mcp_gateway-0.3.1-py3-none-any.whl (53.2 kB view details)

Uploaded Python 3

File details

Details for the file openapi_mcp_gateway-0.3.1.tar.gz.

File metadata

  • Download URL: openapi_mcp_gateway-0.3.1.tar.gz
  • Upload date:
  • Size: 154.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for openapi_mcp_gateway-0.3.1.tar.gz
Algorithm Hash digest
SHA256 abf75aa0c49cbb883ac3860b33c6e048ec1ff00409a5ce31c445a4d8144a626a
MD5 882e3b51576fe477e1c585064a77be90
BLAKE2b-256 e16b80a21fcea5dfae429059ce351bd60ff23b1d6c35d90019020f3b4ca7e544

See more details on using hashes here.

Provenance

The following attestation bundles were made for openapi_mcp_gateway-0.3.1.tar.gz:

Publisher: release.yml on mroops0111/openapi-mcp-gateway

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file openapi_mcp_gateway-0.3.1-py3-none-any.whl.

File metadata

File hashes

Hashes for openapi_mcp_gateway-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b81c14ceb82c7e59f66c9eb472336d6c395cc63e92eb240dee3fe0e7c6716801
MD5 7997dd7ef50349badf84509bd51c4afa
BLAKE2b-256 01e5fd2f2443520c8bc72ec3ef363b27bbf2e4a84f62a78a86c3ea2c07817dbc

See more details on using hashes here.

Provenance

The following attestation bundles were made for openapi_mcp_gateway-0.3.1-py3-none-any.whl:

Publisher: release.yml on mroops0111/openapi-mcp-gateway

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page