MCP Server
Generate Model Context Protocol servers from OpenAPI
Generate Model Context Protocol (MCP) servers from your OpenAPI specification for AI agent integration.
Overview
MCP servers relay API clients to AI agents, eliminating the need to wait for third-party implementations. Create MCP servers for any service with an OpenAPI specification and use them with AI agents like Claude, Cline, and others.
Configuration
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './petstore.yaml',
},
output: {
mode: 'single',
client: 'mcp',
baseUrl: 'https://petstore3.swagger.io/api/v3',
target: 'src/handlers.ts',
schemas: 'src/http-schemas',
},
},
});The mcp client currently only works in single mode.
Generated Structure
src/
├── http-schemas/
│ ├── createPetsBodyItem.ts
│ ├── error.ts
│ ├── index.ts
│ └── pet.ts
├── handlers.ts # Handler functions returning MCP format
├── http-client.ts # Generated fetch client
├── server.ts # MCP tools and server configuration
└── tool-schemas.zod.ts # Zod schemas for tool inputsUsage
1. Build Docker Image
docker build ./ -t mcp-petstore2. Configure AI Agent
For Cline:
{
"mcpServers": {
"petstore": {
"command": "docker",
"args": ["run", "-i", "--rm", "mcp-petstore"],
"disabled": false,
"alwaysAllow": []
}
}
}This allows your AI agent to interact with the API through the MCP protocol.
Request Cancellation
When an MCP client cancels a tool call, the generated server.ts passes the cancellation signal of that call to the underlying HTTP request. The in-flight fetch is aborted with an AbortError instead of running to completion.
tools.findPetsByStatus = server.registerTool(
'findPetsByStatus',
{
// ...
},
(args, ctx) =>
findPetsByStatusHandler(args, {
...options,
signal: options?.signal
? AbortSignal.any([options.signal, ctx.signal])
: ctx.signal,
}),
);If you pass a signal through createMcpServer(options), for example from a custom server to abort every in-flight request on shutdown, it is combined with the per-call signal using AbortSignal.any. Either signal aborts the request.
const shutdown = new AbortController();
const { server } = createMcpServer({ signal: shutdown.signal });
process.on('SIGTERM', () => shutdown.abort());AbortSignal.any requires Node.js 20.3 or later at runtime.
Custom Handler
By default, each generated handler returns the response body as text, marks HTTP status codes of 400 and above as errors, and passes the parsed body as structuredContent. To take over response shaping and error mapping, or to use the tool call context for logging, authorization, or elicitation, provide a custom handler via override.mcp.handler.
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './petstore.yaml',
},
output: {
mode: 'single',
client: 'mcp',
baseUrl: 'https://petstore3.swagger.io/api/v3',
target: 'src/handlers.ts',
schemas: 'src/http-schemas',
override: {
mcp: {
handler: {
path: './custom-handler.ts',
name: 'customHandler',
},
},
},
},
},
});When set, every generated handler binds the tool arguments to a fetcher and delegates to your function together with the tool call context:
import { customHandler } from '../custom-handler';
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import type {
ServerNotification,
ServerRequest,
} from '@modelcontextprotocol/sdk/types.js';
export const findPetsByStatusHandler = async (
args: findPetsByStatusArgs,
options?: RequestInit,
ctx?: RequestHandlerExtra<ServerRequest, ServerNotification>,
) => {
const fetcher = (overrides?: RequestInit) =>
findPetsByStatus(args.queryParams, {
...options,
...overrides,
headers: {
...Object.fromEntries(new Headers(options?.headers)),
...Object.fromEntries(new Headers(overrides?.headers)),
},
});
return customHandler(fetcher, ctx);
};Implement the handler with the following signature. Its return value is used as the tool result as is, so return structuredContent on success whenever the tool declares an outputSchema:
import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
import type {
CallToolResult,
ServerNotification,
ServerRequest,
} from '@modelcontextprotocol/sdk/types.js';
export const customHandler = async (
fetcher: (
overrides?: RequestInit,
) => Promise<{ status: number; data: unknown; headers: Headers }>,
ctx?: RequestHandlerExtra<ServerRequest, ServerNotification>,
): Promise<CallToolResult> => {
const res = await fetcher({
headers: ctx?.sessionId ? { 'Mcp-Session-Id': ctx.sessionId } : undefined,
});
if (res.status >= 400) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
requestId: ctx?.requestId,
status: res.status,
error: res.data ?? null,
}),
},
],
isError: true,
};
}
return {
content: [{ type: 'text', text: JSON.stringify(res.data ?? null) }],
structuredContent: res.data as Record<string, unknown>,
};
};fetcher already carries the tool arguments and the RequestInit passed to createMcpServer, including the cancellation signal described above. Pass overrides to add headers or to replace the signal. Headers from overrides are merged into the base headers instead of replacing them, so headers such as Authorization set through createMcpServer(options) stay on the request.
ctx is the SDK's RequestHandlerExtra for the current tool call. It exposes signal, requestId, sessionId, authInfo, requestInfo, sendNotification, and sendRequest. It is optional so that the generated handlers can still be called directly, for example from tests.
Place the custom handler file outside the generated output directory. If clean: true is set, orval deletes the output directory on each run and will remove any file inside it.
Custom Server
By default, the generated server.ts connects via StdioServerTransport. To use a different transport (e.g., Streamable HTTP for container deployments), provide a custom server function via override.mcp.server.
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
input: {
target: './petstore.yaml',
},
output: {
mode: 'single',
client: 'mcp',
baseUrl: 'https://petstore3.swagger.io/api/v3',
target: 'src/handlers.ts',
schemas: 'src/http-schemas',
override: {
mcp: {
server: {
path: './custom-server.ts',
name: 'customServer',
},
},
},
},
},
});The generated server.ts calls your function with a createMcpServer factory:
import { customServer } from '../custom-server';
const createMcpServer = (
options?: RequestInit,
): { server: McpServer; tools: Record<string, RegisteredTool> } => {
// ...tool registrations
};
customServer(createMcpServer);Implement the custom server function to set up any transport. The following example uses Hono with @hono/mcp for Streamable HTTP:
import type {
McpServer,
RegisteredTool,
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPTransport } from '@hono/mcp';
import { Hono } from 'hono';
export const customServer = (
createMcpServer: (options?: RequestInit) => {
server: McpServer;
tools: Record<string, RegisteredTool>;
},
) => {
const app = new Hono();
const { server } = createMcpServer();
const transport = new StreamableHTTPTransport();
app.all('/mcp', async (c) => {
if (!server.isConnected()) {
await server.connect(transport);
}
return transport.handleRequest(c);
});
Bun.serve({ fetch: app.fetch, port: Number(process.env.PORT ?? 3000) });
};Place the custom server file outside the generated output directory. If clean: true is set, orval deletes the output directory on each run and will remove any file inside it.
Full Example
See the MCP Petstore sample and MCP Custom Server sample on GitHub.