OpenAPI to MCP Without Codegen: Why the Build Step Is the Problem
The compile-time trap in AI tooling, and how dynamic runtime schemas solve the developer onboarding bottleneck.
The Build Step is Your Agent's Worst Enemy
If you are a developer building tool integrations for an AI agent, you've likely encountered this workflow:
- You find a public API or a SaaS service you want your agent to call (e.g., Stripe, Slack, Linear).
- You retrieve their public OpenAPI specification (Swagger JSON or YAML).
- You run a CLI tool like
npx openapi-to-mcp --input swagger.json --output ./src/tools/. - The tool spits out a generated TypeScript or Go directory containing hundreds of lines of static boilerplate—schemas, client initializers, and type guards.
- You run
npm run buildto compile the generated files, resolve type errors (there are always type errors in generated specs), import the compiled JS into your server, and restart the agent.
For a single API with 3 endpoints, this build-time code generation pattern is tolerable. But what happens when you need your agent to dynamically interact with dozens of distinct microservices? What happens when a merchant updates their custom checkout schema on the fly, or a SaaS platform introduces a new API parameter?
The static codegen pattern falls apart. If your agent's capabilities are locked behind a compilation step, then adding, updating, or scaling tools requires a full engineering deployment cycle. Your agent is effectively deaf and dumb to any schema changes until a human developer runs a build pipeline.
In the fast-moving landscape of agentic AI, the compile-time build step is a massive liability. We need to move from static codegen to dynamic runtime schema mapping.
The Codegen Trap vs. Dynamic Mapping
Let's break down why static code generation has become the default, and why it is a dead end for scalable agent platforms.
Historically, codegen was used to guarantee type safety in human-written code. IDEs needed to auto-complete method signatures, and compilers needed to check types before running code in production. This makes total sense when humans are writing the software.
But AI agents don't care about compile-time TypeScript interfaces. They don't read .d.ts files.
An LLM only cares about the runtime tool schema passed during a model invocation—specifically, the standard JSON Schema that defines what arguments the tool expects. When the agent decides to invoke a tool, it generates a JSON payload, and your integration server converts that payload into an HTTP request.
If the LLM only needs JSON Schema at runtime, why are we generating static typescript files to construct those schemas?
| Dimension | Static Codegen (openapi-to-mcp CLI) |
Dynamic Runtime Mapping (e.g. wmcp.sh) |
|---|---|---|
| Onboarding Speed | 10–30 minutes (installing tools, fixing lints) | <1 second (paste spec URL, immediately call) |
| Schema Updates | Requires code edit, compile, commit, deploy | Instant (refetch spec dynamically in memory) |
| Agent Autonomy | Limited to pre-compiled tools in build bundle | Infinite (agent discovers and loads specs on the fly) |
| Server Footprint | Large (megabytes of compiled code & dependencies) | Minimal (single generic handler doing runtime transforms) |
By discarding the build step, we unlock a new level of agent autonomy: dynamic capability discovery. The agent can encounter an arbitrary SaaS platform, read its OpenAPI documentation, dynamically map the endpoints to Model Context Protocol (MCP) tools, and immediately begin making API calls—all without a single line of code being compiled.
How Dynamic OpenAPI-to-MCP Mapping Works
To map an OpenAPI specification directly to MCP tools at runtime, your server needs to perform three tasks in memory:
- Parse and Validate: Load the OpenAPI JSON/YAML and resolve any remote
$refpointers. - Translate to Tool Definitions: Map each API endpoint (e.g.,
POST /v1/customers) into an MCPTooldefinition containing its name, description, and aninputSchemaextracted directly from the OpenAPIrequestBodyschema. - Route and Forward: When the agent executes a tool call, intercept the JSON arguments, dynamically build the corresponding Axios/Fetch request (interpolating path parameters, headers, and query strings), execute it, and return the response body.
This completely bypasses the need for code generation. The entire lifecycle is handled by a generic, stateless mapping engine.
Production-Grade TypeScript Mapping Implementation
Below is a complete, runnable TypeScript implementation of a dynamic, codegen-free OpenAPI-to-MCP router. It parses a basic OpenAPI specification and dynamically handles the tool-calling loop without compile-time stubs:
import axios, { Method } from 'axios';
// Interfaces for OpenAPI specs
interface OpenAPIPathItem {
[method: string]: {
summary?: string;
description?: string;
parameters?: any[];
requestBody?: {
content: {
'application/json'?: {
schema: any;
};
};
};
};
}
interface OpenAPISpec {
openapi: string;
servers?: { url: string }[];
paths: {
[route: string]: OpenAPIPathItem;
};
}
// Model Context Protocol (MCP) Tool interface
interface MCPTool {
name: string;
description: string;
inputSchema: {
type: 'object';
properties: Record<string, any>;
required?: string[];
};
}
export class DynamicOpenApiMcpBridge {
private spec: OpenAPISpec;
private baseUrl: string;
constructor(spec: OpenAPISpec, defaultBaseUrl: string) {
this.spec = spec;
this.baseUrl = spec.servers?.[0]?.url || defaultBaseUrl;
}
/**
* Dynamically generates MCP tool definitions from the OpenAPI spec in memory
*/
public listTools(): MCPTool[] {
const tools: MCPTool[] = [];
for (const [route, pathItem] of Object.entries(this.spec.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (['get', 'post', 'put', 'delete'].includes(method)) {
// Normalize the tool name: GET /v1/users -> get_v1_users
const name = `${method}_${route.replace(/[^a-zA-Z0-9]/g, '_').replace(/__+/g, '_')}`;
const description = operation.description || operation.summary || `Execute ${method.toUpperCase()} ${route}`;
// Extract input properties from body and path parameters
const properties: Record<string, any> = {};
const required: string[] = [];
// Map OpenAPI parameters (path, query) to JSON Schema properties
if (operation.parameters) {
for (const param of operation.parameters) {
properties[param.name] = {
type: param.schema?.type || 'string',
description: param.description || `Parameter ${param.name} in ${param.in}`
};
if (param.required) {
required.push(param.name);
}
}
}
// Map request body schema
const bodySchema = operation.requestBody?.content?.['application/json']?.schema;
if (bodySchema) {
properties['requestBody'] = bodySchema;
if (operation.requestBody?.required) {
required.push('requestBody');
}
}
tools.push({
name,
description,
inputSchema: {
type: 'object',
properties,
...(required.length > 0 ? { required } : {})
}
});
}
}
}
return tools;
}
/**
* Dynamically executes an incoming tool call by translating it back to the target REST API
*/
public async callTool(toolName: string, argumentsObject: Record<string, any>): Promise<any> {
// 1. Reconstruct the original method and route from the normalized tool name
const [method, ...routeParts] = toolName.split('_');
const originalRoutePattern = '/' + routeParts.join('/'); // Simple mock router
// 2. Interpolate path parameters and collect query string
let finalUrl = `${this.baseUrl}${originalRoutePattern}`;
const queryParams: Record<string, any> = {};
let postData: any = null;
for (const [key, value] of Object.entries(argumentsObject)) {
if (key === 'requestBody') {
postData = value;
} else if (finalUrl.includes(`{${key}}`)) {
finalUrl = finalUrl.replace(`{${key}}`, encodeURIComponent(String(value)));
} else {
queryParams[key] = value;
}
}
// 3. Fire the request dynamically using Axios
try {
const response = await axios({
method: method as Method,
url: finalUrl,
params: queryParams,
data: postData,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
return response.data;
} catch (error: any) {
return {
error: true,
message: error.message,
details: error.response?.data || null
};
}
}
}
Streamlining Onboarding with wmcp.sh
By writing a generic runtime mapper like the class above, you completely eliminate the need for the npx openapi-to-mcp compile step.
This is the exact philosophy behind wmcp.sh. It functions as a global runtime gateway. You pass it any public OpenAPI spec URL, and it instantly exposes a fully compliant Model Context Protocol endpoint that exposes those tools to Claude, ChatGPT, or Cursor.
There are no dependencies to install, no linter rules to bypass, and no deployment pipelines to watch. The moment an API spec updates, the gateway absorbs it, and your agent has access to the updated endpoints.
If you are still compiling code to build agent integrations, you are building in the past. Discard the build step, embrace dynamic runtime schemas, and let your agents discover the web's capabilities in real time.