If you search for how to generate an MCP server from an OpenAPI document, you find generators and tutorials. They are useful, and they are also the fastest way to learn what a generator cannot decide for you. So instead of describing them, we ran two of the most cited against the Sample Shop API and recorded the output next to the hand-designed servers in the examples repository.
Method
- Input:
sample-api/openapi.json, an OpenAPI 3.1 document with five operations, an API-key security scheme, a requiredIdempotency-Keyheader onPOST /orders, and an irreversiblePOST /orders/{id}/cancel. - Date: 7 October 2026. Versions: FastMCP 4.0.11 (
FastMCP.from_openapi, Python 3.13), openapi-mcp-generator 4.1.0 (Node 20 and Node 24). - Probe: the same MCP client used by the conformance test listed the tools, then called
searchProductswithlimit: 99,searchProductsnormally, andcancelOrderwith an unknown id. - Not run: Speakeasy's generator and Gram hosting (a hosted product we did not sign up for) and the
mcp-from-openapinpm package (its page did not load for us). They are mentioned only for what their own documentation says.
What FastMCP produced
FastMCP.from_openapi(openapi_spec, client) converts in-process, no files generated.
- Tools: 5, one per operation, named by
operationId:searchProducts,getProduct,createOrder,getOrder,cancelOrder. - Descriptions: copied from the spec's
description, falling back tosummary. SogetProductis described as "Get one product" andgetOrderas "Get one order", nothing more. - Schemas: parameters and request-body fields with the spec's constraints (
limit1 to 20,quantity1 to 10). TheIdempotency-Keyheader becomes a required tool argument literally namedIdempotency-Key. - Annotations: none. Nothing marks a tool read-only or destructive.
- Auth: you construct the HTTP client with the key in its headers; the credential stays server-side, which is correct.
- Results: the raw upstream JSON, including
priceCentsand every field. - Errors:
limit: 99returned anisErrorresult whose text was "HTTP error 400: Bad Request" plus the raw upstream body, with a traceback logged server-side.cancelOrderon an unknown id returned "HTTP error 404" likewise. Nothing tells the model whether to retry. - Guarding:
cancelOrderexecutes immediately. No confirmation step exists. - Ran? Yes, in-process, first time. It warned that passing an
httpx.AsyncClientis deprecated in favour ofhttpx2.
What openapi-mcp-generator produced
npx openapi-mcp-generator --input openapi.json --output gen --transport streamable-http scaffolds a TypeScript project: package.json, tsconfig.json, .env.example, src/index.ts, src/streamable-http.ts, and a browser test page.
- Tools: 5, named by
operationId, same as above. - Descriptions: copied from the spec. The
cancelOrderdescription even contains the spec's own warning that the action is irreversible; the generated tool does nothing with that. - Schemas: query and path parameters flattened; the request body nested as a
requestBodyobject;Idempotency-Keyas a required top-level property. A model must therefore invent an idempotency key with no guidance on reusing it across retries. - Annotations: none.
- Auth: read from
API_KEY_APIKEYin.env, attached server-side.API_BASE_URLoverrides the spec's server. Correct, and well documented in the scaffold. - Results and errors: we could not observe them, because:
- Ran? The build succeeded and the server announced its endpoint, but the first request over Streamable HTTP crashed the process with
TypeError [ERR_INVALID_STATE]: Invalid state: ReadableStream is lockedfrom itsfetch-to-nodedependency, on Node 24 and again on Node 20 (its stated minimum). We did not test the stdio transport. This may well be fixed by the time you read this; the point is that a generated scaffold is a starting point you must run, not a result.
What the hand-designed servers do differently
The examples wrap the same five operations in four tools, and the differences are the whole design argument:
- Four tools, not five.
getOrderis internal:cancel_orderuses it to show the user what will be cancelled. Fewer tools means fewer wrong choices. - Descriptions that instruct. Each says when to use the tool, what it returns, what to do with
nextCursor, and what it does not do. idempotencyKey, explained. The argument exists in both, but the examples tell the model to generate one key per user request and reuse it on retry, which is the only reason it exists.- A guarded cancel.
cancel_orderrefuses untilconfirm: true, after returning the order details for the user to approve. - Annotations.
readOnlyHint,destructiveHint,idempotentHinton every tool. - Summarised results.
id,name,category, a formatted price,inStock; no raw records. - Errors that say what to do. Invalid input, not found, conflict, unavailable, each with the next step.
- Tested. The same 25-check scenario passes against all four languages in CI.
When a generator is the right tool
Generators are good at the mechanical part, and both of these did it correctly: operation names, parameter types, constraints, security scheme, base URL. If you want a quick, local, read-mostly server for your own use, FastMCP's one-liner is hard to beat, and openapi-mcp-generator gives you a scaffold to edit once it runs.
The gap is the same in both: nothing about how an assistant should behave is in an OpenAPI document, so nothing about it comes out of a generator. Tool selection, descriptions, idempotency semantics, confirmation, result shaping and error guidance are design work. That is the work the build guides show in each language, and the work API to Agents does for companies that would rather not do it themselves.
Speakeasy and Gram
Speakeasy's documentation recommends enriching the OpenAPI document (descriptions, examples, operation ids, documented errors) and offers an x-speakeasy-mcp extension to shape generated tools, with Gram as managed hosting. That is a reasonable way to push some of the design work into the spec. We did not benchmark it; if you do, the probes above (limit: 99, an unknown id, a cancel with no confirmation) are a good place to start.