npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dynamic-openapi-mcp

v0.1.8

Published

Transform any OpenAPI v3 spec into a fully functional MCP server

Readme

dynamic-openapi-mcp

Any OpenAPI spec. Instant AI tools.

Point it at a spec — your AI agent can call the API. OpenAPI v3JSON & YAMLAuto-authZero config Every endpoint becomes a tool. Every schema becomes a resource.

npm version npm downloads TypeScript Node.js License

Quick Start · The family · Agent Setup · Auth · Filtering · Programmatic API · CLI


Quick Start

npx dynamic-openapi-mcp -s https://petstore3.swagger.io/api/v3/openapi.json

That'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.json

Now ask Claude: "list all available pets" — it will call listPets and return real data.


Table of Contents


What's Inside

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

The family

Three complementary projects, one spec, three output surfaces — pick the one that fits the use case:

| Sibling | Output | Runs when | Best when | |:--------|:-------|:----------|:----------| | dynamic-openapi-mcp | Live MCP server (stdio) | Every tool call spins the server | You want real-time introspection, auto-refreshed OAuth tokens, typed tool I/O | | dynamic-openapi-cli | Bash CLI (optionally bundled) | Humans and scripts invoke it | You want a commit-friendly shim humans and CI can run | | dynamic-openapi-skill | Static SKILL.md | Claude loads it on demand | You want zero runtime, diff-friendly docs, and model-driven calls via curl / fetch |

All three share the same parser and auth layer. Switching between them is a matter of pointing them at the same spec.

Setup with AI Agents

Claude Code

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

Cursor

Go 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"
      }
    }
  }
}

Windsurf

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-..."
      }
    }
  }
}

Claude Desktop

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

Multiple APIs

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-..." }
    }
  }
}

Authentication

Choosing an Auth Strategy

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

Via environment variables

# 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.yaml

Or set them in the MCP config env block — same effect, cleaner setup.

Supported schemes

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

This scheme name maps to:

OPENAPI_AUTH_BEARERAUTH_TOKEN=sk-123

For a basic auth scheme named basicAuth, use:

OPENAPI_AUTH_BASICAUTH_TOKEN=username:password

Programmatic Examples

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

Temporary Tokens and Refresh

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:

  1. Exchanges credentials for a temporary token.
  2. Caches the token in memory.
  3. Refreshes slightly before expires_in or expires_at.
  4. Retries once on 401 Unauthorized after forcing a fresh token.
  5. 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.tokenExchange also supports form-encoded requests via request.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.location can be header, query, or cookie.
  • 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 }
    },
  },
})

How Auth Is Usually Modeled in OpenAPI

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:

  1. The protected endpoints declare bearerAuth or apiKeyAuth in securitySchemes.
  2. A normal operation in paths documents 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 issued

This 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 401 should 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: Bearer

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

Troubleshooting Auth

  • If requests return 401 Unauthorized, first confirm the OpenAPI spec's securitySchemes matches 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. Use auth.custom only when the exchange is too irregular to describe declaratively.
  • If the provider requires the temporary token in a query string or cookie, auth.tokenExchange supports apply.location: 'query' and apply.location: 'cookie'.

Programmatic Usage

pnpm add dynamic-openapi-mcp
import { 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()

Custom base URL

const mcp = await createOpenApiMcp({
  source: './spec.yaml',
  baseUrl: 'http://localhost:3000',
  headers: { 'X-Custom-Header': 'value' },
})

From an inline spec

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' } },
        },
      },
    },
  },
})

Inspecting the parsed spec

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

Retry Behavior

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.

CLI Reference

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
      --server-index <n>         Select Nth server entry (default: 0)
      --include-tag <name>       Only expose operations with this tag (repeatable, comma-separated)
      --exclude-tag <name>       Hide operations with this tag (repeatable, comma-separated)
      --include-operation <id>   Only expose these operationIds (repeatable, comma-separated)
      --exclude-operation <id>   Hide these operationIds (repeatable, comma-separated)
  -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 |

Offline bundles

Package any OpenAPI spec into a single standalone bash binary that behaves as an MCP server — no network call at startup, deterministic md5, portable across machines.

dynamic-openapi-mcp bundle \
  -s https://petstore3.swagger.io/api/v3/openapi.json \
  --name petstore-mcp \
  --out ./bin/petstore-mcp

The generated file embeds the dereferenced spec as base64 JSON and execs dynamic-openapi-mcp with --source <tmpfile> at runtime. Any additional arguments are forwarded to the runner, so it drops straight into .mcp.json:

{
  "mcpServers": {
    "petstore": {
      "command": "/absolute/path/to/bin/petstore-mcp"
    }
  }
}

Bundled binaries also expose:

| Subcommand | Purpose | |:-----------|:--------| | --show-spec | Decode and print the embedded spec | | --spec-md5 | Print the md5 of the embedded spec | | --spec <url\|file> | Override the embedded spec at runtime | | update | Re-fetch the original spec and rewrite the binary in-place | | install | Symlink (or --copy) the binary into $XDG_BIN_HOME or ~/.local/bin | | uninstall | Remove a previous install |

Run dynamic-openapi-mcp bundle --help for the full list of options.

Filtering operations

Not every endpoint needs to reach the AI. Two ways to cut the surface:

Flags (and programmatic filters)

# only expose the `pets` and `store` tags
dynamic-openapi-mcp -s ./spec.yaml --include-tag pets --include-tag store

# hide admin endpoints and one noisy op
dynamic-openapi-mcp -s ./spec.yaml --exclude-tag admin --exclude-operation debugDump

# allowlist specific operations — tags are ignored for these
dynamic-openapi-mcp -s ./spec.yaml --include-operation listPets,getPetById

# mix-and-match: everything under `pets`, minus one write op
dynamic-openapi-mcp -s ./spec.yaml --include-tag pets --exclude-operation deletePet

Programmatic equivalent:

const mcp = await createOpenApiMcp({
  source: './spec.yaml',
  filters: {
    tags: { include: ['pets'], exclude: ['admin'] },
    operations: { include: ['healthCheck'], exclude: ['debugDump'] },
  },
})

Precedence (first match wins): x-hiddenoperations.excludeoperations.includetags.exclude → includes as allowlist. operations.include escapes a matching tags.exclude, but operations.exclude wins over everything except x-hidden.

x-hidden vendor extension

Let the spec author hide an endpoint from every consumer of this tool — no flags needed:

paths:
  /admin/reset:
    post:
      operationId: adminReset
      x-hidden: true       # always removed, regardless of filter flags

Good for internal-only endpoints that ship in the public spec but shouldn't be called from AI agents / bundled CLIs / skills.

x-mcp-hidden — MCP-only opt-out

Sometimes you want the operation visible in the CLI or skill consumers, but not exposed as an MCP tool — typically risky operations where you trust a human at the terminal but not an autonomous agent:

paths:
  /admin/wipe:
    delete:
      operationId: wipeEverything
      x-mcp-hidden: true   # invisible to MCP; still callable from the CLI

Distinct from x-hidden, which removes the operation everywhere.

MCP_MAX_TOOLS — tool budget

Specs with hundreds of operations poison the agent context with tool listings. Cap it:

MCP_MAX_TOOLS=50 dynamic-openapi-mcp -s ./huge-spec.yaml

When the spec exceeds the budget, operations are ranked (non-deprecated + tagged first, then alphabetical) and the surplus is registered as a single list_available_operations tool the agent can call to discover what was trimmed.

Tool safety annotations

Every registered tool gets MCP ToolAnnotations derived from the HTTP method, so hosts (Claude Desktop, Cursor) can prompt for confirmation on the right operations:

| Method | readOnlyHint | destructiveHint | idempotentHint | openWorldHint | |:-------|:--------------:|:------------------:|:----------------:|:---------------:| | GET, HEAD, OPTIONS, TRACE | ✓ | ✗ | ✓ | ✓ | | POST | ✗ | ✗ | ✗ | ✓ | | PUT | ✗ | ✗ | ✓ | ✓ | | PATCH | ✗ | ✗ | ✗ | ✓ | | DELETE | ✗ | ✓ | ✓ | ✓ |

Override at the spec level with vendor extensions:

paths:
  /search:
    post:
      operationId: searchThings
      x-side-effect: read-only   # POST that only reads — read-only annotations
  /admin/wipe:
    get:
      operationId: wipeEverything
      x-destructive: true        # GET that actually destroys — destructive annotation

Resolution: x-side-effect > x-destructive > HTTP method default.

Tool descriptions

Tool descriptions are synthesised deterministically from the operation's summary/description plus parameter signature plus response shape — much richer than truncate(summary):

Fetch pet by id

Parameters:
  - petId(path, integer<int64>, required)
  - status(query, enum: "available"|"pending"|"sold", optional)

Returns: 200 — array<Pet> — OK

Curated descriptions win via x-description-override:

paths:
  /search:
    post:
      operationId: searchThings
      x-description-override: |
        Search returns up to 25 pets matching the query, ordered by relevance.
        Call get-pet-by-id afterwards for full details.

How the Mapping Works

Operations → Tools

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/json is sent as JSON.
  • application/x-www-form-urlencoded is serialized as URLSearchParams.
  • multipart/form-data is serialized as FormData.
  • application/octet-stream and 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.

Schemas → Resources

| OpenAPI | MCP Resource URI | |:--------|:-----------------| | Full dereferenced spec | openapi://spec | | components.schemas.Pet | openapi://schemas/Pet | | components.schemas.User | openapi://schemas/User |

Prompts

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

License

MIT