I Built openapi-mcp-gateway: Multi-Spec OpenAPI-to-MCP With Real OAuth2 In Python
This gateway started as a demo. The job was to turn our company's OpenAPI spec into an MCP server so non-technical product folks could try the API through Claude Desktop. The naive version came together quickly.
Then the demo grew. For a customer-support-style agent to be useful, it needed to chain our product API with the internal issue tracker, and both gate access on the user's identity, not a shared service token. The audience was non-technical, so streamable HTTP was the floor. But I did not know what the eventual production client would be, so I wanted stdio and SSE on the same binary as well. And because the spec was actively changing, I needed the MCP layer to track it automatically rather than be hand-maintained.
By the time everything ran stably (multi-API, real per-user OAuth2, all three MCP transports, auto-regenerated from the spec), I had built openapi-mcp-gateway.
It is a Python service that turns OpenAPI specs into MCP servers, mounts as many of them as you want in one process, and does real per-user OAuth2 with token relay. You point it at a YAML and it runs.
Try It
uvx openapi-mcp-gateway --spec https://petstore3.swagger.io/api/v3/openapi.json --name petstore
# MCP server live at http://127.0.0.1:8000/petstore/mcp
Or add it as a dependency:
uv add openapi-mcp-gateway
Connect from Claude Desktop, Cursor, Cline, or any other MCP client. Streamable HTTP, SSE, and stdio are all supported. Python 3.11+.
What The Gateway Actually Does
Three things, each shown in code.
1. Multiple APIs In One Process
Most agent tasks touch more than one API. GitHub for repo lookups, your internal product API for the actual work, an external CRM. Point a single YAML at all of them:
# servers.yml
host: 0.0.0.0
port: 8000
url: http://localhost:8000
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, users:read, workspaces:read]
uvx openapi-mcp-gateway --config servers.yml
Three MCP servers at /petstore/mcp, /github/mcp, /asana/mcp. Each with its own auth. Each independently filterable with policy.allow / policy.deny, so you do not blast every endpoint of every spec at the model.
Visually, this YAML produces:
One process, three mount paths, three different auth backends, three different upstream identities. None of them leak into each other.
2. Real Per-User OAuth2 (Token Relay)
This is the constraint that drove most of the design. When a user opens Claude Desktop and connects to one of the mounted servers:
The gateway plays two roles at once, acting as an OAuth Authorization Server to the MCP client and as an OAuth Client to the upstream API. It mints its own MCP-scoped tokens with their own TTLs and revocation, and keeps a private mapping from those tokens to the upstream tokens.
The result is the part that matters. The MCP client never holds a credential for an API it does not know about, and the upstream API's audit log shows the actual end user.
And because every mounted server has its own auth config, one gateway process can run several different OAuth setups in parallel: Asana OAuth on /asana/mcp, GitHub bearer on /github/mcp, your internal OAuth on /myapp/mcp, all at once. Three different upstream identities, three independent OAuth flows, no cross-talk in the token store, the auth resolver, or the request path. The Asana token issued to one user cannot be confused with the GitHub bearer used to look up a repo. Each (server, user) pair lives in its own namespace.
For service-to-service calls, client_credentials is also supported, with lazy fetch and concurrent-refresh dedup under an asyncio.Lock so N concurrent tool calls hitting the gateway at expiry produce exactly one IdP request. Different shape, different OAuthFlowHandler.
3. FastAPI-Native @mcp_tool
Beyond raw OpenAPI specs, the gateway also takes a live FastAPI app as input. If you already have one, decorate the routes you want exposed:
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()
Tool calls run in-process through httpx.ASGITransport. No localhost roundtrip, no parallel spec, no separate uvicorn. Auth is auto-detected from the app's securitySchemes.
How It Compares To Existing Tools
I did the homework, so you do not have to. As of May 2026, here is the OpenAPI-to-MCP converter landscape (tools whose primary job is "take an OpenAPI spec, produce an MCP server"):
| mroops0111/openapi-mcp-gateway | fastapi_mcp | awslabs/mcp | ivo-toby | harsha-iiiv | ckanthony | |
|---|---|---|---|---|---|---|
| Language | Python | Python | Python | TS | TS (codegen) | Go |
| Input | OpenAPI or FastAPI app | FastAPI app only | OpenAPI | OpenAPI | OpenAPI | OpenAPI |
| Multi-spec in one process | Yes (YAML) | No | Yes | No | No | No |
| OAuth2 authorization_code w/ relay | Yes | Yes | Cognito only | Yes | No | No |
| OAuth2 client_credentials | Yes (lazy + lock) | Not documented | No | Yes | Yes | No |
| stdio + SSE + Streamable HTTP | All three | SSE + StrHTTP | stdio | Partial | All three | Single port |
| FastAPI-native decorator | Yes | Whole-app only | No | No | No | No |
| Pluggable Redis token store | Yes | No | No | No | No | No |
Reading guide:
- If your only input is a FastAPI app, fastapi_mcp is more focused than mine. The in-process ASGI trick is its native idea, and I borrowed it for
@mcp_tool. - If you need multi-spec on AWS and Cognito is your auth, awslabs/mcp is purpose-built for that.
- If you want a TypeScript runtime, ivo-toby/mcp-openapi-server is the closest analogue.
- If you want a generated Node project you control end-to-end, harsha-iiiv/openapi-mcp-generator is the right shape.
- If you want a Python service you point at a YAML, that hosts several specs at once, that does both OAuth2 flows on real upstream providers (not just Cognito), and that has a decorator for the FastAPI app you probably already have, this gateway is for you.
Concretely, this gateway tends to be the right fit in three situations:
- Internal agents that have to touch several in-house APIs. Customer support, finance ops, IT helpdesk. The agent needs your product API plus your issue tracker plus maybe your CRM, each with its own auth. One process, several mounts, no inter-service leakage.
- B2B APIs where actions need to happen as the actual end user. Signing a contract, moving money, closing a ticket on someone's behalf. Anything where the audit log on the upstream side has to show the human, not a shared service account. The token relay handles the per-user identity end to end.
- Demo or PoC stage with a moving spec. The OpenAPI spec is still evolving, you do not yet know what the production MCP client will be, and you do not want to hand-maintain an MCP server in parallel. Point the gateway at the spec, get all three transports for free, regenerate every time the spec changes.
What Is And Is Not There Yet
Shipping today: OAuth2 authorization_code with token relay, OAuth2 client_credentials with concurrency primitives, multi-server in one process (each with independent auth), FastAPI-native @mcp_tool, operation filtering (allow / deny / marked_only), Redis token store, all three MCP transports.
On the roadmap, in case any of these would unblock you:
- Flexible OAuth2 scope mapping. Today, upstream scopes pass through to MCP one-to-one. Some setups need to expose only a subset, alias names, or group several upstream scopes under one MCP-side scope. Configurable in YAML, per server.
-
Merging multiple OpenAPI specs into one MCP server. Today one spec produces one mounted server. Some upstream APIs split a single product's docs across several spec files (by tag, by version, by section), and forcing the agent to pick across N mount paths for what is logically one API is awkward. Want a
specs:list that the gateway combines into a single MCP server. -
MCP primitives beyond tools, with per-operation YAML overrides. Today every operation in your spec becomes an MCP tool. MCP also has resources, prompts, and sampling, and not every operation is best modelled as a tool (
GET /users/{id}is more naturally a resource than a tool). Want a YAML override per operation, which lets you choose the MCP primitive, rename it, hide it, or reshape its inputs. -
Dynamic tool exposure (skill-style discovery). For large APIs with hundreds of operations, registering every operation as its own tool blows the agent's context window before the agent has done anything. Want an opt-in mode where the gateway exposes only three meta-tools (
list,get,call), and the agent fetches operation details on demand. Same pattern as agent skills.
If any of these are blocking you, please open an issue with the shape of your problem. Concrete use cases drive the design more than my guesses.
Closing
This is a personal open-source side project, not anything I deploy at work. The constraints came from the kinds of agent flows I think about because of my dayjob (a B2B e-signature API), but the gateway itself is independent of that. Sharing it here in case the situations above sound like yours.
Top comments (0)