PluginWorld
Ef

effect-openapi-mcp

MCP

Turns any API with an OpenAPI 3.0/3.1/3.2 spec into callable MCP tools: it generates a tool per supported operation, validates arguments, and forwards authenticated requests, served over stdio or local HTTP or embedded as a library. No hand-written tool per endpoint is required.

@rtzll · v0.0.0 · MIT License · updated today

SECURITY

B

SCORE

68

STARS

▲ 0

PLUG IN

git clone https://github.com/rtzll/effect-openapi-mcp.git

See the README to configure this MCP server

README

Effect OpenAPI MCP

Expose an API as MCP tools using its OpenAPI specification. Supply the spec, API target and credentials; the adapter generates tools, validates arguments and calls the API. No hand-written tool per endpoint is needed.

Use it as a stdio or local HTTP server, or embed its Effect 4 library. It supports a defined subset of OpenAPI 3.0 / 3.1 / 3.2, in JSON or YAML.

Quickstart · Your API · MCP clients · Library · Configuration · Compatibility · Security

Quickstart

From a source checkout, in the project directory containing package.json, install Vite+, open a new terminal, then run:

vp install --frozen-lockfile
vp run demo

Vite+ selects Node from .node-version, pnpm from package.json#packageManager, and the project toolchain from the lockfile. No separate Node or pnpm setup is needed. The package requires Node 24.18+.

The demo builds the package, starts a local mock MCP server, calls its tools, checks invalid input, then shuts down. It needs no .env, credentials or external API. Only tool/dependency installation needs network access or populated caches.

Use your own API

Copy .env.example to .env, then replace its values. For example:

MCP_OPENAPI_SPEC=/absolute/path/to/your/openapi.yaml
MCP_API_BASE_URL=https://api.example.com/v1/
MCP_CREDENTIALS='{"ApiKeyAuth":"replace-with-your-api-key"}'
MCP_METHODS='["get"]'
MCP_TRANSPORT=http

ApiKeyAuth must match a security-scheme name in your spec; use {} for an anonymous API. The target override is optional when the spec declares the right server. The specification can also be an HTTPS URL.

Inspect before serving:

vp run start inspect
vp run start check --strict
vp run start

inspect lists tools and exclusions without calling the API. check requires a nonempty, exportable catalog; --strict also rejects unsupported operations. Intentional method/tag filters are allowed. The HTTP endpoint defaults to http://127.0.0.1:3000/mcp. Restart after changing the spec or configuration.

Without filters, all supported operations are exposed, including writes. The example above limits discovery and execution to GET operations. Use credentials with only the permissions you need. The bundled inventory spec is an example contract; it requires your own matching API for real requests.

Add --json to inspect for machine-readable output. To avoid task-runner output, use vp exec node --env-file=.env src/cli.ts inspect --json.

vp run start --help lists CLI options. vp run start check checks your API spec; vp run check checks this project's code.

Connect an MCP client

Try the offline fixture in Claude Code

After the quickstart, print a ready-to-use configuration:

vp exec node scripts/client-config.ts

Merge the printed mcpServers entry into your Claude Code project's .mcp.json. Open Claude Code there, run /mcp, and approve openapi-fixture when prompted. Ask: Use echo_message to echo “Café + 😀” with count 3. The tool should return {"message":"Café + 😀","count":3}. The generator prints configuration only; it does not edit files or start a server.

Connect your own API over stdio

Build the executable and get the selected Node path:

vp run build
vp exec node -p "process.execPath"

Set MCP_TRANSPORT=stdio in .env. In a client using mcpServers configuration, replace these example paths with your actual absolute paths:

{
  "mcpServers": {
    "my-api": {
      "command": "/absolute/path/to/node",
      "args": [
        "--env-file=/absolute/path/to/openapi-mcp/.env",
        "/absolute/path/to/openapi-mcp/dist/cli.js"
      ]
    }
  }
}

Other clients use the same executable and arguments in their own configuration format. The client owns the process. Use absolute paths for the local spec too, keep stdout protocol-only, and build before connecting. Adapter logs go to stderr. Rebuild after source changes; regenerate fixture configuration if paths move.

Troubleshooting: missing dist means run vp run build; missing tools means check inspect, credentials and filters. For oversized catalogs, narrow MCP_METHODS/MCP_TAGS to fit your client's limits. HTTP clients can instead connect to the running loopback endpoint above.

Use the library

Import from effect-openapi-mcp. compileOpenApi(document, options) compiles an assembled document without I/O; loadOperationCatalog(location, options) loads and resolves a file/URL through Effect. Compose HttpApi, generatedTools and HTTP/stdio server layers when embedding the adapter. Imports never start a server.

These complete examples build and run offline:

Command after vp run Demonstrates
example:library Validate arguments and substitute HTTP with a layer
example:recovery Handle an actual HTTP 429 with explicit fallback data
example:cancellation Interrupt a request and observe cleanup

Every vp run check type-checks all examples and runs their offline tests against the built library.

To use the library in another application, create an installable tarball:

vp pm pack --pack-destination /tmp

Install the printed .tgz file with your application's package manager, keeping its Effect dependencies aligned with package.json. The library and CLI ship compiled JavaScript. Copy TypeScript examples and their imported helpers into your application's source before running them with Node; Node does not strip TypeScript inside node_modules.

vp run test:package /absolute/path/to/the-printed-archive.tgz --online checks the public types, CLI and packaged examples in an independent fresh consumer.

Configuration

vp run start loads .env if present. Direct Node runs need --env-file explicitly. Method/tag filters are JSON arrays; both must match when set, and empty arrays mean unrestricted. Credential values are raw keys/tokens (username:password for Basic); OAuth token acquisition and refresh happen outside this adapter. API targets and remote specs require HTTPS, except loopback HTTP; redirects are rejected.

Variable Default Purpose
MCP_OPENAPI_SPEC Required OpenAPI JSON/YAML file or HTTPS URL
MCP_API_BASE_URL Spec's servers Optional target override
MCP_CREDENTIALS {} JSON object of security-scheme names to secrets
MCP_TRANSPORT http http or stdio
MCP_METHODS [] JSON array of lowercase HTTP methods to expose
MCP_TAGS [] JSON array of operation tags to expose
MCP_SPEC_TIMEOUT_MS 10000 Specification read/download deadline
MCP_REFERENCES_MODE none External references: none, local, same-origin
MCP_MAX_REFERENCE_DOCUMENTS 128 Maximum root and referenced documents
MCP_MAX_REFERENCE_DEPTH 64 Maximum dereference chain length
MCP_REQUEST_HEADERS {} Fixed API request headers as a JSON object
MCP_TOOL_HEADERS [] Optional per-call header names exposed in tool inputs
MCP_PARAMETER_PROFILE brackets-indexed Use spec for strict portable deepObject encoding
MCP_TIMEOUT_MS 10000 Upstream deadline, including reading the response body
MCP_MAX_SPEC_BYTES 33554432 Total specification graph bytes
MCP_MAX_RESPONSE_BYTES 8388608 Maximum decoded upstream response bytes
MCP_MAX_REQUEST_BYTES 8388608 Maximum MCP HTTP input and encoded upstream request body bytes
PORT 3000 Loopback HTTP port

External references default off. local confines them to the entry file's canonical directory; same-origin confines them to its HTTPS/loopback origin. They share the graph byte/deadline limits, never inherit API credentials and cannot follow redirects. Relative local paths use the process working directory.

The default brackets-indexed profile serializes declared deepObject query/form fields with indexed brackets, including nested arrays and objects. Other parameter styles follow their declarations. This convention extends beyond OpenAPI's portable flat-object rules; use MCP_PARAMETER_PROFILE=spec when an API requires strict portable encoding. That mode excludes operations with unsupported shapes.

Indexed encoding preserves numeric map keys. Empty objects and arrays emit no fields; use an empty string when the API requires an explicit clearing value.

Stripe's published spec works with the default profile. Use credentials named bearerAuth. Leave MCP_API_BASE_URL unset: Stripe's file operations use files.stripe.com, while most operations use api.stripe.com.

To pin Stripe's API version and optionally target a connected account:

MCP_REQUEST_HEADERS='{"Stripe-Version":"2026-09-30.endive","Stripe-Account":"acct_example"}'
MCP_TOOL_HEADERS='["Idempotency-Key"]'

Omit Stripe-Account when using your own account. Each tool then accepts an optional headers object, for example {"headers":{"Idempotency-Key":"order-123"}}. Use a distinct key for each logical write and reuse it only when repeating that same write. Keep idempotency keys per call rather than setting a fixed default. Per-call values override fixed defaults. Declared header schemas retain their constraints; authentication and transport headers cannot be configured this way. Fixed values are redacted in configuration and never included in tool schemas.

Compatibility reference

Area Supported scope / important limits
Documents OpenAPI 3.0/3.1/3.2 JSON or one YAML 1.2 document; Swagger 2, duplicate keys, YAML aliases/custom tags and multiple documents are rejected
Schemas Native Effect validation for supported constraints, compositions and recursive refs; unsupported constructs produce diagnostics rather than approximate validation
HTTP GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS and OpenAPI 3.2 QUERY; JSON, supported form-urlencoded, multipart and ordinary UTF-8 text; YAML/PDF/octet-stream responses; no streaming
Authentication Header/query API keys, bearer, Basic and supplied OAuth/OIDC tokens; missing/unsupported protected credentials omit the operation, never retry anonymously
MCP Tools over HTTP/stdio; 2026-07-28 and tested legacy revisions; no resources/prompts or retired separate SSE transport
Responses Full JSON/text content; response schemas are not validated; no automatic pagination or retries

Tool arguments group locations as path, query, headers and body. Valid operationId values become tool names; other names are normalized predictably. Objects are returned directly as structured content; other results use { "result": ... }. Defaults/formats are annotations, not value injection or additional format validation. Run inspect and check --strict to see which operations can be exposed and why others are excluded.

Multipart file fields (for example body.file) take this JSON value:

{
  "dataBase64": "AAH/gA==",
  "filename": "example.bin",
  "mediaType": "application/octet-stream"
}

Use canonical padded base64, a simple ASCII filename and a concrete media type. Paths and URLs are not read. Other body fields keep their declared types. Multipart support covers closed object schemas with top-level binary file fields; unsupported encodings and file constraints produce diagnostics. The request byte limit includes multipart headers and boundaries.

Declared application/pdf and application/octet-stream responses return { "mimeType": "application/pdf", "dataBase64": "JVBERi0…" } through both the library and MCP structured content. Decode dataBase64 to recover the original bytes. MCP_MAX_RESPONSE_BYTES limits the received bytes before base64 expansion; oversized or mismatched response types fail without returning partial data.

application/yaml responses return the original UTF-8 text, preserving comments, line endings and YAML syntax. The library returns a string; MCP returns { "result": "..." }. The same response byte limit applies. YAML responses are not parsed into objects.

Uploads to a presigned URL returned by an earlier API call require a separate workflow. Generated tools use the specification's servers or the configured base URL; they do not accept arbitrary destination URLs at runtime.

In a source checkout, vp run compatibility and vp run benchmark generate local reports from the pinned fixture corpus. The benchmark measures the default configuration; vp run benchmark --include-strict compares both profiles. Compatibility checks always cover both. These checks establish the tested subset, not compatibility with every live API or client. Corpus counts measure compilation and schema export, not execution of every valid input. Stripe has pinned-spec request tests covering query/form encoding, metadata, uploads and PDF downloads; representative query/form requests also run through a local HTTP listener. Live Stripe sandbox verification has not been performed.

Security

Load trusted specifications: they select API targets and can contain expensive schemas. HTTP binds to loopback and checks Host/Origin. Upstream credentials do not authenticate incoming MCP clients or isolate tenants; remote hosting needs separate authentication and authorization. Tool annotations do not enforce approval.

Successful API responses may contain sensitive or untrusted content. API error bodies are withheld, but successful responses are returned unchanged. Cancellation aborts local HTTP work and cannot undo an upstream mutation already performed.

License

Project code is MIT. Third-party test fixtures retain their upstream licenses and notices and are excluded from the npm package. The manifests in fixtures/sources.json and fixtures/json-schema/sources.json record pinned sources, license files and verification hashes. Fixture documents are unchanged; larger files are gzip compressed.

The QUERY/tag examples are by the OpenAPI Initiative, under CC BY 4.0.

SIMILAR PLUGINS