npm.io
0.2.1 • Published yesterdayCLI

@data-fair/openapi-mcp

Licence
AGPL-3.0-only
Version
0.2.1
Deps
5
Size
173 kB
Vulns
0
Weekly
0

openapi-mcp

Turns an OpenAPI document annotated with a custom x-agent extension into MCP tools for AI agents. Use it as a library to build a tool set from a spec and wire it into any MCP server, or run the bundled standalone binary to serve a spec directly over stdio or HTTP.

Install

npm install @data-fair/openapi-mcp

Library

import { load } from '@data-fair/openapi-mcp'
import { createMcpServer } from '@data-fair/openapi-mcp/adapters/mcp'

const toolSet = await load('https://example.com/openapi.json', { profiles: ['explore'] })
const server = createMcpServer(toolSet, { name: 'my-api', version: '1.0.0' })
// server.connect(transport) with any @modelcontextprotocol/sdk transport

load returns { profiles, instructions, tools, skills } — provider-agnostic tools, each with an input schema, a description, MCP annotations and an execute(params) that returns { text, structuredContent?, isError? } and never throws. toMcpServer(toolSet, server) registers them on an existing SDK Server if you already have one.

Standalone server

The package also installs an openapi-mcp binary that reads its configuration from the environment:

Env var Required Default Description
INDEX_URL one of URL of a deployment index listing service-level OpenAPI documents
OPENAPI_URL one of URL of a single OpenAPI document
PROFILES no Comma-separated profile set; the default profile otherwise
REFRESH_INTERVAL no 300 Seconds between conditional re-fetches of the index and documents; 0 disables
LINT no error error, warn or off — see Annotation lint
LOCALE no en Locale used to resolve localized text
BASE_URL no Overrides servers[0].url from the document
TRANSPORT no stdio stdio or http
PORT no 8080 Port to listen on when TRANSPORT=http
API_KEY_HEADER no Header name added to every upstream request
API_KEY no Value sent in API_KEY_HEADER
STRUCTURED_CONTENT no false Set to true to also return structuredContent from tool calls

In HTTP mode a request's ?profiles=explore,edit selects the set for that request. HTTP mode here is single-identity and stateless: it is for development. A deployed server that serves several callers forwards each caller's identity per call through the adapter's context hook — that is data-fair/mcp v2, not this binary.

Coding agents behind nhi-proxy

Identity comes from the environment; the binary implements nothing for it:

NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://127.0.0.1:7331 \
NODE_EXTRA_CA_CERTS=~/.config/nhi-proxy/koumoul.com/ca.crt \
INDEX_URL=https://koumoul.com/data-fair/api/v1/agents/index.json PROFILES=explore,edit openapi-mcp

API_KEY_HEADER + API_KEY are a single-credential convenience for standalone use. Anything richer (OAuth, multiple headers, per-request auth) is done in code with the library's fetch option.

Example MCP client configuration:

{
  "mcpServers": {
    "my-api": {
      "command": "npx",
      "args": ["-y", "@data-fair/openapi-mcp"],
      "env": { "OPENAPI_URL": "https://example.com/openapi.json" }
    }
  }
}

Composing a deployment

An index lists a deployment's service-level documents; createComposer merges them into one tool set per profile set, over one document cache:

import { createComposer } from '@data-fair/openapi-mcp'
import { createMcpHttpHandler } from '@data-fair/openapi-mcp/adapters/mcp'

const composer = await createComposer('https://host/data-fair/api/v1/agents/index.json', { locale: 'fr' })
composer.profiles()                       // every declared profile, expanded, with the index's wording
const c = await composer.compose(['explore', 'edit'])
c.toolSet.tools                           // each operation once, index order then document order
c.services                                // [{ id, status: 'ok' | 'skipped' | 'error', tools, reason? }]
setInterval(() => composer.refresh(), 300_000)   // conditional GETs; the library runs no timer

const handler = createMcpHttpHandler(composer, { name: 'my-server', version: '1.0.0' }, {
  context: (request) => ({ headers: { cookie: request?.headers.get('cookie') ?? '' }, identity: request?.headers.get('cookie') ?? undefined })
})

The index is the one document this library dictates:

{
  "version": 1,
  "services": [{ "id": "data-fair", "openapi": "https://host/data-fair/api/v1/api-docs.json" }],
  "profiles": { "full": { "title": "Full", "includes": ["explore", "edit"] } },
  "skills": [{ "name": "publishing-workflow", "description": "…", "profiles": ["edit"] }]
}

A failing service is excluded and reported; only a bad index throws. Tool names must be unique across services (namePrefix is how); on a collision the later service is excluded. A request naming a profile no service declares is an error, not an empty set.

A compatibility route composes a subset of services under an overridden prefix, e.g. composer.compose(['explore'], { services: ['data-fair'], namePrefix: '' }) to serve the names a caller already has. createMcpHttpHandler also takes a source chosen per request — createMcpHttpHandler((request) => …, info) — for one handler serving several such routes; publish change notifications yourself with handler.notify.toolsChanged(), since there is no single live source to subscribe to.

Identity per call

Tool.execute(params, ctx) takes a CallContextfetch, headers, signal, identity. The library never reads a cookie or holds a credential: a server derives the context from the request it serves, a page passes its own fetch, the binary relies on the environment. identity partitions json-layout editor sessions per caller.

Skills

x-agent.skills (and an index's skills) are served as resources through the official skills extension (io.modelcontextprotocol/skills): skills/list, skills/get, and resources/read of skill://<service-id>/<name>/SKILL.md, with a SHA-256 manifest. Skill names follow the Agent Skills format (^[a-z0-9]+(-[a-z0-9]+)*$). The same text still renders into instructions, so clients without the extension lose nothing.

Per-service golden

toolSetSnapshot(toolSet) is the agent-facing surface as plain data. A service's CI loads its own document with lint: 'error' for every declared profile and diffs the snapshot against a committed golden, so a change to a tool's name, description or schema is a reviewable diff in the pull request that caused it.

Editor tool groups

Annotate a write operation with editor and it becomes eight json-layout form tools — describeState, getData, setData, setFieldValue, getFieldSuggestions, editArray, saveForm, reloadForm — instead of one opaque body-submitting tool. A session per record loads the current document, validates every edit, and saves the whole document when it is valid.

put:
  operationId: updateLine
  x-agent:
    profiles: [write]
    name: dataset_line
    editor:
      schemaOperation: readSchema
      schemaParams: { mimeType: application/schema+json }
      readOperation: readLine

@json-layout/agents and @json-layout/core are optional peers, imported lazily and needed only when an editor annotation is present. See docs/editor-tool-groups.md for the annotation, the tool surface, schema preprocessing and limitations.

Large request bodies

A write operation's request body schema can be far too large to put in a tool definition — data-fair's dataset body is 28 KB, roughly twice its entire six-tool read set. Annotate the operation with body: compact and the tool exposes a single body property described by a compact listing (name, required, type, allowed values, a short description) instead of merging the schema's properties at top level.

The real schema still validates the body before a request is sent, so a caller that misreads the listing gets a JSON path and a keyword locally rather than a round trip. body: flat, the default, keeps the previous behaviour.

Annotation lint

load checks that a description an annotation authored still agrees with the schema it describes — a description promising a scalar for a parameter whose schema is an array gets the agent rejected before any request is sent. Descriptions inherited from the OpenAPI document are not linted: they belong upstream.

A disagreement refuses the tool set by default, listing every one at once. Pass lint: 'warn' to print and continue, or lint: 'off' to skip the check.

Coding agent skill

A skill that teaches coding agents to write and review x-agent annotations lives in skills/openapi-mcp. Install it into a project with:

npx skills add data-fair/openapi-mcp

Contributing

Development commands, the evaluation harness and the release process are in CONTRIBUTING.md.

License

AGPL-3.0-only

Keywords