Skip to content

Auto-Generating MCP Tools from OpenAPI Specs: An End-to-End Architecture Guide

A concrete engineering walkthrough for converting OpenAPI specs into MCP tool JSON, covering schema enhancement, tag-based filtering, and rate-limit handling.

Nachi Raman Nachi Raman · · 14 min read
Auto-Generating MCP Tools from OpenAPI Specs: An End-to-End Architecture Guide

Building AI agents that interact with external SaaS platforms requires a strict, enforceable contract between the LLM and the API. The Model Context Protocol (MCP) provides the transport layer, but you still have to define the tools. For engineering teams, the immediate instinct is to take a vendor's OpenAPI specification, run it through a script, and expose every endpoint as an MCP tool.

Auto-generating MCP tools from an OpenAPI specification looks trivial on paper: parse the spec, walk each operation, emit a JSON Schema, and register it as a JSON-RPC 2.0 tool. It is not trivial. A naive 1:1 mapping almost always fails in production, producing an unusable server that either overflows the LLM's context window, mis-routes tool calls, or silently corrupts request payloads because the schemas were copied verbatim from a Swagger file that was never designed for a probabilistic caller.

Providing an end-to-end example of auto-generating tools from OpenAPI documentation to MCP tool JSON requires more than just parsing YAML. You have to account for LLM context window limits, hallucinated parameters, polymorphic schemas (oneOf, anyOf), and the brutal realities of runtime authentication and rate limiting.

This guide provides a deep, architectural walkthrough of converting OpenAPI specifications into production-ready MCP tools. We will cover how to parse the specs, enhance the schemas specifically for LLM comprehension, implement tag-based filtering to prevent context bloat, and handle API execution at runtime.

The Promise and Peril of OpenAPI to MCP Tool JSON

The appeal of auto-generating MCP tools from OpenAPI is obvious. Maintaining hand-coded JSON Schema tool definitions for every endpoint across dozens of integrations is an unscalable architecture. Vendor APIs change constantly. Endpoints are deprecated, required fields shift, and documentation often lies. If you already publish an OpenAPI document, the temptation is to point a generator at it and get an MCP server for free.

The problem is that OpenAPI documents describe an API's surface; MCP tools describe an agent's choices. The two are not the same shape.

The first wall you hit is tool explosion. A 200-endpoint OpenAPI document generates roughly 200 MCP tools when auto-converted. That same conversion can push 40,000-80,000 tokens of schema into context. According to developers using frameworks like FastMCP, dumping massive schemas wastes context slots, and models hit accuracy limits well before they reach hard limits. When an LLM is presented with 200 unfiltered CRM endpoints, it struggles to select the correct tool and frequently hallucinates parameters.

The ceiling is not theoretical. Cursor enforces a hard limit of 40 MCP tools total regardless of how many servers are installed, and GitHub Copilot caps chat requests at 128 tools. Hit either limit and the client refuses to load your server or silently truncates the toolset.

Furthermore, incomplete or inconsistent OpenAPI specs are the primary cause of auto-generated MCP tool failures. A 2025 academic study evaluating 50 real-world APIs found that fixing just 19 lines of spec inconsistencies per API improved out-of-the-box MCP tool success from 76.5% to 99.9%. Despite the rapid adoption of the protocol, building production-ready MCP servers remains highly manual. The same study found that out of over 22,000 MCP-tagged repositories created within six months of the protocol's release, fewer than 5% contained actual servers due to repetitive scaffolding requirements.

A working OpenAPI-to-MCP pipeline has to do four things, in order: parse and dereference the spec, transform each operation into an LLM-friendly JSON Schema, filter the resulting tool catalog down to a session-appropriate subset, and execute calls with proper auth and rate-limit propagation.

Step 1: Parsing the OpenAPI Spec and Extracting Schemas

The first step in the pipeline is reading the OpenAPI document, resolving references, and translating endpoints into the base JSON-RPC 2.0 tool format.

Start with a resolver, not a parser. Real-world OpenAPI documents lean heavily on $ref, allOf, oneOf, and anyOf. If your generator emits raw references into the tool schema, the MCP client will not resolve them—it just sees {"\$ref": "#/components/schemas/Contact"} and either errors out or hallucinates a shape. Every reference must be resolved inline, with circular-reference protection, before the schema is handed to the model.

Here is an example of a raw OpenAPI path item for creating a contact:

paths:
  /crm/contacts:
    post:
      summary: Create a contact
      operationId: createContact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ContactInput'
      responses:
        '200':
          description: Successful response

For each paths [path][method] operation, your parser must extract five things:

  1. Operation identity: operationId (or a synthesized name from path + method), tags, and deprecation flags. The operationId createContact is often too generic if you are serving multiple integrations. A better generated name includes the integration and resource context: create_a_crm_contact.
  2. Path parameters: usually IDs, which are always required.
  3. Query parameters: schema, required flag, description.
  4. Request body: for application/json, the resolved schema; skip binary and multipart for a v1 implementation.
  5. Responses: primarily 2xx shapes, so you know what the tool will return.

The Flat Namespace Problem

The MCP inputSchema is a single flat JSON Schema object. OpenAPI, however, splits parameters by location: query, path, header, and body. When an MCP client (like Claude) calls a tool, it passes a single, flat JSON object containing the arguments.

If your OpenAPI spec has a path parameter named id and a body parameter named id, they will collide in the MCP tool call. You have to merge them into one namespace. Your parser must detect these collisions and rename them (e.g., path_id vs body_id), or rely on strict schema separation during the runtime execution phase. Path params become required properties. Query params get merged in. The request body's properties get spread at the top level of inputSchema, not nested under a body key, otherwise the model has to guess the wrapping.

A minimum viable transform for a GET /contacts/{id} operation looks like this:

const tool = {
  name: toSnakeCase(`get_${resource}_by_id`),
  description: op.summary || op.description || `${method} ${path}`,
  inputSchema: {
    type: 'object',
    properties: {
      id: { type: 'string', description: 'The contact ID. Required.' },
      // ...merged query and body params
    },
    required: ['id'],
  },
}

And here is what the extracted MCP tool JSON should look like before enhancement:

{
  "name": "create_a_crm_contact",
  "description": "Create a contact",
  "inputSchema": {
    "type": "object",
    "properties": {
      "first_name": { "type": "string" },
      "last_name": { "type": "string" },
      "email": { "type": "string" }
    },
    "required": ["email"]
  }
}

Parsing Gotchas

A few gotchas routinely break real integrations:

  • Header parameters: MCP tools should not accept auth headers as arguments. Strip anything that resembles Authorization, X-API-Key, or vendor-specific auth headers and inject them server-side.
  • File uploads: multipart/form-data and binary streams don't round-trip cleanly through MCP text content. Handle these as separate custom tools or skip them.
  • nullable and enum fields: OpenAPI 3.0 uses nullable: true; JSON Schema draft 2020-12 uses type: ["string", "null"]. Normalize to whichever your MCP client expects, or the schema validation on the client side will reject valid inputs.

Spec quality is the hidden variable. Pruning your OpenAPI document before generation—excluding non-useful endpoints like health and internal inspect routes—is the first pass most teams skip. Do it before you generate a single tool.

For more context on parsing structured metadata into JSON Schema contracts, see our guide on how do MCP servers auto-generate tools from API documentation.

Step 2: Enhancing Schemas for LLM Comprehension

LLMs are not traditional REST clients. They do not read documentation side-by-side with their code editor. They rely entirely on the description fields embedded in the JSON Schema to understand how to format their inputs.

The schemas that ship in most OpenAPI files are written for humans reading docs, not models writing tool calls. A property description like "cursor" is legally valid and completely useless. If you pass a raw OpenAPI schema directly to an LLM, it will often fail on pagination, date formatting, and enum selection. You must programmatically enhance the schemas during the generation phase.

Injecting Pagination Instructions

Pagination is a classic failure point for AI agents. If an API uses cursor-based pagination, the LLM might try to increment the cursor like a page number (e.g., sending next_cursor: 2 instead of the base64 string it received).

To fix this, your generation script should intercept specific query parameters like limit and next_cursor and overwrite their descriptions with explicit, LLM-friendly instructions.

if (method === 'list' && propertyName === 'next_cursor') {
  schema.properties.next_cursor = {
    type: 'string',
    description: 'The cursor to fetch the next set of records. 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.'
  };
}

The resulting JSON Schema block provides an unambiguous contract for the probabilistic caller:

{
  "next_cursor": {
    "type": "string",
    "description": "Cursor to fetch the next page. Pass back exactly the value received in the previous response's nextCursor field, without decoding, modifying, or parsing it."
  }
}

Enforcing Required Fields and Formats

OpenAPI specs frequently omit required arrays for nested objects. Your parser should traverse the schema recursively and explicitly hoist required fields; don't just copy the top-level required list. For get, update, and delete operations against a resource, the id field should be a first-class, required property with a description that names the resource type. "The id of the contact to retrieve. Required." beats "id" every time.

Additionally, if a field expects an ISO 8601 datetime, append that requirement to the description: "The start time of the event. Must be formatted as ISO 8601 (e.g., 2026-01-01T12:00:00Z)." By injecting these guardrails, you drastically reduce the number of HTTP 400 Bad Request errors your agent triggers.

Rewriting Descriptions for Intent

A description like "Retrieves a list of resources filtered by the given criteria" tells the model nothing about when to call this tool. Rewrite it in terms of intent: "List HubSpot contacts. Use this when the user asks about people in their CRM."

Benchmarks show OpenAI models hit a hard API limit at 128 tools while accuracy degrades well before that threshold. The fix is intent-driven tool design. Instead of exposing get_user, get_user_by_id, get_user_by_email, and search_users as separate tools, collapse them behind a single high-signal tool where the argument shape communicates intent.

Warning: Verbose descriptions are their own failure mode. A well-known FastMCP regression generated extremely verbose tool descriptions that exploded the context window when Claude tried to use them, with the description formatter adding thousands of characters per tool. Keep descriptions dense: what the tool does, when to use it, one line each for non-obvious parameters.

Step 3: Implementing Tag-Based Filtering to Save Context Windows

Even a well-pruned spec produces too many tools for one agent session. Exposing every endpoint of a massive SaaS platform like Salesforce or Jira to an LLM at once is a catastrophic architectural mistake. A CRM with 60 endpoints exposes contacts, deals, companies, tickets, tasks, notes, meetings, custom objects, and admin routes. An agent that answers "what deals are closing this month" needs six of them. The other 54 are pure context tax.

To solve this, you must implement tag-based tool filtering. Instead of one monolithic MCP server, you generate scoped servers based on the agent's current task.

Using OpenAPI Tags for Curation

Most well-maintained OpenAPI specs organize endpoints using the tags array. You can map these tags to functional groups, or use a richer tool_tags metadata layer if the raw tags are too coarse:

tool_tags:
  contacts: ["crm", "sales"]
  deals: ["crm", "sales"]
  tickets: ["support"]
  ticket_comments: ["support"]
  users: ["directory"]
  organizations: ["directory", "support"]

At server-creation time, the caller declares which tags they want. A support agent gets tags: ["support"] and sees ticket-related tools. A sales agent gets tags: ["sales"] and sees deals. Same server, same underlying integration, different tool surface.

Method-Category Filters

Combine tag filters with method-category filters to slice the catalog on the second axis. A read-only research agent should never see create, update, or delete tools. A pattern that works well in production:

  • read - matches get and list
  • write - matches create, update, delete
  • custom - matches non-CRUD operations like search, download, import

Categories combine: methods: ["read", "custom"] exposes reads plus vendor-specific search endpoints but excludes any mutation. This is how you give an agent Salesforce read access plus SOQL search without the ability to update records.

flowchart TD
    A["Raw OpenAPI Spec"] --> B["Parse & Resolve $refs"]
    B --> C["Enhance Schemas for LLM"]
    C --> D["Apply Tag Filters<br>(e.g., 'support', 'read-only')"]
    D --> E["Generate Scoped MCP Tools"]
    E --> F["Serve via JSON-RPC 2.0"]

Exposing unneeded tools to an agent creates a significant security risk from over-permissioned agents and a severe performance hit known as Context Rot. Too many tools degrade an LLM's ability to reliably select the right tool. Tag filtering acts as both a context-window optimization and a security boundary. An agent tasked with analyzing ticket metrics does not need the ability to delete users, and removing those tools entirely eliminates the risk of hallucinated destructive actions.

One validation rule worth enforcing at the server-creation API: reject a filter combination that produces zero tools. It sounds obvious, but it is the single most common cause of "my MCP server has no tools" support tickets.

For a deeper dive into the N×M integration bottleneck and why manual schema maintenance is unscalable, read our 2026 architecture guide on auto-generated MCP tools.

Step 4: Handling Authentication and Rate Limits at Runtime

Generating the tool JSON is only half the battle. When the LLM decides to call create_a_crm_contact, your infrastructure must execute that API call securely. If you are building a custom MCP server for Claude or another LLM, this requires a robust proxy layer that handles authentication and rate limiting without leaking credentials to the AI client.

OAuth Token Management Belongs Server-Side

MCP clients should never handle third-party API keys, OAuth access tokens, or session cookies directly. The MCP server URL itself acts as the authentication boundary.

When a request hits your proxy API, the server must look up the integrated account associated with the MCP session. The LLM sees a tool called list_all_hubspot_contacts and knows nothing about how the underlying request is authenticated. When the tool is invoked, the server pulls the stored token, injects the Authorization: Bearer <token> header into the outbound HTTP request, and executes it.

OAuth refresh is the messy part—the platform must schedule work ahead of token expiry or refresh it synchronously before executing the tool call. If you refresh reactively on 401 Unauthorized errors, you will burn one failed LLM call per token rotation.

Explicit Rate Limit Handling

AI agents operate much faster than human users. A while loop inside an agentic framework can easily blast an upstream API with 50 requests per second, instantly triggering an HTTP 429 Too Many Requests response.

How your MCP server handles rate limits dictates the reliability of your agent. This is where a lot of hosted MCP layers get it wrong. They swallow HTTP 429 responses, retry with backoff, and hide the throttling from the caller. That sounds nice until an agent loop stalls for 30 seconds with no signal, or the retry burns through the vendor's daily quota because the model keeps re-issuing the same call.

Truto does not automatically retry or absorb rate limit errors. Automatically retrying behind the scenes ties up server threads and hides critical system state from the orchestration layer. Instead, when an upstream API returns an HTTP 429, Truto passes that error directly to the caller. Crucially, it normalizes the upstream rate limit information into standardized IETF headers:

  • ratelimit-limit: The maximum number of requests allowed in the current window.
  • ratelimit-remaining: The number of requests remaining.
  • ratelimit-reset: The time at which the rate limit window resets.

By passing these standard headers back through the MCP tools/call response, the caller decides how to back off. That decision belongs at the agent orchestration layer, where it has context about the whole loop, not at the transport layer.

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32000,
    "message": "Rate limit exceeded. Try again in 14 seconds.",
    "data": {
      "status": 429,
      "headers": {
        "ratelimit-limit": "100",
        "ratelimit-remaining": "0",
        "ratelimit-reset": "1710000014"
      }
    }
  }
}

Error Semantics and Idempotency

When a tool call fails, return the error inside the MCP result content with isError: true, not as a JSON-RPC protocol error. The model can read tool-level errors and adapt (retry with different arguments, try a different tool); it cannot recover from a protocol-level error because those terminate the exchange.

Finally, implement idempotency where you can get it. Any tool that maps to POST or PATCH should accept (or synthesize) an idempotency key so a model retry doesn't create two contacts. If the upstream API supports Idempotency-Key headers, pass them through. If not, at least dedupe on (tool_name, argument_hash) within a short window at the server layer.

How Truto Automates the OpenAPI-to-MCP Pipeline

Building this pipeline from scratch—parsing OpenAPI, enhancing schemas, filtering tags, managing OAuth state, and normalizing rate limits—requires months of dedicated engineering. Truto's design pushes the entire pipeline into a data-driven engine so adding a new integration doesn't mean writing a new MCP server.

Truto's architecture contains zero integration-specific code. There are no hardcoded if (provider === 'salesforce') statements in the database or runtime logic. Instead, Truto relies on declarative configuration. Each integration is a declarative config defining the resources it exposes, the methods on each resource, the auth flow, and the pagination style.

When you connect an integration, Truto dynamically derives the MCP tools from two existing data sources:

  1. Integration Resources: The proxy endpoints the integration actually serves.
  2. Documentation Records: Curated human-readable descriptions and JSON Schemas for each resource method.

Documentation-Driven Curation

Truto uses documentation as a strict quality gate. If an endpoint exists in an OpenAPI spec but lacks a corresponding documentation record in Truto, it is skipped entirely. No documentation means no tool. Undocumented endpoints don't leak into the tool catalog. This prevents messy or deprecated endpoints from polluting the LLM's context window.

Real-Time Tool Generation

Tools are never cached or pre-built as static JSON files. Tool generation happens dynamically on every tools/list or tools/call request. This ensures that the MCP server always reflects the exact, current state of the integration's schema and honors any environment-level overrides or tag filters you have applied.

When the agent executes a tool, the request delegates to Truto's generic proxy API handlers. The engine extracts the tool's query and body schemas, splits the flat MCP arguments object into the correct HTTP parameters, applies the OAuth credentials, and executes the request. Because the same generic execution engine handles every integration, the same OpenAPI-to-MCP pipeline works for HubSpot, Salesforce, Jira, Zendesk, or any of the 100+ integrations Truto ships.

To learn more about securely exposing these auto-generated tools to your end-users via a multi-tenant architecture, review our guide on generating MCP servers for SaaS users.

Strategic Next Steps

Auto-generating MCP tools from OpenAPI is a solved problem in the small and an unsolved problem at production scale. The small version is a parser and a schema transform. The production version is spec pruning, schema enhancement, tag filtering, OAuth lifecycle, rate-limit propagation, and per-tenant scoping—and it has to survive every vendor deprecating an endpoint on a Tuesday.

If you want your AI agents to interact reliably with external SaaS platforms, you must move beyond naive 1:1 OpenAPI dumping. Treat your API definitions as raw data, enhance them for LLM comprehension, and rely on a generic execution pipeline to handle the heavy lifting at runtime. Start with a scoped tool catalog (25 well-chosen tools beats 200 raw ones every time), enforce tag-based filtering at server creation, and get auth and rate-limit handling right before you optimize anything else.

FAQ

Why shouldn't I map every OpenAPI endpoint to an MCP tool?
A naive 1:1 mapping causes a 'tool explosion' that overwhelms the LLM's context window. A 200-endpoint spec can consume 40,000-80,000 tokens just for tool schemas. Once the catalog exceeds 25-50 tools, LLMs struggle to select the correct tool and begin hallucinating parameters. Furthermore, clients like Cursor and GitHub Copilot enforce hard limits of 40 and 128 tools, respectively.
How do you handle rate limits with MCP tools?
Do not automatically retry requests server-side. Pass HTTP 429 errors directly back to the MCP caller with standardized IETF headers (ratelimit-limit, ratelimit-remaining, ratelimit-reset). The agent orchestration layer has the context to decide whether to apply exponential backoff, switch tools, or surface the error, whereas the transport layer does not.
How do you fix pagination parameters for LLMs?
You must programmatically enhance the JSON Schema descriptions during tool generation. Explicitly instruct the LLM to return cursor values exactly as received, without decoding, modifying, or parsing them, otherwise the agent may treat a base64 string like a page number.
What is tag-based tool filtering in MCP?
Tag-based filtering uses OpenAPI tags or custom metadata to group tools by functional area (e.g., 'crm', 'support'). Combined with method-category filters (read, write, custom), it allows you to generate scoped MCP servers that only expose the tools necessary for a specific task, preserving the context window and improving security.
Should MCP tools accept auth headers as arguments?
No. API keys, OAuth tokens, and session cookies should never appear in tool argument schemas. The MCP server must act as the authentication boundary, holding the credential for the connected account and injecting it server-side into outbound requests.

More from our Blog