Shaping Curated MCP Servers
How RapiDoc applies smart heuristics to a plain OpenAPI spec to cut prompt token tax, keep internal endpoints away from agents, and build an MCP manifest with a runnable TypeScript server directly in the browser.
Why Naive 1:1 REST-to-MCP Fails
When teams first connect OpenAPI to AI agents, their initial instinct is to convert every REST endpoint into an individual MCP tool. As Jeremiah Lowin explains in "Stop Converting Your REST APIs to MCP", this naive approach severely degrades LLM performance:
Endpoint-as-a-Tool Trap
- Token Window Bloat: 30–50 raw JSON schemas consume 4,000–8,000 tokens on every conversation turn.
- Internal Leaks: Infrastructure endpoints (
/healthz,/metrics,/admin/debug) confuse the model. - Transport Pollution: Headers like
Authorization,Cookie, andX-Api-Keypollute tool parameters.
Agent-First Tool Design
- Lean Token Footprint: Reference lookups become MCP Resources, and a live token budget warns before the tool catalog saturates the context.
- Safety Annotations: every tool carries MCP
readOnlyHint,destructiveHintandidempotentHint, so hosts can ask before aPOST,PUT,PATCHorDELETE. - Zero Admin Leaks: Automatic filtering via
x-internal,x-hidden, anddeprecated: true. - Clean Auth Separation: Transport headers are stripped; the runtime applies the spec's
securitySchemesfrom environment variables.
Key Performance Indicators (KPIs)
Unlike generic converters that blindly dump hundreds of endpoints into an agent's context, RapiDoc is engineered to meet strict, quantifiable enterprise production benchmarks:
Context Overhead Control
Skips internal, hidden, deprecated and webhook operations, strips transport headers and moves reference lookups into resources. getMcpBudget() estimates the per-turn overhead and warns above 5,000 tokens or 30 tools.
Auth Stays Out of the Model's Hands
Auth headers and API-key parameters are never tool arguments. The runtime applies the spec's securitySchemes from environment variables and percent-encodes every path parameter it substitutes.
One Manifest Drives Everything
manifest.json holds every tool with real JSON Schema arguments and MCP annotations. runtime.js executes it with fetch in Node, browsers and edge workers, and server.ts is the same file for every API.
Competitive KPI Matrix: RapiDoc vs. Generic MCP Generators
How RapiDoc stacks up against raw 1:1 endpoint wrappers and generic cloud CLI tools:
| Performance Dimension / KPI | Generic 1:1 Converters | Generic Cloud CLI Generators | RapiDoc Curated Generator |
|---|---|---|---|
| Internal & Deprecated Endpoints | Exposed as tools | Partially filtered | Skips x-internal, x-hidden, deprecated and webhooks |
| Output | Generated code per API | Hosted proxy or heavy CLI | One JSON manifest, one fixed runtime, a runnable TypeScript project |
| Tool Arguments | Flat string bags | Partial schemas | Real JSON Schema: nested bodies, arrays, enums, required |
| Safety Annotations | None | None | MCP readOnlyHint, destructiveHint, idempotentHint |
| Authentication | Keys as tool arguments | Hardcoded static credentials | From securitySchemes, via environment variables |
| Backend / Cloud Dependency | External Node server required | Paid cloud SaaS or heavy CLI | Zero Backend (100% In-Browser & Air-Gapped) |
| Real-Time Token Budget Warnings | None (Blind to token saturation) | None | Built-in turn budget & token saturation guards |
Smart Heuristics (Out of the Box)
RapiDoc needs no extra annotations. It derives every tool and resource from standard OpenAPI fields during export:
Tool Names & Descriptions
Tool names are the snake_case form of each operationId, or derived from method and path when it is missing. Descriptions come from the summary, else the description, else METHOD /path.
Header & Transport Sanitization
Headers like Authorization, Cookie, X-Api-Key, User-Agent, and Accept are stripped from LLM parameter schemas, as is any parameter that carries an API key named in securitySchemes. The runtime applies the scheme itself, from an environment variable named after it, against the base URL from servers[0] (or API_BASE_URL).
Internal & Deprecated Route Exclusion
Operations marked x-internal: true, x-hidden: true or deprecated: true, and webhooks, are omitted from generated agent tools so models never see admin or stale APIs.
Static Resource Classification
A GET with no required parameters on a reference-data path (/reference, /currencies, /countries, /tiers, /categories, /status, /schema) becomes an MCP Resource (reference://<name>), preserving tool calling budgets.
Safety Annotations
Every tool carries MCP annotations: readOnlyHint for GET, destructiveHint for PUT, PATCH and DELETE, idempotentHint for GET, PUT and DELETE, so agent hosts can ask the user before running a consequential tool.
Token Budget Diagnostics
RapiDoc dynamically estimates prompt context tokens per conversation turn and generates warnings if tool count exceeds 30 or context exceeds 5,000 tokens.
From OpenAPI to MCP
In the Model Context Protocol, Tools represent executable actions, while Resourcesrepresent contextual reference data the model can inspect without function calling. Here is how each operation in a plain spec is treated:
# Plain OpenAPI: no extra annotations needed
security:
- api_key: [] # Applied by the runtime from API_KEY
paths:
/orders:
post:
operationId: placeOrder # Tool "place_order"
summary: Place a new order # Tool description
parameters:
- name: Authorization # Stripped: never a tool argument
in: header
schema: { type: string }
requestBody: # Tool argument "body", full JSON Schema
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/NewOrder" }
# POST: readOnlyHint false, so hosts can ask before running it
/orders/{orderId}:
get:
operationId: getOrder # Read-only tool "get_order"
summary: Get an order
/currencies:
get:
operationId: getCurrencies # Resource "reference://currencies"
summary: List supported currencies
/admin/database/vacuum:
post:
summary: Trigger internal database maintenance
x-internal: true # Excluded
/orders/legacy:
get:
summary: Legacy order list
deprecated: true # Excluded
components:
securitySchemes:
api_key: # Copied into the manifest's sources
type: apiKey
in: header
name: X-API-KeyOpenAPI Field Reference
| OpenAPI Field | Applies To | Effect on the Manifest |
|---|---|---|
operationId | Operation | Tool name in snake_case (placeOrder → place_order); derived from method and path when missing. |
summary / description | Operation | Tool description (summary first), stripped of HTML and Markdown and truncated to 300 characters. |
x-internal / x-hidden | Operation | When true, the operation is left out. |
deprecated | Operation | When true, the operation is left out. |
| HTTP method | Operation | Sets the tool's annotations: readOnlyHint for GET, destructiveHint for PUT, PATCH and DELETE, idempotentHint for GET, PUT and DELETE. |
Parameters and requestBody | Operation | The tool's inputSchema: path, query, header and cookie parameters by name, plus a body property with the request body's full schema ($refs inlined, read-only properties dropped). |
| Path | Operation | A parameterless GET on a reference-data path becomes a reference:// resource. |
| Header parameters | Parameter | Auth and transport headers (Authorization, Cookie, X-API-Key, User-Agent, ...) are not exposed as tool arguments. |
servers | Root Spec | Default base URL, overridable with the API_BASE_URL environment variable. |
securitySchemes / security | Root Spec / Operation | Copied into the manifest; the runtime reads each scheme's credential from an environment variable named after it (petstore_auth → PETSTORE_AUTH). |
The Manifest
The export is a plain JSON manifest.json, not generated code. It holds the server (name, version, description), one entry in sources (the base URL from servers[0] and the spec's securitySchemes), the tools, the resources and the workflows of a tied <rapi-flow>, each one a tool of its own. Each operation tool has a real JSON SchemainputSchema, MCP annotations and an operation block that tells the runtime where every argument goes:
{
"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": [] }]
}
}Real JSON Schema arguments
Path, query, header and cookie parameters by name, plus a body property with the request body's full schema: nested objects, arrays, enums and required. $refs are inlined, cycles cut, read-only properties dropped.
MCP safety hints
readOnlyHint for GET, destructiveHint for PUT, PATCH and DELETE, idempotentHint for GET, PUT and DELETE.
How to make the call
source, method, path, params with each argument's location, and the operation's security requirement (else the spec's).
One Runtime, One TypeScript Project
Export MCP Server downloads <server-name>-mcp.zip: the manifest plus files that are identical for every API. Run it with npm install and npm start, or copy the mcpServers entry from claude_desktop_config.json into Claude Desktop or Cursor:
# The zip: manifest.json, runtime.js, server.ts, package.json, README.md, claude_desktop_config.json
# (and workflow-runtime.js when the page has a <rapi-flow> with workflows)
unzip my-api-mcp.zip -d my-api-mcp
cd my-api-mcp
npm install
# One environment variable per security scheme (upper snake case); API_KEY is the fallback
API_KEY="your-key" npm start # runs: tsx server.ts
# Point the server at another environment
API_BASE_URL="https://staging.example.com/v2" API_KEY="your-key" npm start1. Executes any tool with fetch
createMcpRuntime(manifest, { fetch, credentials, baseUrls }) puts each argument where operation.params says, percent-encodes path parameters, applies the security scheme (API key in header, query or cookie; HTTP Basic; Bearer tokens for HTTP bearer, OAuth 2 and OpenID Connect) and returns the MCP result: pretty-printed content, structuredContent for JSON objects, isError with the HTTP status and body on 4xx and 5xx. The same file runs in Node, browsers and edge workers.
2. A fixed stdio server
Built on @modelcontextprotocol/sdk, it reads manifest.json, registers its tools and resources and hands every call to the runtime. It is the same file for every API, so there is nothing API-specific to review. package.json depends on @modelcontextprotocol/sdk and tsx.
3. Credentials by security scheme
Each scheme reads an environment variable named after it in upper snake case (petstore_auth → PETSTORE_AUTH), with API_KEY as the fallback for any scheme. HTTP Basic takes user:password or <NAME>_USERNAME / <NAME>_PASSWORD. API_BASE_URL overrides the base URL. The Claude Desktop and Cursor config has one env entry per scheme.
4. Other languages read the same manifest
Only TypeScript is exported today. Because the manifest holds everything a server needs, a server in another language is a fixed file reading the same manifest.json, not another code generator.
Using MCP in Your Documentation
Enable the MCP generator in your documentation portal with a single attribute, or trigger it programmatically:
Declarative HTML Setup
<!-- 1. Enable the Export MCP Server button in the API overview -->
<script type="module" src="https://unpkg.com/rapidoc/dist/rapidoc-min.js"></script>
<rapi-doc
spec-url="/specs/my-api.yaml"
mcp="true"
mcp-server-name="my-api-mcp"
theme="slate"
> </rapi-doc>Programmatic JavaScript API
// Access the RapiDoc custom element in JavaScript
const doc = document.querySelector('rapi-doc');
// 1. Build the manifest, the project files and the host configuration in memory
const result = doc.generateMcpServer({ serverName: 'my_api_mcp' });
console.log(result.manifest.tools); // [{ name, description, inputSchema, annotations, operation }]
console.log(result.files['server.ts']); // also manifest.json, runtime.js, package.json, README.md, ...
console.log(result.hostConfig); // Claude Desktop / Cursor JSON config
console.log(result.budget); // { totalTools: 5, estimatedTokens: 850, ... }
// 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`);Ready to test the Curated MCP Generator?
Explore our live interactive demo with side-by-side token diagnostics, tuner options, and the exported manifest, server and runtime to inspect.