Point it at a spec — your AI agent can call the API.
OpenAPI v3 • JSON & YAML • Auto-auth • Zero config
Every endpoint becomes a tool. Every schema becomes a resource.
Quick Start · Agent Setup · Auth · Programmatic API · CLI
npx dynamic-openapi-mcp -s https://petstore3.swagger.io/api/v3/openapi.jsonThat's it. The MCP server starts, your AI agent discovers all the tools, and can call the Petstore API.
For Claude Code, add it in one command:
claude mcp add petstore -- npx dynamic-openapi-mcp -s https://petstore3.swagger.io/api/v3/openapi.jsonNow ask Claude: "list all available pets" — it will call listPets and return real data.
- Quick Start
- What's Inside
- Setup with AI Agents
- Authentication
- Programmatic Usage
- CLI Reference
- How the Mapping Works
- License
| Category | What you get |
|---|---|
| Tools | One per operation — GET /pets becomes listPets, with fully typed inputs |
| Resources | Full spec as openapi://spec + each schema as openapi://schemas/{name} |
| Prompts | describe-api for an overview, explore-endpoint for details on any operation |
| Auth | Bearer, API Key (header/query/cookie), Basic, OAuth2 client credentials, token exchange |
| Bodies | JSON, form-urlencoded, multipart/form-data, and octet-stream request bodies |
| Sources | URL, local file (JSON/YAML), inline string, or JavaScript object |
The flow is simple: AI calls a tool → dynamic-openapi-mcp makes the real HTTP request → response comes back as MCP content.
Add to your project's .mcp.json:
{
"mcpServers": {
"my-api": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "https://api.example.com/openapi.json"],
"env": {
"OPENAPI_AUTH_TOKEN": "your-bearer-token"
}
}
}
}Or add via CLI:
claude mcp add my-api -- npx dynamic-openapi-mcp -s https://api.example.com/openapi.jsonGo to Settings → MCP and add a new server, or add to .cursor/mcp.json:
{
"mcpServers": {
"my-api": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "./specs/api.yaml"],
"env": {
"OPENAPI_API_KEY": "your-api-key"
}
}
}
}Add to ~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"my-api": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "https://api.example.com/openapi.json"],
"env": {
"OPENAPI_AUTH_TOKEN": "sk-..."
}
}
}
}Add to your config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"my-api": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "/absolute/path/to/spec.yaml"],
"env": {
"OPENAPI_AUTH_TOKEN": "your-token"
}
}
}
}Connect several APIs at once — each runs as a separate MCP server, and the AI sees all their tools combined:
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json"],
"env": { "OPENAPI_AUTH_TOKEN": "ghp_..." }
},
"stripe": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "./specs/stripe.yaml"],
"env": { "OPENAPI_AUTH_TOKEN": "sk_..." }
},
"internal-api": {
"command": "npx",
"args": ["dynamic-openapi-mcp", "-s", "https://internal.company.com/api/v1/openapi.json"],
"env": { "OPENAPI_API_KEY": "key-..." }
}
}
}| If your API uses... | Use this | Auto-refresh | Best for |
|---|---|---|---|
| Static bearer token | OPENAPI_AUTH_TOKEN or auth.bearerToken |
No | Personal access tokens, fixed service tokens |
| Static API key | OPENAPI_API_KEY or auth.apiKey |
No | Header/query/cookie API keys declared in the spec |
| Basic auth | auth.basicAuth |
No | Legacy username/password APIs |
| OAuth2 client credentials | auth.oauth2 |
Yes | Machine-to-machine OAuth flows with tokenUrl |
| Temporary token exchange | auth.tokenExchange |
Yes | Non-standard credId / credSecret login flows |
| Fully custom auth logic | auth.custom |
You implement it | Edge cases not covered by built-in strategies |
# Bearer token (most common)
OPENAPI_AUTH_TOKEN=sk-123 npx dynamic-openapi-mcp -s ./spec.yaml
# API key
OPENAPI_API_KEY=key-456 npx dynamic-openapi-mcp -s ./spec.yaml
# Per-scheme (matches securitySchemes names in your spec)
OPENAPI_AUTH_BEARERAUTH_TOKEN=sk-123 npx dynamic-openapi-mcp -s ./spec.yamlOr set them in the MCP config env block — same effect, cleaner setup.
| Scheme | Env var | Programmatic config |
|---|---|---|
| Bearer | OPENAPI_AUTH_TOKEN or OPENAPI_AUTH_<SCHEME>_TOKEN |
auth.bearerToken |
| API Key (header/query/cookie) | OPENAPI_API_KEY or OPENAPI_AUTH_<SCHEME>_KEY |
auth.apiKey |
| Basic | OPENAPI_AUTH_<SCHEME>_TOKEN as user:pass |
auth.basicAuth |
| OAuth2 (client credentials) | — | auth.oauth2 |
| Token exchange | — | auth.tokenExchange |
| Custom | — | auth.custom (function) |
Resolution order: programmatic config → per-scheme env var → global env var.
Per-scheme environment variables are derived from the securitySchemes name in your OpenAPI document:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearerThis scheme name maps to:
OPENAPI_AUTH_BEARERAUTH_TOKEN=sk-123For a basic auth scheme named basicAuth, use:
OPENAPI_AUTH_BASICAUTH_TOKEN=username:passwordBearer token:
const mcp = await createOpenApiMcp({
source: './spec.yaml',
auth: { bearerToken: process.env.MY_API_TOKEN! },
})API key:
const mcp = await createOpenApiMcp({
source: './spec.yaml',
auth: { apiKey: process.env.MY_API_KEY! },
})Basic auth:
const mcp = await createOpenApiMcp({
source: './spec.yaml',
auth: {
basicAuth: {
username: process.env.API_USER!,
password: process.env.API_PASSWORD!,
},
},
})OAuth2 client credentials with automatic token caching and refresh:
const mcp = await createOpenApiMcp({
source: './spec.yaml',
auth: {
oauth2: {
clientId: process.env.OAUTH_CLIENT_ID!,
clientSecret: process.env.OAUTH_CLIENT_SECRET!,
tokenUrl: 'https://auth.example.com/oauth/token',
scopes: ['pets:read', 'pets:write'],
},
},
})dynamic-openapi-mcp caches the retrieved access token in memory and refreshes it when it is close to expiration.
Many APIs are not true OAuth2, but still issue a short-lived bearer token after exchanging credentials such as credId and credSecret.
For these APIs, use auth.tokenExchange. The built-in strategy:
- Exchanges credentials for a temporary token.
- Caches the token in memory.
- Refreshes slightly before
expires_inorexpires_at. - Retries once on
401 Unauthorizedafter forcing a fresh token. - Reuses a single in-flight refresh promise so concurrent MCP calls do not stampede the auth server.
Example:
import { createOpenApiMcp } from 'dynamic-openapi-mcp'
const mcp = await createOpenApiMcp({
source: './spec.yaml',
auth: {
tokenExchange: {
tokenUrl: 'https://auth.example.com/session',
request: {
contentType: 'application/json',
fields: {
credId: process.env.CRED_ID!,
credSecret: process.env.CRED_SECRET!,
},
},
response: {
tokenField: 'access_token',
expiresInField: 'expires_in',
},
apply: {
location: 'header',
name: 'Authorization',
prefix: 'Bearer ',
},
},
},
})Notes:
auth.tokenExchangealso supports form-encoded requests viarequest.contentType: 'application/x-www-form-urlencoded'.- If the token response is nested, use dot-paths such as
response.tokenField: 'data.accessToken'. - If there is no expiry metadata, the token stays cached until the API returns
401, then a new exchange is attempted once. apply.locationcan beheader,query, orcookie.- The token cache is in-memory. If the MCP process restarts, it will fetch a new token on the next request.
- If your auth flow cannot be described declaratively, fall back to
auth.custom.
Advanced fallback with auth.custom:
const mcp = await createOpenApiMcp({
source: './spec.yaml',
auth: {
custom: async (_url, init) => {
const headers = new Headers(init.headers)
headers.set('Authorization', `Bearer ${await getMyTokenSomehow()}`)
return { ...init, headers }
},
},
})For protected endpoints, OpenAPI usually describes the final auth mechanism used when calling the API, not the full lifecycle of how a client should fetch and refresh credentials.
Standard bearer auth:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
security:
- bearerAuth: []API key auth:
components:
securitySchemes:
apiKeyAuth:
type: apiKey
name: X-API-Key
in: header
security:
- apiKeyAuth: []OAuth2 client credentials:
components:
securitySchemes:
oauth:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://auth.example.com/oauth/token
scopes:
pets:read: Read pets
security:
- oauth: [pets:read]Custom temporary token flows are usually documented in two separate places:
- The protected endpoints declare
bearerAuthorapiKeyAuthinsecuritySchemes. - A normal operation in
pathsdocuments the login or token-exchange endpoint.
Example:
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
paths:
/auth/token:
post:
summary: Exchange credId and credSecret for a temporary token
requestBody:
required: true
responses:
'200':
description: Token issuedThis pattern is common, but it does not fully tell a generic client:
- which credentials should come from environment variables
- which response field contains the token
- how long the token is valid
- when to refresh it
- whether a
401should trigger a new exchange
That is why true OAuth2 is easiest to automate from OpenAPI alone, while custom temporary-token systems usually need either explicit auth.tokenExchange config or a small amount of user-supplied code.
If you want to document these custom flows more explicitly for users of this library, a future vendor extension could look like this:
x-dynamic-openapi-mcp-auth:
type: tokenExchange
tokenUrl: https://auth.example.com/session
request:
contentType: application/json
fields:
credId:
env: CRED_ID
credSecret:
env: CRED_SECRET
response:
tokenField: access_token
expiresInField: expires_in
tokenType: BearerThis is not used by dynamic-openapi-mcp today, but it shows the kind of metadata that would make temporary-token flows much easier to automate.
- If requests return
401 Unauthorized, first confirm the OpenAPI spec'ssecuritySchemesmatches how the real API expects auth. - If you use environment variables, prefer per-scheme variables when the spec defines multiple auth schemes.
- If your token expires every few minutes, use programmatic auth instead of a static env var.
- If your provider gives you a login endpoint that is not OAuth2, start with
auth.tokenExchange. Useauth.customonly when the exchange is too irregular to describe declaratively. - If the provider requires the temporary token in a query string or cookie,
auth.tokenExchangesupportsapply.location: 'query'andapply.location: 'cookie'.
pnpm add dynamic-openapi-mcpimport { createOpenApiMcp } from 'dynamic-openapi-mcp'
const mcp = await createOpenApiMcp({
source: 'https://petstore3.swagger.io/api/v3/openapi.json',
auth: { bearerToken: 'my-token' },
})
// Start as MCP server over stdio
await mcp.serve()const mcp = await createOpenApiMcp({
source: './spec.yaml',
baseUrl: 'http://localhost:3000',
headers: { 'X-Custom-Header': 'value' },
})const mcp = await createOpenApiMcp({
source: {
openapi: '3.0.3',
info: { title: 'My API', version: '1.0.0' },
servers: [{ url: 'https://api.example.com' }],
paths: {
'/hello': {
get: {
operationId: 'sayHello',
summary: 'Say hello',
responses: { '200': { description: 'OK' } },
},
},
},
},
})const mcp = await createOpenApiMcp({ source: './spec.yaml' })
console.log(mcp.spec.title) // "My API"
console.log(mcp.spec.operations) // ParsedOperation[]
console.log(mcp.spec.schemas) // { Pet: {...}, User: {...} }By default, dynamic-openapi-mcp retries only safe methods: GET, HEAD, OPTIONS, and TRACE.
This keeps reads resilient without risking duplicate writes on POST, PUT, PATCH, or DELETE.
If you want different behavior, set fetchOptions.retryPolicy:
const mcp = await createOpenApiMcp({
source: './spec.yaml',
fetchOptions: {
retries: 2,
retryPolicy: 'all', // 'safe-only' (default) | 'all' | 'none'
},
})Notes:
retryPolicy: 'safe-only'is the default.retryPolicy: 'all'retries mutating requests too.retryPolicy: 'none'disables request retries entirely.- Built-in auth token fetches use their own internal retry behavior and are not blocked by the default safe-only policy.
dynamic-openapi-mcp [options] [source]
Options:
-s, --source <url|file> OpenAPI spec URL or file path
-b, --base-url <url> Override the base URL from the spec
-h, --help Show help
| Environment Variable | Description |
|---|---|
OPENAPI_SOURCE |
Spec URL or file path (alternative to -s) |
OPENAPI_BASE_URL |
Override base URL |
OPENAPI_AUTH_TOKEN |
Bearer token for authentication |
OPENAPI_API_KEY |
API key for authentication |
Each operation in the spec becomes one MCP tool:
| OpenAPI | MCP Tool |
|---|---|
operationId: listPets |
Tool name: listPets |
GET /pets/{petId} (no operationId) |
Tool name: get_pets_by_petId |
summary or description |
Tool description (truncated to 200 chars) |
| Path + query + header params | Top-level input properties |
| Request body | Input property under body key |
Request bodies preserve the original media type when possible:
application/jsonis sent as JSON.application/x-www-form-urlencodedis serialized asURLSearchParams.multipart/form-datais serialized asFormData.application/octet-streamand other binary bodies support{ dataBase64, filename?, contentType? }.
Response handling follows the same idea:
- JSON is pretty-printed.
- Images are returned as MCP image content.
- Other binary payloads are returned as binary metadata plus base64 when small enough to inline.
| OpenAPI | MCP Resource URI |
|---|---|
| Full dereferenced spec | openapi://spec |
components.schemas.Pet |
openapi://schemas/Pet |
components.schemas.User |
openapi://schemas/User |
| Prompt | Args | What it returns |
|---|---|---|
describe-api |
— | Overview with title, version, all endpoints, auth schemes, schemas |
explore-endpoint |
operationId |
Full details: parameters, request body schema, responses, security |
MIT