---
title: "OpenAPI to MCP: How MCP Servers Auto-Generate Tools From API Docs"
slug: openapi-to-mcp-how-mcp-servers-auto-generate-tools-from-api-docs
date: 2026-08-31
author: Riya Sethi
categories: ["AI & Agents", Engineering, By Example]
excerpt: Learn how MCP servers auto-generate tools from API documentation. A technical walkthrough of converting OpenAPI specs into LLM-ready JSON-RPC tools.
tldr: "Naively converting OpenAPI specs to MCP tools causes context bloat and parameter collisions. You must flatten namespaces, enhance schemas for LLMs, and filter tools by tags."
canonical: https://truto.one/blog/openapi-to-mcp-how-mcp-servers-auto-generate-tools-from-api-docs/
---

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


If you point an MCP client at a [documentation-driven MCP server](https://truto.one/auto-generated-mcp-tools-for-ai-agents-a-2026-architecture-guide/), 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](https://truto.one/how-do-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:

```yaml
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:

```yaml
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:

```json
{
  "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:

```json
{
  "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](https://truto.one/auto-generating-mcp-tools-from-openapi-specs-an-end-to-end-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:

```typescript
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:

```json
{
  "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:

```json
{
  "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:

```mermaid
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:

```typescript
{
  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:

```json
{
  "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:

```mermaid
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:

```json
{
  "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:

```json
{
  "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.

> Stop writing manual JSON schemas for every API endpoint. Truto provides unified APIs and dynamically [generates production-ready MCP servers for your SaaS users](https://truto.one/how-to-generate-mcp-servers-for-your-saas-users-2026-architecture-guide/) from your integration documentation, with tag-based filtering, auth refresh, and pass-through rate limits built in. Partner with us to scale your AI agent's connectivity today.
>
> [Talk to us](https://cal.com/truto/partner-with-truto)
