Skip to main content
Most HTTP APIs already describe themselves. An OpenAPI spec names every operation, types every parameter, and shapes every response; that is exactly the information an MCP server needs to advertise tools. createOpenAPIServer() reads a spec and returns a ready FastMCP: every operation becomes a tool whose input schema is derived from the operation’s parameters and request body, and whose handler calls the real API endpoint. You write no handlers and no schemas; the spec is the source of truth.
The generated server is a plain FastMCP, so everything else in this documentation applies to it unchanged: run it on stdio or HTTP, mount it into a gateway, wrap it in middleware, or gate it with auth. This is the TypeScript counterpart of Python FastMCP’s FastMCP.from_openapi, and the two implementations generate identical component names and schemas from the same spec, so a server can move between the two runtimes without its clients noticing. spec accepts a parsed object or YAML/JSON text, in OpenAPI 3.0 or 3.1. References must be local to the document; an external $ref is rejected, because the generator will not fetch remote schemas at build time.

Generated tools

Each operation’s tool advertises a single flat input schema. Path, query, header, and cookie parameters merge with the request body’s properties into one object, so a model calling the tool fills in plain named arguments and the generator routes each one back to its proper location in the HTTP request. When a parameter name appears in more than one location, the non-body occurrences are disambiguated with a location suffix (id__path, id__query); body properties always keep their bare names. The generator also respects OpenAPI access modes. A property marked readOnly: true belongs to responses only, so it is omitted from the tool’s input schema and its name is removed from the input required list. A property marked writeOnly: true belongs to requests only, so it stays in the input schema but is omitted from the tool’s output schema, and its name is removed from the output required list. The filter applies at every nesting level, including $defs. The output schema comes from the operation’s success response (the first of 200, 201, 202, 204, or any other 2xx). MCP requires tool output schemas to be objects, so a response typed as an array or scalar is wrapped in { result: ... } and the schema carries an x-fastmcp-wrap-result marker; at call time the response is wrapped to match. JSON responses become structuredContent, everything else is returned as text, and a non-2xx status becomes a tool error carrying the status code, reason, and response body. Tool names come from the spec, in this order: the operationId (trimmed at the first __), an override from the names option, the operation’s summary, or {METHOD}_{path} as a last resort. Names are slugified, capped at 56 characters, and deduplicated with numeric suffixes (search, search_2). To rename specific operations, map their operationIds:

The HTTP client

Requests go to the spec’s first servers entry by default, with server variables filled from their declared defaults. The client option overrides or completes that: a spec with no servers entry (or a relative one) needs an explicit baseUrl. Default headers ride on every request, auth resolves fresh headers per request (the right hook for token refresh), and timeoutMs bounds each call at 30 seconds unless you say otherwise. Request-specific values always win: a header parameter supplied by the caller replaces a default header of the same name.
The client sends with the global fetch. Pass client.fetch to substitute your own, for proxying, recording, or tests.

Route maps

By default every operation becomes a tool, which is the right call for LLM clients: tools are the component type models can invoke. Route maps let you carve out exceptions. Each map matches on HTTP methods, a path pattern (searched unanchored, so pets matches /api/pets/{id}), and required tags; the first matching map decides whether the route becomes a tool, resource, resourceTemplate, or is excluded entirely. Your maps are checked in order before the default catch-all.
This example is the classic REST reading: admin endpoints disappear, GETs with path parameters become resource templates, other GETs become resources, and every write remains a tool. A generated resource lives at resource://{name}; a template appends its path parameters in alphabetical order (resource://getPet/{petId}), and reading one routes the parameters back into the HTTP path. For decisions a declarative map cannot express, routeMapFn runs on every route after the maps and may return a different type (or nothing, to keep the mapped one). A map can also attach mcpTags to the components it creates, and the top-level tags option tags everything, which pairs well with transforms like VersionFilter.

Customizing components

componentFn is the last word before registration. It receives the parsed route and the component about to be registered, and mutates it in place: rename a tool, rewrite a description for the model’s benefit, adjust tags, or tighten a schema.
By default tools advertise the exact output schema derived from the spec. If the upstream API’s responses drift from its spec (a common ailment), set validateOutput: false and tools advertise a permissive object schema instead, so structurally surprising responses still flow through as structured JSON. Output schemas also omit format keywords: MCP validators treat format as a hard assertion on tool results, and real APIs routinely return values (an empty string for an unset URL, say) that a spec’s format: uri would reject. Input schemas keep their formats.

Running the generated server

A generated server file is an ordinary FastMCP entry point, so the CLI treats it like any other server: npx fastmcp run server.ts for stdio, npx fastmcp inspect --file server.ts to see what was generated, and the standard env vars (MCP_TRANSPORT, MCP_PORT) select the transport in deployment. To publish it as an npm package that hosts launch with npx <your-package>, see ship as a package: the entry file needs a shebang line and a bin field, or the host will report a crash on launch.

Python parity

Generation is contract-compatible with Python FastMCP: names, input schemas, output schemas, wrap markers, collision suffixes, and route-map semantics match, and the test suite pins them against snapshots generated by the Python implementation. A few behaviors differ. Python forwards inbound MCP HTTP request headers to the upstream API; the TypeScript server does not, because its request context does not yet expose inbound headers, so upstream credentials belong in client.headers or client.auth. And where Python renders error-response bodies through its own object formatting, TypeScript appends the raw response text; the HTTP error {status} prefix is identical in both. Three generation behaviors are deliberate fixes of bugs the Python implementation still has: required fields inside an optional requestBody stay required, a lone body field of an object-like schema is JSON-wrapped, and output schemas drop format keywords.