Skip to content

OpenAPI to MCP: How MCP Servers Auto-Generate Tools From API Docs

Learn how MCP servers auto-generate tools from API documentation. A technical walkthrough of converting OpenAPI specs into LLM-ready JSON-RPC tools.

Riya Sethi Riya Sethi · · 13 min read
OpenAPI to MCP: How MCP Servers Auto-Generate Tools From API Docs

If you point an MCP client at a documentation-driven MCP server, it receives a list of ready-to-call tools without anyone hand-writing a single JSON Schema. That magic trick has a boring explanation: the server walks a structured API description (an OpenAPI document, a curated resource registry, or something similar), emits one JSON-RPC 2.0 tool per operation, flattens path, query, and body parameters into a single input schema, and enhances descriptions so a probabilistic caller can actually use them. The interesting engineering is in the parts that break.

If you are building AI agents that interact with external SaaS platforms, you have likely hit a wall. Manually writing and maintaining JSON Schema tool definitions for every API endpoint across dozens of providers is an unscalable architecture. Vendor APIs change constantly. Endpoints are deprecated, required fields shift, and documentation often lies. Hardcoding your AI agent's tools against these moving targets guarantees your engineering team will spend their cycles patching broken schemas instead of improving agent reasoning.

This is not a hypothetical problem. Forty percent of enterprise applications will be integrated with task-specific AI agents by the end of 2026, up from less than 5% today, according to Gartner Inc. Every one of those agents will be asking your API for tools. Your customers expect your platform to expose its integrations to their agents immediately. If your integration layer cannot produce clean MCP tools from documentation on demand, you will spend the next 18 months writing them by hand.

This guide provides a concrete, architectural walkthrough of how MCP servers auto-generate tools from API documentation. It covers the OpenAPI-to-MCP translation, the parameter collisions everyone hits, schema hygiene for LLM comprehension, how to keep tool counts under the threshold where model accuracy collapses, and how to hand off authentication and rate limits to a runtime layer so the MCP server stays stateless.

Why a Naive OpenAPI to MCP Dump Fails

The immediate instinct for most engineering teams is to take a vendor's OpenAPI specification, run it through a script, and expose every single endpoint as an MCP tool. This naive 1:1 mapping fails in production immediately.

MCP tools use JSON Schema, and OpenAPI uses JSON Schema, but the semantics do not line up. Copying operations verbatim produces schemas that break the LLM, the router, or both. The basic mapping is mostly clean: path becomes tool name, parameters become input schema, success response becomes output schema. Vendors that ship OpenAPI-to-MCP generators all follow that skeleton. The differences show up in the lossy cases.

Three things go wrong when you dump an entire spec into an MCP server:

  1. Tool explosion. A REST API with 200 endpoints becomes 200 tools. LLMs struggle with tool selection accuracy when presented with more than 25 to 50 tools at once. Frontier models degrade sharply once the tool list crosses a few dozen entries because every tool description competes for the same context budget and the router has to disambiguate more candidates per call. Ship all 200, dump an entire spec into an MCP server, and the agent starts guessing while wasting massive amounts of valuable context window space.
  2. Parameter collisions. OpenAPI separates parameters into different locations: path, query, header, and body. MCP tools need all these combined into a single schema. You have to combine the request body, path parameters, query parameters, and header parameters all into one schema, and handle any naming collisions automatically. A path parameter called id, a query parameter also called id, and a body parameter with an id field all land in the same flat namespace at call time.
  3. Lossy response contracts. Only the success response schema (2xx) maps to the MCP outputSchema. Error responses (4xx, 5xx) are returned to the agent as tool errors, not as alternative output shapes - usually the right thing, occasionally not. If your agent relied on a typed 422 to branch, that logic has to move to the tool caller.

That is before you get to $ref cycles, multipart/form-data, polymorphic oneOf bodies, and pagination shapes that only make sense if you read the SDK. To build a production-grade MCP server, tool generation must be dynamic, filtered, and documentation-driven. Rather than exposing everything, the system should act as a quality gate.

Step-by-Step: An OpenAPI to MCP Mapping Example

Let us look at exactly how an OpenAPI operation translates into an executable MCP tool. We will examine both a read operation and a write operation to see how different parameters are handled.

The Source: OpenAPI Operations

Start with a standard OpenAPI read operation. A GET /contacts/{id} endpoint on a CRM:

paths:
  /contacts/{id}:
    get:
      operationId: getContact
      summary: Retrieve a contact by ID
      tags: [crm, contacts]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: include_deleted
          in: query
          schema: { type: boolean, default: false }
      responses:
        '200':
          description: Contact record
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Contact' }

Now consider a POST /contacts endpoint that includes query and body parameters:

paths:
  /contacts:
    post:
      summary: Create a new contact
      operationId: createContact
      parameters:
        - name: include_associations
          in: query
          schema:
            type: boolean
          description: Return associated company data
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - email
              properties:
                email:
                  type: string
                  format: email
                first_name:
                  type: string

The Output: MCP Tool Definitions

When an MCP client calls tools/list, a documentation-driven MCP server converts these into single JSON-RPC tools. The transformation process involves extracting the summary, generating a descriptive name, and combining the parameters.

Here is the generated tool for the GET operation:

{
  "name": "get_single_hubspot_contact_by_id",
  "description": "Retrieve a contact by ID.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "id": {
        "type": "string",
        "description": "The id of the contact to get. Required."
      },
      "include_deleted": {
        "type": "boolean",
        "description": "Include soft-deleted contacts.",
        "default": false
      }
    },
    "required": ["id"]
  }
}

And here is the generated tool for the POST operation:

{
  "name": "create_a_crm_contact",
  "description": "Create a new contact",
  "inputSchema": {
    "type": "object",
    "properties": {
      "include_associations": {
        "type": "boolean",
        "description": "Return associated company data"
      },
      "email": {
        "type": "string",
        "description": "The contact's email address"
      },
      "first_name": {
        "type": "string",
        "description": "The contact's first name"
      }
    },
    "required": ["email"]
  }
}

A few architectural decisions and mechanics worth calling out:

  • Snake_case names. LLMs handle get_single_hubspot_contact_by_id or create_a_crm_contact more reliably than getContact. The tool name doubles as a strong semantic hint about the operation. Names derived from the provider label plus resource plus method survive tokenization better and reduce hallucinated tool calls.
  • Description sourcing. Prefer summary, fall back to description, then to a synthesized "{method} {path}". Empty descriptions are the single fastest way to get an agent to call the wrong tool.
  • $ref dereferencing. MCP Tool schemas must be completely self-contained, meaning they cannot reference anything outside themselves. This requires "dereferencing" - a process of resolving all references and inlining them into a single schema. Recursive references (Comment.replies -> Comment) need cycle detection or the resolver never returns.
  • operationId is optional in practice. Many vendor specs omit or duplicate it. Deriving a name from (provider, resource, method) is more reliable than trusting operationId.

Documentation-driven servers add one more gate: a tool only shows up if a curated documentation record exists for that resource and method. Raw OpenAPI dumps skip this and expose every deprecated endpoint the vendor forgot to remove. For a longer treatment of the end-to-end pipeline, see the OpenAPI to MCP architecture guide.

Flattening Path, Query, and Body Into One Input Schema

If you look closely at the JSON outputs above, you will notice a critical architectural difference between HTTP and MCP. OpenAPI separates arguments by location: path parameters, query parameters, and request bodies. MCP does not. MCP requires a single, flat JSON Schema for all arguments. When the LLM calls the tool, it passes a single flat object.

The merge is straightforward when names do not collide:

const inputSchema = {
  type: 'object',
  properties: {
    ...pathParams,
    ...queryParams,
    ...bodyProperties,
  },
  required: [
    ...pathRequired,
    ...queryRequired,
    ...bodyRequired,
  ],
}

This creates a namespace collision problem. What happens if an OpenAPI spec defines an id parameter in the path (the ID of the account) and an id field in the request body (the ID of the user being assigned)? When they do collide, you have three options:

  1. Prefix. Rename body.id to body_id. Safe but forces the LLM to learn a synthetic vocabulary that does not appear anywhere in the vendor docs.
  2. Location priority. Pick a winner (usually path wins over query wins over body) and drop the losers. Simple, and in practice the losing fields are almost always the same value the caller would have provided anyway.
  3. Two-schema split. Keep query and body as separate sub-schemas, and let the runtime router decide which arguments go where based on property membership.

Option 3 is what documentation-driven servers tend to pick, because it preserves the exact semantics of the vendor API.

How the proxy layer handles flat inputs

When the LLM executes the tool via tools/call, it sends the flat argument object. The MCP server's proxy layer must dynamically split these arguments back into their HTTP-specific locations before making the upstream request.

  1. The proxy inspects the original OpenAPI query schema and body schema.
  2. It iterates through the flat LLM arguments.
  3. If an argument key exists in the query schema, it routes it to the HTTP query string.
  4. If it exists in the body schema, it routes it to the HTTP JSON body.

Every MCP integration guide converges on the same practical advice on this point. When converting OpenAPI specs to MCP tools, you hit parameter conflicts. You need an explicit mapper for building HTTP requests. Without a persisted parameter-location map, you cannot reconstruct the HTTP request from the LLM's flat argument object.

Enhancing Schemas So the LLM Actually Uses Them Correctly

The raw JSON Schema from an OpenAPI spec is written for humans who read documentation and machines that validate payloads. LLMs are neither. They read schemas as prompts. A description field is not metadata to them; it is the instruction.

Extracting JSON Schema from OpenAPI is only the baseline. To make the tools reliable, you must actively enhance the schemas with instructions designed specifically for LLMs. Four enhancements matter more than any others:

1. Inject required-ID hints on individual methods. For get, update, and delete operations, the path parameter is the identifier. The description should say so explicitly:

{
  "id": {
    "type": "string",
    "description": "The id of the contact to get. Required."
  }
}

2. Add pagination controls with cursor discipline. LLMs do not intuitively understand API pagination mechanics. If an endpoint uses cursor-based pagination, the OpenAPI spec might just define a next_cursor string parameter. If you pass that raw schema to an LLM, it might try to guess a cursor value or hallucinate a page number. For list methods, inject limit and next_cursor fields with descriptions that force pass-through behavior:

{
  "limit": {
    "type": "string",
    "description": "The number of records to fetch"
  },
  "next_cursor": {
    "type": "string",
    "description": "The cursor to fetch the next page. Always send back exactly the cursor value you received (nextCursor) without decoding, modifying, or parsing it. This can be found in the response of the previous tool invocation."
  }
}

Without the "do not decode" instruction, models will happily strip base64 padding, URL-decode opaque tokens, or invent structured cursor formats. This one description eliminates an entire class of pagination bugs.

3. Normalize required fields. OpenAPI lets you sprinkle required: true inside individual property definitions, but MCP clients expect the standard JSON Schema required: [] array at the parent object. A tree walk that collects required flags and hoists them to the correct level is a small piece of code that fixes 80% of tool-call validation errors.

4. Explicit Error Response Handling. OpenAPI error responses (4xx, 5xx) do not naturally map to MCP output schemas. The MCP protocol expects tools to return a standard result object. If the upstream API fails, the proxy layer must catch the HTTP error and return it to the LLM wrapped in the MCP error format, setting isError: true so the agent knows to adjust its strategy.

Preventing Context Bloat With Tag-Based Tool Filtering

Every tool description you expose to an LLM costs tokens on every request. A 200-endpoint API shipped as 200 MCP tools burns 15-30k tokens before the model has read the user's message. Context bloat is not a rounding error; it is the dominant cost driver for agent workloads.

Because LLMs degrade when exposed to more than 50 tools, you cannot serve a massive API surface on a single MCP endpoint. You must implement filtering. The fix is tool scoping. Group endpoints by functional area and let the MCP server owner select which groups to expose:

flowchart LR
  A["OpenAPI Spec<br>200 operations"] --> B["Tag Grouping"]
  B --> C["crm: 40 tools"]
  B --> D["support: 25 tools"]
  B --> E["directory: 15 tools"]
  B --> F["billing: 30 tools"]
  C --> G["MCP Server A<br>tags: crm"]
  D --> H["MCP Server B<br>tags: support, directory"]

OpenAPI already gives you the primitive: the tags field on each operation. A documentation-driven MCP server layers a second signal on top - a mapping from resource name to tag groups defined in integration config:

{
  tool_tags: {
    contacts: ['crm', 'sales'],
    deals: ['crm', 'sales'],
    tickets: ['support'],
    ticket_comments: ['support'],
    users: ['directory'],
    organizations: ['directory', 'support'],
  }
}

When a client initializes the MCP server, it requests a specific configuration:

{
  "name": "Support-only MCP",
  "config": { "tags": ["support"] }
}

At runtime, when the LLM requests tools/list, the server dynamically filters the documentation records. Only resources whose tags intersect the filter set produce tools. A "support-only" server built from the config above exposes tools for tickets, ticket_comments, and organizations, and nothing else.

Method-level filters compound this. Restricting a server to read operations (get, list), or write operations (create, update, delete), or custom operations (search, download, import), collapses tool counts further. Combined tag and method filters routinely cut a 200-tool provider dump down to 12-30 relevant tools per server, which is the sweet spot where models stop hallucinating tool names and context windows are preserved.

A useful validation gate: refuse to create an MCP server whose filter combination produces zero tools, and return the available tags and methods in the error message. This catches copy-paste config bugs before they hit production.

Handling Authentication and Rate Limits at Runtime

MCP is a transport protocol. It defines how tools are listed and called over JSON-RPC, but it is entirely stateless. It does not carry OAuth tokens, it does not know about rate limit headers, and it does not manage credential refresh or backoff queues. When an LLM calls a generated tool, the MCP server must delegate the actual API execution to a proxy layer.

The Execution Flow

Here is how the architecture handles a tool call securely:

sequenceDiagram
    participant Agent as LLM Agent
    participant MCPServer as MCP Server
    participant Proxy as Proxy API Layer
    participant Upstream as Upstream SaaS API

    Agent->>MCPServer: tools/call (flat arguments)
    MCPServer->>MCPServer: Validate token & split arguments
    MCPServer->>Proxy: Execute request (query + body)
    Proxy->>Proxy: Inject OAuth Bearer Token
    Proxy->>Upstream: HTTP POST /contacts
    Upstream-->>Proxy: HTTP 201 Created
    Proxy-->>MCPServer: Parsed JSON response
    MCPServer-->>Agent: MCP Result (isError: false)

The pattern that works is: the MCP tool call arrives, the server looks up the connected account, injects the current OAuth token (refreshed transparently ahead of expiry), makes the upstream call, and returns the response wrapped in MCP's content envelope.

Managing Rate Limits

When generating tools from API documentation, developers often wonder how to handle rate limits. The answer is radical honesty: you pass them back to the caller.

If the upstream API returns an HTTP 429 Too Many Requests, the proxy layer should not silently absorb it or attempt infinite retries. Doing so traps the LLM in a pending state, burning execution time. Retry, backoff, and circuit breaker logic live in the agent, not the integration layer. The caller has request context (was this a user-initiated action or a background sweep?) that the integration layer cannot see, and picking a backoff strategy without that context produces bad behavior.

Instead, the proxy normalizes the upstream rate limit headers into standardized IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset) and returns the 429 error directly to the MCP server. The server then formats this as a tool error:

{
  "isError": true,
  "content": [
    {
      "type": "text",
      "text": "HTTP 429: Rate limit exceeded. Reset in 45 seconds. Do not retry immediately."
    }
  ]
}

Passing the 429 up with real headers lets the caller (the LLM or its orchestration framework) read the error, apply exponential backoff, and schedule the retry.

Auth Refresh and the Response Envelope

Auth refresh is the mirror-image concern. OAuth tokens expire, and refresh has to happen without the LLM being aware. Documentation-driven MCP servers typically schedule refreshes ahead of TTL and treat needs_reauth as a hard failure that surfaces as a tool error rather than a silent retry loop.

The response envelope itself is small but strict:

{
  "jsonrpc": "2.0",
  "id": 42,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"result\": {...}, \"next_cursor\": \"...\", \"request_id\": \"...\"}"
    }]
  }
}

On failure, add isError: true to the result content. Do not throw JSON-RPC errors for upstream failures; MCP clients handle content-level errors more gracefully than protocol-level ones.

Strategic Next Steps

Auto-generating MCP tools from OpenAPI specifications is not a simple parsing exercise. It is a solved problem in the trivial case and a hard problem at production quality. The mapping algorithm (path + method to tool name, merged parameters to input schema, 2xx response to output schema) is table stakes. The engineering budget goes into schema hygiene, flattening namespaces, implementing tag-based filtering to protect context windows, and building a robust proxy layer to handle the brutal realities of OAuth and rate limits.

If you are building this in-house, budget for the long tail: recursive $ref cycles, oneOf bodies, multipart uploads, per-vendor pagination shapes, and the description-quality problem (LLMs are only as good as the descriptions you feed them). If you are buying it, verify the vendor exposes tag and method filters, splits flat MCP arguments back into path/query/body correctly, and does not silently absorb rate limit errors.

If you are building AI agents that need to connect to enterprise SaaS platforms, do not build this generation pipeline from scratch.

FAQ

How do MCP servers auto-generate tools from API documentation?
They parse a structured API description (like an OpenAPI document or curated resource registry), emit one JSON-RPC 2.0 tool per operation, dereference all $ref pointers, flatten path/query/body parameters into a single input schema, and enhance descriptions so an LLM can call the tool correctly.
Why does dumping a full OpenAPI spec into MCP degrade agent accuracy?
Tool descriptions cost tokens on every request, and LLMs get worse at tool selection as the candidate list grows past a few dozen. A 200-endpoint spec produces 200 tools, burns tens of thousands of tokens per call, and forces the router to disambiguate more candidates. Tag and method filters cut this to a more manageable 10-30 tools per server.
How do you handle OpenAPI parameter collisions when mapping to MCP?
MCP tools take a single flat input object, so a path `id`, query `id`, and body `id` all collide in one namespace. Common fixes are prefixing (`body_id`), location priority (path wins over query wins over body), or persisting a parameter-location map so the runtime router can split the flat arguments back into the correct HTTP locations.
How do MCP tools handle OpenAPI error responses?
Only 2xx success responses naturally map to the MCP output schema. Non-2xx responses are returned to the agent as tool errors (`isError: true` in the result content), not as alternative output shapes. Agents that need to branch on typed error codes have to inspect the error content string rather than a typed schema.
Who handles rate limits and OAuth in an auto-generated MCP server?
MCP itself is stateless and handles neither. A proxy layer beneath the MCP server refreshes OAuth tokens ahead of expiry and passes HTTP 429 responses (with standardized `ratelimit-*` headers) back to the caller. The agent owns retry and backoff because only it has the request context needed to pick the right strategy.

More from our Blog