Skip to content
v12.2
GitHub

MCP Server

AI agents such as Claude and Cursor call APIs through tools served over the Model Context Protocol (MCP). RapiDoc turns your OpenAPI spec into an MCP server, and curates it on the way, because exposing every endpoint one-to-one works poorly for agents: internal endpoints leak, transport headers become tool arguments the model has to guess, and every extra tool costs context on every turn.

RapiDoc reads your plain OpenAPI spec, leaves out internal and deprecated operations, strips transport and credential headers from tool arguments, and turns reference-data lookups into MCP resources. The result is an MCP manifest: plain JSON with one tool per operation, real JSON Schema arguments and MCP safety annotations. It ships in a small TypeScript project you can run as-is.

You can create it three ways:

  • In the browser: the Export MCP Server button of <rapi-doc> (or its JavaScript methods). Nothing is sent to a server.
  • On the command line: rapidoc mcp export and rapidoc mcp serve (@rapidoc/cli).
  • In Node: @rapidoc/mcp, for build scripts and your own servers.

No extra annotations are needed. Every tool and resource comes from standard OpenAPI fields:

  1. Tool names: The tool name is the snake_case form of the operationId (placeOrder becomes place_order). Operations without an operationId get a name derived from the HTTP method and path. Duplicate names get a _2, _3 suffix.
  2. Tool descriptions: The operation summary is used, else its description, else METHOD /path. HTML and Markdown are stripped and the text is truncated to 300 characters.
  3. Left out: Operations marked x-internal: true or x-hidden: true, operations marked deprecated: true, and webhooks are left out.
  4. Headers: Headers like Authorization, Cookie, X-Api-Key, User-Agent and other transport metadata are stripped from LLM tool arguments, as is any parameter that carries an API key named in the spec’s securitySchemes. The runtime applies the right scheme itself, from environment variables, and reads the base URL from servers[0] (overridable with API_BASE_URL).
  5. Resources: A GET with no required parameters whose path contains a reference-data word (/reference, /currencies, /countries, /tiers, /categories, /status, /schema) becomes an MCP resource instead of a tool. Its URI is reference:// and the tool name without get_, with dashes (getCurrencies becomes reference://currencies).
  6. Safety annotations: Every tool carries MCP annotations: readOnlyHint for GET only, destructiveHint for PUT, PATCH and DELETE, idempotentHint for GET, PUT and DELETE. Hosts use them to ask the user before running a consequential tool.
  7. Token budget: RapiDoc estimates how many tokens the tool list adds to every turn, and warns above 30 tools or 5,000 tokens.

Here is a plain OpenAPI 3.x snippet and how each operation is treated:

openapi: 3.0.3
info:
title: Commerce Fulfillment API
version: 2.4.0
servers:
- url: https://api.example.com/v2 # Base URL in the manifest
security:
- api_key: [] # Applied by the runtime from API_KEY
paths:
# 1. Tool "place_order" (POST: readOnlyHint false)
/orders:
post:
summary: Place a new order
operationId: placeOrder
parameters:
- name: Authorization # Stripped: never a tool argument
in: header
schema: { type: string }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customerId, items]
properties:
customerId: { type: string }
items: { type: array, items: { type: string } }
shippingMethod: { type: string, enum: [standard, express, overnight] }
# 2. Read-only tool "get_order"
/orders/{orderId}:
get:
summary: Get an order
operationId: getOrder
parameters:
- name: orderId
in: path
required: true
schema: { type: string }
# 3. MCP Resource "reference://currencies" (GET, no required params, reference-data path)
/currencies:
get:
summary: List supported currencies
operationId: getCurrencies
# 4. Excluded: internal endpoint
/admin/database/vacuum:
post:
summary: Trigger internal database maintenance
x-internal: true
# 5. Excluded: deprecated endpoint
/orders/legacy:
get:
summary: Legacy order list
deprecated: true
components:
securitySchemes:
api_key:
type: apiKey
in: header
name: X-API-Key

The export is data, not generated code. manifest.json describes the whole server:

  • server: name (snake_case of the mcp-server-name attribute or the API title), version and description from info.
  • sources: the API, named api, with the baseUrl from servers[0] (server variables filled with their defaults) and the spec’s securitySchemes copied as they are. An Arazzo document can add more sources.
  • tools: one per operation that survives the exclusions above.
  • resources: the reference-data lookups, with a reference:// URI.
  • workflows: one entry per Arazzo workflow when an Arazzo document is given (see Workflows as tools); otherwise empty.

Each tool has a name, a description, an inputSchema, MCP annotations and an operation block that tells the runtime how to make the HTTP call. The inputSchema is real JSON Schema: path, query, header and cookie parameters become properties by name, and the request body becomes a body property holding the full body schema, with nested objects, arrays, enums and required intact. $refs are inlined, cycles are cut, and read-only properties are dropped because they never go in a request.

The place_order tool from the example above:

{
"name": "place_order",
"description": "Place a new order",
"inputSchema": {
"type": "object",
"properties": {
"body": {
"type": "object",
"required": ["customerId", "items"],
"properties": {
"customerId": { "type": "string" },
"items": { "type": "array", "items": { "type": "string" } },
"shippingMethod": { "type": "string", "enum": ["standard", "express", "overnight"] }
}
}
},
"required": ["body"]
},
"annotations": { "readOnlyHint": false, "destructiveHint": false, "idempotentHint": false },
"operation": {
"source": "api",
"method": "POST",
"path": "/orders",
"params": [{ "name": "body", "in": "body", "contentType": "application/json" }],
"security": [{ "api_key": [] }]
}
}

operation.params records where each argument goes (path, query, header, cookie or body), and operation.security is the operation’s security requirement, else the spec’s. Because the manifest holds everything a server needs, servers in other languages can read the same manifest.json later without regenerating anything.


runtime.js executes any tool in the manifest with fetch, so the same file runs in Node, browsers and edge workers. createMcpRuntime(manifest, { fetch, credentials, baseUrls }) returns listTools(), listResources(), callTool(name, args) and readResource(uri). A tool call:

  1. Puts each argument where operation.params says, percent-encoding path parameters and serializing arrays as repeated query parameters.
  2. Applies the security scheme: an API key in a header, query parameter or cookie; HTTP Basic; a Bearer token for HTTP bearer, OAuth 2 and OpenID Connect.
  3. Sends the request and returns an MCP result: content with the JSON response pretty-printed, structuredContent when the response is a JSON object, and isError with the HTTP status and body on a 4xx or 5xx.

Export MCP Server downloads <server-name>-mcp.zip:

File Purpose
manifest.json The manifest described above
runtime.js The runtime for operation tools; identical for every API
workflow-runtime.js Runs workflow tools; present only when the manifest has workflows, identical for every API
server.ts A stdio server on @modelcontextprotocol/sdk that registers the manifest’s tools and resources and hands every call to the runtime; identical for every API
package.json Depends on @modelcontextprotocol/sdk; tsx as a dev dependency; npm start runs tsx server.ts
README.md How to run it and which environment variables it reads
claude_desktop_config.json A ready-made mcpServers entry for Claude Desktop and Cursor
unzip commerce-fulfillment-api-mcp.zip -d commerce-fulfillment-api-mcp
cd commerce-fulfillment-api-mcp
npm install
API_KEY="your-key" npm start

Credentials are read from environment variables named after the security schemes in the spec, in upper snake case: the scheme petstore_auth reads PETSTORE_AUTH, api_key reads API_KEY. API_KEY is also the fallback for any scheme without its own variable. HTTP Basic takes user:password in the scheme’s variable, or <NAME>_USERNAME and <NAME>_PASSWORD. API_BASE_URL overrides the manifest’s base URL for every source.

When the page also has a <rapi-flow> element tied to the <rapi-doc> (or you pass --arazzo to rapidoc mcp), the export adds one tool per Arazzo workflow. The manifest’s workflows holds each workflow’s steps with their operations resolved, and the runtime runs them: inputs become the tool’s arguments, every step’s parameters and body are filled from $inputs and earlier steps’ outputs, success criteria are checked (a step without criteria succeeds on a 2xx response), onSuccess / onFailure actions (end, goto, retry) are followed, and the workflow’s outputs are the result. The same runtime powers the Run button of <rapi-flow>, which runs a workflow in the browser with the credentials entered in the tied <rapi-doc> and lights up each step. A workflow that uses something the runtime does not run yet (a step that calls another workflow, regex / jsonpath / xpath criteria) is left out, and generateMcpServer().problems says why.

import { buildMcpManifest, createMcpRuntime, parseArazzo } from '@rapidoc/mcp';
const arazzo = parseArazzo(readFileSync('petstore.arazzo.yaml', 'utf8'));
const manifest = buildMcpManifest(spec, { arazzo, onProblem: console.warn });
const runtime = createMcpRuntime(manifest, { credentials: { api_key: process.env.API_KEY } });
const result = await runtime.callTool('adopt_pet', { quantity: 1 }, {
onStep: (step) => console.log(step.stepId, step.outcome, step.response?.status),
});

Set the mcp attribute:

<rapi-doc
spec-url="/specs/my-api.yaml"
mcp="true"
mcp-server-name="my-api-mcp"
> </rapi-doc>
  • mcp (default "false"): "true" shows an Export MCP Server button in the API overview that downloads the project as a zip. "false", "none" or an empty value hides it.
  • mcp-server-name (optional): the server’s name. Defaults to the spec’s title.

You can build the manifest, trigger the download, or check the token budget from code. Every method accepts an options object with serverName, includeInternal and includeDeprecated:

const doc = document.querySelector('rapi-doc');
// 1. Build the manifest, the project files and the token diagnostics in memory
const result = doc.generateMcpServer({ serverName: 'my_store_mcp' });
console.log(result.manifest.tools.map((t) => t.name)); // ['place_order', 'get_order', ...]
console.log(result.files['server.ts']); // every file of the zip, path → text
console.log(result.budget); // { totalTools: 5, estimatedTokens: 850, ... }
console.log(result.hostConfig); // Claude Desktop / Cursor config JSON
// 2. Download the project as <server-name>-mcp.zip
doc.downloadMcpServer();
// 3. Inspect token budget overhead before exporting
const budget = doc.getMcpBudget();
console.log(`Overhead: ~${budget.estimatedTokens} tokens/turn across ${budget.totalTools} tools.`);

result.files has the keys manifest.json, server.ts, runtime.js, package.json, README.md and claude_desktop_config.json, plus workflow-runtime.js when there are workflows. getMcpBudget() returns { totalTools, totalResources, estimatedTokens, consequentialCount, warnings }, where consequentialCount is the number of tools that are not read-only.

npx rapidoc mcp export openapi.yaml writes the same project as the Export button, and npx rapidoc mcp serve openapi.yaml runs the server over stdio without writing files (Command Line).

The same builder, runtime and project writer are available without the browser or any element, for build scripts and servers:

npm install @rapidoc/mcp
import { readFileSync } from 'node:fs';
import { parse } from 'yaml';
import { buildMcpManifest, createMcpRuntime, writeMcpProject } from '@rapidoc/mcp';
const spec = parse(readFileSync('openapi.yaml', 'utf8'));
const manifest = buildMcpManifest(spec, { serverName: 'petstore' });
// Call a tool directly, with credentials by security scheme
const runtime = createMcpRuntime(manifest, { credentials: { api_key: process.env.API_KEY } });
const result = await runtime.callTool('get_pet_by_id', { petId: 1 });
// Or write the same project the Export button downloads
const files = writeMcpProject(manifest); // path → text

@rapidoc/mcp ships TypeScript types (McpManifest, McpRuntime, …). Its only dependency is yaml (for Arazzo documents); requests use Node’s fetch.


Every exported project includes a ready-to-use configuration for Claude Desktop and Cursor, with one env entry per security scheme in the spec (API_KEY alone when the spec declares none):

Copy the generated snippet to your claude_desktop_config.json, and run the host from the unzipped folder or make the server.ts path absolute:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

For the Swagger Petstore, which declares the schemes api_key and petstore_auth:

{
"mcpServers": {
"petstore_mcp": {
"command": "npx",
"args": ["-y", "tsx", "server.ts"],
"env": {
"API_KEY": "your-api-key",
"PETSTORE_AUTH": "your-petstore-auth"
}
}
}
}