Skip to main content
Glama
README.md
# OpenAPI → MCP Generator

Published on npm as `@beernathan87/openapi-mcp-generator` (the unscoped name was already taken); the command name stays `openapi-mcp`.

Turn an existing REST/OpenAPI application into an MCP server - **security-first**: nothing is exposed until you opt it in, destructive operations need an explicit second flag, auth never comes from the model, and every call is confined to one base URL with timeouts, size caps and a rate limit.

```bash
npx openapi-mcp init https://api.example.com/openapi.json      # -> openapi-mcp.config.json, all tools disabled
#   edit: "enabled": true on the operations you want; destructive ones also need "confirmDestructive": true
npx openapi-mcp list                                            # what would be exposed
npx openapi-mcp console                                         # local test page: call tools through the real executor
npx openapi-mcp serve                                           # stdio MCP server for your client
```

MCP client config (Claude Desktop / Claude Code / Cursor):

```json
{ "mcpServers": { "petstore": { "command": "npx", "args": ["openapi-mcp", "serve", "--config", "/abs/path/openapi-mcp.config.json"], "env": { "API_TOKEN": "..." } } } }
```

## What it generates

For every operation in the spec (OpenAPI 3.x, JSON or YAML, local `$ref`s resolved):

- a tool name (`operationId`, or `method_path`), a description with method, path and a `[WRITE]`/`[DESTRUCTIVE]` tag
- a JSON Schema input built from path/query/header parameters plus the request body (`body` argument), with `required` and `additionalProperties: false`
- MCP annotations derived from the method and wording: `readOnlyHint` (GET/HEAD), `destructiveHint` (DELETE, or "delete/remove/destroy/purge/reset/revoke" in the summary), `idempotentHint`, `openWorldHint: false`
- a risk class `read | write | destructive` shown in `list` and the console

## Guardrails (executor)

- Only the config's `baseUrl` origin is ever contacted; path parameters are URL-encoded; dot segments, slash/backslash and percent escapes are rejected, including in route templates. Review the base URL before enabling anything.
- Auth is injected from an environment variable (`auth.type`: `bearer` | `header` | `basic`; `auth.env`, default `API_TOKEN`). Tool arguments cannot set `Authorization`, `Cookie`, `Host` or other transport headers.
- Timeout (`limits.timeoutMs`, 15 s), response cap (`limits.maxResponseBytes`, 256 KiB - truncated and flagged), per-process rate limit (`limits.maxCallsPerMinute`, 60), no redirect following.
- Disabled tools are not merely refused - they are not listed. Enabled destructive tools without `confirmDestructive: true` are blocked, and the config fails validation so you notice.
- `refresh` re-reads the spec after an API update, keeps enabled flags for matching method/path pairs, and reports operations added/removed - new endpoints stay disabled.

## Test console

`openapi-mcp console` serves a page on `127.0.0.1` only: it lists all operations (greyed out when disabled), shows the generated schema and annotations, lets you edit JSON arguments and calls the tool through the same executor the MCP server uses, showing the exact request URL and the response.

## Limits (v1)

OpenAPI 3.x only (no Swagger 2.0); external `$ref`s are not fetched; `oneOf`/`anyOf` bodies are passed through as-is; multipart/file uploads are not supported; responses are returned as text/JSON up to the cap; no OAuth flows (bring a token). Single-process rate limiting.

## Development

```bash
npm install
npm test   # includes an in-memory MCP client <-> server round trip with a fake upstream API
```

MIT.

## Production notes

- **Verified 2026-09-13** from the packed tarball on Windows 11 and Linux (node:22 container) against two real APIs: **httpbin.org** through a rich spec (component `$ref`s, `allOf`, three auth styles, optional/array/object/header parameters, path templates, error responses, a deprecated operation) and **Swagger Petstore v3** from its public URL. Through a real MCP stdio session: tools listed with correct annotations; path traversal in a path parameter rejected; `Authorization`/`Host` supplied as tool arguments ignored (auth comes only from the environment); upstream 404/500/503 and a 4 s timeout returned as tool errors while the session stayed protocol-clean and exited 0; disabled and undeclared-parameter operations fail closed. `refresh` after a spec change disabled an operation whose method changed to DELETE, kept a confirmed destructive tool enabled, left a new operation disabled and reported the removed one.
- **Exit codes:** `serve` exits 1 on an invalid config (including enabled-but-unconfirmed destructive tools) before speaking MCP; stdout is protocol-only, diagnostics go to stderr.
- **Auth:** the token env var must be present at `serve` time or every call fails closed (`auth requires environment variable …`). Put it in the MCP client's `env` block, never in the config file.
- **Update:** `npm install -g @beernathan87/openapi-mcp-generator@latest`, then `openapi-mcp refresh` per config; review the added/removed list. Configs are plain JSON you own - back them up with your project.

### Troubleshooting

| Symptom | Cause / fix |
|---|---|
| `baseUrl must be an absolute http(s) URL` on `init` from a local file | the spec's `servers[0].url` is relative; init from the spec's URL instead, or set `baseUrl` in the config by hand |
| `missing path parameter …` | the spec does not declare a parameter its path template uses; declare it in the spec and `refresh` |
| a tool is listed by `list` but not by the client | the client cached `tools/list`; restart the client after editing the config |
| `auth requires environment variable API_TOKEN` | export the variable (or the one named by `auth.env`) in the client's server `env` |

## Credits

Created by Nathan Beer. Developed by Nathan Beer with AI-assisted engineering using Claude and ChatGPT. Third-party licenses: [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md). Not affiliated with or endorsed by Anthropic, OpenAI, SmartBear/Swagger or the Model Context Protocol project.

## Security review contract (v0.1)

The config is trusted local policy. The spec, model arguments and upstream response are untrusted. Review destinations and operation semantics before enabling tools: method-derived risk hints cannot prove that an API is read-only. `confirmDestructive` is a persistent operator opt-in, not per-call user consent. A newly destructive operation is disabled on refresh; descriptions, schemas, names and annotations are regenerated, replacing manual overrides. New routes remain disabled even when an operation ID is reused. Deprecated enabled operations warn on `serve` stderr.

Relative or templated server URLs are rejected; provide an absolute HTTP(S) URL without credentials, query or fragment. Imports are limited to 4 MiB, 512 operations, 64 KiB per input schema, depth 40 100,000 expansion nodes and 8 MiB expanded string data; remote imports time out after 15 seconds. External refs are not fetched and recursive refs become placeholders. Review these incomplete schemas before use. Descriptions/titles have control and format characters removed and are capped at 1,000 characters. This does **not** neutralize prompt injection: run `mcp-scan` against the generated server before deployment and after refresh, and treat response text as untrusted data.

Only declared top-level parameters are serialized; unknown top-level arguments are dropped. Request bodies remain API payloads, without runtime JSON Schema validation or recursive filtering. Object/array path, query and header parameters use JSON strings; OpenAPI style/explode is not implemented. GET/HEAD bodies are omitted. Basic auth uses one environment variable containing `user:pass`. Custom auth headers cannot be overridden by model arguments. API-specific query credentials and semantic routing headers are not inferred: do not enable parameters that act as credentials, proxy destinations or routing overrides.

Responses are streamed up to the byte cap and cancelled when exceeded; redirects are unsuccessful results. Non-JSON content stays text (binary is decoded as UTF-8). Text plus structuredContent duplicates the bounded response; JSON escaping can increase wire size by a constant factor. Peer exception details are withheld. Exact raw and Basic-base64 credential echoes are redacted from response bodies/content types; transformed, split, or encoded secrets cannot reliably be recognized. The upstream necessarily receives configured auth: only use a trusted destination, and treat console/results as sensitive.

The console binds to loopback, checks Host and exact Origin when present, and limits call bodies to 256 KiB. It is not an authentication boundary against local processes. Rate limits use a rolling 60-second process-local window, including invalid attempts. `serve` reserves stdout for MCP; diagnostics go to stderr; CLI failures exit 1.