# MCP Server

Generate [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) 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

```ts
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',
    },
  },
});
```

> **Note:**
> 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 inputs
```

## Usage

### 1. Build Docker Image

```bash
docker build ./ -t mcp-petstore
```

### 2. Configure AI Agent

For Cline:

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

```ts
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.

```ts
const shutdown = new AbortController();
const { server } = createMcpServer({ signal: shutdown.signal });
process.on('SIGTERM', () => shutdown.abort());
```

> **Note:**
> `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`.

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

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

```ts
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.

> **Note:**
> 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`.

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

```ts
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](https://hono.dev) with [`@hono/mcp`](https://github.com/honojs/middleware/tree/main/packages/mcp) for Streamable HTTP:

```ts
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) });
};
```

> **Note:**
> 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](https://github.com/orval-labs/orval/tree/master/samples/mcp/petstore) and [MCP Custom Server sample](https://github.com/orval-labs/orval/tree/master/samples/mcp/custom-server) on GitHub.
