Skip to content

OpenAPI Generator

npm

Turn any REST/OpenAPI 3.x or Swagger 2.0 spec into a working MCP server — either by generating typed TypeScript files ahead of time, or by parsing the spec at startup and proxying requests at runtime.

This is the ultimate Legacy API Migration strategy for AI Agents. Whether you need to connect thousands of legacy corporate endpoints to an LLM, or simply want to bridge Swagger to Claude Desktop, this generator completely automates the creation of strict MVA architectures, models, and Presenters.

bash
npx openapi-gen generate -i ./petstore.yaml -o ./generated
API_BASE_URL=https://api.example.com npx tsx ./generated/server.ts

Install

bash
npm install @vurb/openapi-gen

Peer dependencies: Vurb.ts and zod.

Generated Output

generated/
├── models/      ← Zod schemas with .strict() validation
├── views/       ← Presenters with response shaping
├── agents/      ← MCP tool definitions with full annotations
├── index.ts     ← ToolRegistry barrel
└── server.ts    ← Server bootstrap (stdio or SSE)

Every file follows the MVA Convention.

Schema Fidelity — OpenAPI to Strict Zod

The ZodCompiler walks every OpenAPI SchemaNode and emits strict Zod objects. Path and query parameters get z.coerce for automatic string-to-type coercion. Response schemas get .strict() to reject undeclared fields at runtime.

typescript
// models/pet.schema.ts (generated)
export const PetResponseSchema = z.object({
    id: z.number().int(),
    name: z.string(),
    status: z.enum(['available', 'pending', 'sold']).optional(),
}).strict();

Annotation Inference

The EndpointMapper reads the HTTP method of each operation and infers the correct MCP annotation:

HTTP MethodAnnotation
GET, HEAD, OPTIONSreadOnly: true
DELETEdestructive: true
PUTidempotent: true
POST, PATCHdefault
typescript
// agents/pet.tool.ts (generated)
export const petTools = defineTool<ApiContext>('pet', {
    annotations: { title: 'Pet' },
    actions: {
        get_by_id: {
            readOnly: true,
            description: 'Find pet by ID',
            returns: PetPresenter,
            params: z.object({
                petId: z.coerce.number().int().describe('ID of pet'),
            }),
            handler: async (ctx, args) => {
                const res = await fetch(`${ctx.baseUrl}/pet/${args.petId}`);
                return res.json();
            },
        },
        delete: {
            destructive: true,
            params: z.object({ petId: z.coerce.number().int() }),
            handler: async (ctx, args) => { /* ... */ },
        },
    },
});

Code Generation Pipeline

Five compilation stages transform the spec into production-ready TypeScript. Swagger 2.0 specs are auto-converted before processing. Each stage is independently importable:

typescript
import { parseOpenAPI, mapEndpoints, emitFiles, mergeConfig } from '@vurb/openapi-gen';

const spec = parseOpenAPI(yamlString);
const mapped = mapEndpoints(spec);

const config = mergeConfig({
    features: { presenters: true, tags: true },
    server: { name: 'my-server', toolExposition: 'grouped' },
    includeTags: ['pet'],
});

const files = emitFiles(mapped, config);

for (const file of files) {
    writeFileSync(`./out/${file.path}`, file.content);
}

The generated code is fully editable — modify handlers, add middleware, attach Presenters.

Swagger 2.0 Support

Swagger 2.0 specs are automatically detected and converted to OpenAPI 3.0 internally — no configuration needed. Both the CLI and programmatic API accept v2 specs:

bash
# Works with legacy Swagger 2.0 specs out of the box
npx openapi-gen generate -i ./petstore-v2.json -o ./generated

The internal Swagger2Converter handles all structural differences:

Swagger 2.0→ OpenAPI 3.0
host + basePath + schemesservers array
definitionscomponents.schemas
parameters[in: body]requestBody
parameters[in: formData]requestBody (multipart/urlencoded)
#/definitions/Model#/components/schemas/Model
produces / consumesPer-operation content types

You can also use the converter standalone:

typescript
import { isSwagger2, convertSwagger2ToV3, parseOpenAPI } from '@vurb/openapi-gen';

const raw = JSON.parse(specJson);
if (isSwagger2(raw)) {
    const v3 = convertSwagger2ToV3(raw);
    // v3 is now a valid OpenAPI 3.0 object
}

// Or just pass directly — auto-detected:
const spec = parseOpenAPI(specJson); // works with both v2 and v3

Runtime Proxy Mode

For rapid prototyping, loadOpenAPI() parses the spec at startup and creates live proxy handlers with no code generation step. Accepts both OpenAPI 3.x and Swagger 2.0:

typescript
import { loadOpenAPI } from '@vurb/openapi-gen';
import { defineTool, ToolRegistry } from '@vurb/core';

const tools = loadOpenAPI(specYaml, {
    baseUrl: 'https://api.example.com',
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
});

const registry = new ToolRegistry();
for (const tool of tools) {
    registry.register(defineTool(tool.name, {
        description: tool.description,
        actions: Object.fromEntries(
            tool.actions.map(a => [a.name, {
                description: a.description,
                readOnly: a.method === 'GET',
                handler: async (ctx, args) => a.handler(ctx, args),
            }])
        ),
    }));
}

When the API spec changes, restart the server and the tools update automatically.

Full Production Example

typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { defineTool, ToolRegistry, createServerAttachment } from '@vurb/core';
import { loadOpenAPI } from '@vurb/openapi-gen';
import { readFileSync } from 'node:fs';

const specYaml = readFileSync('./petstore.yaml', 'utf-8');
const tools = loadOpenAPI(specYaml, {
    baseUrl: process.env.API_BASE_URL!,
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
});

const registry = new ToolRegistry();
for (const tool of tools) {
    const builder = defineTool(tool.name, {
        description: tool.description,
        actions: Object.fromEntries(
            tool.actions.map(a => [a.name, {
                description: a.description,
                readOnly: a.method === 'GET',
                destructive: a.method === 'DELETE',
                handler: async (ctx, args) => a.handler(ctx, args),
            }])
        ),
    });
    registry.register(builder);
}

const server = new McpServer({ name: 'petstore-mcp', version: '1.0.0' });
createServerAttachment(server, registry);
await server.connect(new StdioServerTransport());

Configuration

Create openapi-gen.yaml in your project root. The CLI auto-detects it, or pass --config <path>.

yaml
input: ./specs/petstore.yaml
output: ./generated

features:
  tags: true              # Tag-based tool grouping
  annotations: true       # Infer readOnly / destructive / idempotent
  presenters: true        # Generate Presenter files with Zod schemas
  descriptions: true      # Include OpenAPI summaries on actions
  toonDescription: false  # TOON-optimized descriptions
  serverFile: true        # Generate server.ts bootstrap
  deprecated: comment     # 'include' | 'skip' | 'comment'

naming:
  style: snake_case       # 'snake_case' | 'camelCase'
  deduplication: true     # Auto-suffix duplicates (_2, _3)

server:
  name: petstore-mcp
  version: 1.0.0
  transport: stdio          # 'stdio' | 'sse'
  toolExposition: flat      # 'flat' | 'grouped'
  actionSeparator: '_'      # Flat mode delimiter: pet_get_by_id

context:
  import: '../types.js#AppCtx'

includeTags: [pet, store]
excludeTags: [internal]

CLI Flags

bash
npx openapi-gen [options]
FlagDefault
--input <path>From config
--output <dir>./generated
--config <path>Auto-detect
--base-url <expr>ctx.baseUrl
--server-name <name>openapi-mcp-server
--context <import>Built-in ApiContext

CLI flags override config file values.

Exposition Strategy

StrategyBehaviorBest for
flat (default)Each action = independent MCP toolGranular control, privilege isolation
groupedAll actions merge into one toolToken economy, large APIs
yaml
server:
  toolExposition: grouped
  actionSeparator: '_'

Name Resolution

operationIdsnake_casecamelCase
getPetByIdget_pet_by_idgetPetById
findPetsByTagsfind_pets_by_tagsfindPetsByTags
addPetadd_petaddPet

When operationId is missing: GET /petslist_pets, POST /petscreate_pets. Duplicates auto-suffix: list_pets, list_pets_2.

Tag Filtering

yaml
includeTags: [pet, store]
excludeTags: [admin, internal]

Custom Context

yaml
context:
  import: '../types.js#AppCtx'
typescript
import type { AppCtx } from '../types.js';
const petTools = defineTool<AppCtx>('pet', { /* handlers receive ctx: AppCtx */ });

API Reference

loadOpenAPI(input, config)

OptionTypeDefaultDescription
baseUrlstringBase URL for API calls
headersRecord<string, string>{}Default headers sent with every request
fetchFntypeof fetchglobalThis.fetchCustom fetch function

RuntimeTool

MemberTypeDescription
namestringTool name (from OpenAPI tag)
descriptionstringTool description
actionsRuntimeAction[]Compiled action definitions

RuntimeAction

MemberTypeDescription
namestringAction name (from operationId)
descriptionstringOpenAPI summary
methodstringHTTP method
pathstringURL path template
handler(ctx, args) => Promise<unknown>Pre-wired HTTP proxy handler

Programmatic API

ExportDescription
parseOpenAPI(input)Parse YAML/JSON to ApiSpec AST (v2 + v3)
mapEndpoints(spec)Apply naming, annotations, dedup
emitFiles(mapped, config)Generate TypeScript files
mergeConfig(partial)Merge partial config with defaults
loadConfig(path?)Load config from YAML file
compileZod(schema)Compile a SchemaNode to Zod code
loadOpenAPI(input, config)Runtime mode — parse + proxy (v2 + v3)
buildHandler(action)Build a single HTTP proxy handler
isSwagger2(obj)Detect Swagger 2.0 documents
convertSwagger2ToV3(obj)Convert Swagger 2.0 → OpenAPI 3.0

Requirements

DependencyVersion
Node.js≥ 18
Vurb.ts^2.0.0 (peer)
zod^3.25.1 || ^4.0.0 (peer)
yaml^2.7.0 (bundled)