A REST API with thousands of endpoints will generate thousands of functions for an agent if you run the usual OpenAPI generator. Speakeasy’s default is still that mapping. Cloudflare measured their own API at 2,594 operations and 1,170,523 tokens with full schemas, which does not fit a 200k context window.
A single list-endpoints function is cheaper than that, and it still dumps the catalog. One deployment fronts dozens of APIs, some with hundreds of endpoints each. Asking the agent to list them fills the window with operations the current question does not need, unless the agent already knows which operation to call. The spec has to be an index the agent can search. A query for “list customers” should return the retrieve operations from every catalog in reach: Salesforce GET /customers, a billing listAccounts, a CRM searchContacts. Then one invoke.
I built that path in mcp-data-platform, also hosted as Plexara. This post is the pattern, as it sits in pkg/toolkits/apigateway, parsed by kin-openapi v0.147.0.
This is the ninth post in MCP by Design. The earlier notes covered Go, composition, steering, knowledge, testing, Starlark, Hive, and session handles. This one is the index. It is grounded in the open-source
txn2/mcp-data-platform, also available hosted as Plexara.
§One Function Per Endpoint
The mapping is mechanical. Each OpenAPI operationId becomes a tool name, the parameter schema becomes the tool’s input schema, and tools/list grows with the spec. Speakeasy’s generator still emits that shape from speakeasy quickstart --mcp: one TypeScript file per endpoint, then the operator prunes. Stainless’s original generator did the same, enable_all_resources: true, POST /products becoming create_product. Kong and Apigee will project an existing API as MCP at the gateway without a generated repo at all. The default is the spec, 1:1.
That default has a measured cost. Anthropic, in Code execution with MCP (4 November 2025), reported that agents connected to thousands of tools process hundreds of thousands of tokens before they read the request, and that one workflow dropped from 150,000 tokens to 2,000 once the model wrote code against the API instead of loading every tool definition. A later post, advanced tool use (24 November 2025), put traditional tool loading at 134k tokens before their on-demand search tool. Cloudflare published the large-API exhibit: the Cloudflare API MCP server table is 2,594 native tools and 1,170,523 tokens with full schemas, 244,047 with required parameters only, and about 1,000 tokens in Code Mode (20 February 2026) with two tools, search and execute. The OpenAPI document itself is about two million tokens if you paste it.
The other published answer is not a smaller generator but a different surface. WorkOS, in Designing an MCP server from a REST API (30 April 2026), starts from agent goals and maps a 24-endpoint toy API onto eight tools, three resources, and two prompts. Roland Huß’s MCP tool design patterns argues the same point: an API is not an MCP tool, and wrap-every-endpoint accuracy falls once the list crosses roughly ten to thirty tools. CData’s Jerod Johnson, Why Your MCP Server Shouldn’t Mirror Your API (29 May 2026), wants capability-shaped tools. Stainless now ships a code-execution tool plus a docs-search tool. Cloudflare’s Code Mode is JavaScript against the spec, run in a Worker isolate.
Those are real alternatives. Code-mode wins on a single product API the vendor owns, where the model can import a typed client and the sandbox is the product. Intent-shaped tools win on one API the team designed, where plan_week_meals is a better verb than GET /recipes. The remaining case is many vendor APIs whose shapes nobody in the room controls: Salesforce plus a weather service plus a county GIS plus an internal billing API. You do not rewrite those as eight intent tools, and you do not give the agent a JavaScript sandbox against each of them. You keep the OpenAPI documents and make them searchable.
Kin Lane counted whether that mapping had landed in the documents themselves. On 19 August 2026 he scanned 17,615 OpenAPI documents from 7,213 providers as published, and found MCP-related x- extensions on ten of those providers, 0.14 percent. The mapping is not in the spec. It is in the generator, or it is an index over the spec.
This piece does not wrap Salesforce as one tool per CRM object. It does not claim four tools beat Code Mode on Cloudflare’s own API. It covers a working slice: index the documents, search them, call one operation.
§Listing Is Still the Dump
Collapsing 2,594 functions into one list_endpoints tool is the obvious next step. It is cheaper. It is also still a dump.
A scoped list, one connection, fifty operations, a filter by spec section, is the right drill-down once the agent already knows which API it is on. Across dozens of connections it is the wrong first move. The agent either spends the turn reading operations the question did not ask for, or it already had enough context to skip the list and name the operation. Search has to be first. The list stays for the section the search just pointed at.
§Index the Document
Each operation needs a vector when the spec is saved, not when the agent asks. The text that goes into the embedding is the text an agent is likely to say, not the HTTP verb:
- summary first, because that is the sentence the API author wrote for humans
- description next, when they bothered
- path, so domain nouns like
customersandinvoicesland in the vector - tags
Leave the method out. “list customers” should match GET /customers and POST /customers/search because of the summary and the path, not because the query contained GET.
In the Go server that looks like this. The comment is the argument:
func buildEmbedText(op OperationSummary, description string) string {
parts := make([]string, 0, 4)
if op.Summary != "" {
parts = append(parts, op.Summary)
}
if description != "" {
parts = append(parts, description)
}
if op.Path != "" {
parts = append(parts, op.Path)
}
if len(op.Tags) > 0 {
parts = append(parts, strings.Join(op.Tags, " "))
}
return strings.Join(parts, " ")
}
Do not bake the deployment’s base path into that string. A sandbox and a production org that share a spec should share the vectors. Changing https://kanplan.example.gov/... to https://geo.example.gov/... is a connection problem, not a re-embed.
Store the vectors on the catalog, keyed by spec name and operation_id, not on the connection. Every connection that mounts salesforce-rest / 2024-10 reuses the same index. Embed at save time, off the request path, in batches small enough that a 300-operation spec does not time out the provider. If the index is not ready yet, fall back to substring match and say so. Do not stall the agent on a job queue.
Substring match is the floor. It misses when the agent’s phrasing does not share vocabulary with the spec author. “create order” does not match a summary of “Place a new order”. Pure cosine finds the neighbor and loses the exact path hit. A blend that leans semantic and still credits a substring match covers both. In this server that blend is 0.6 cosine and 0.4 lexical, and it is the default once the index exists.
§Search Across Catalogs
One search call, not one search per API. The query fans across every catalog the caller is allowed to see and returns a short ranked list. The hit is a pointer: method, path, summary, and the operation_id the invoke will use. The full schema stays out of the turn until the agent asks for that one operation.
“list customers” is scored against Salesforce, billing, CRM, a weather API, and whatever else is mounted. Operations that retrieve customers rise. Operations that delete invoices or rotate keys do not occupy the window.
Two filters, both fail closed. Hide operations the caller’s role could not invoke, so the listing and the call cannot disagree. Hide whole connections the caller is not granted. Count what you hid. A shortened list that does not say so reads as “does not exist.”
In this server that search is the same search tool that already fans across the warehouse catalog and saved knowledge. API operations are in that corpus by default. The agent does not have to discover that a gateway exists, list connections, and query each one. A scoped api_list_endpoints remains for the drill-down, the way a catalog browser remains next to a catalog search.
§Call the One You Meant
After the hit, the agent needs a small, stable surface that does not grow when you add an API. Four calls are enough:
- list the specs in a catalog (Drive, Calendar, Gmail, Admin, or a single
default) - list operations in one spec, optionally ranked by a query
- read the schema of one
operation_id(parameters, body, responses; no auth schemes, the connection already has credentials) - invoke that
operation_id
In this server those are api_list_specs, api_list_endpoints, api_get_endpoint_schema, and api_invoke_endpoint. Adding ten catalogs does not add a thousand tools. tools/list stays four names.
Address the invoke by operation_id and path_params, not by concatenating /v1/users/ and 123. The catalog already has /v1/users/{id}. The id that ranked is the id that calls:
{
"connection": "nws",
"operation_id": "getGridpointForecast",
"path_params": {"office": "EAX", "gridX": "50", "gridY": "60"}
}
OpenAPI path templates are not always one placeholder per slash. National Weather Service uses /points/{latitude},{longitude} and /gridpoints/{office}/{gridX},{gridY}/forecast. Substitute per placeholder, escape the values, leave the comma alone. Refuse an empty value, report every missing name at once, and refuse a stray key that matches no placeholder. Echo the resolved path on the result. A wrong catalog prefix otherwise returns as a generic upstream 400 whose cause is invisible.
The Starlark weather-watch script already calls this way. platform.call("api_invoke_endpoint", { "connection": "nws", "operation_id": "getGridpointForecast", "path_params": ... }) is an ordinary tool call. Search is not a chat-only trick.
Invoke is gated. A call without a session handle is refused, same as a warehouse query. That refusal is the previous note. Search does not need the handle; it is how the model finds the id it will later invoke.
An upstream HTTP status is a successful proxy. 404 and 500 come back as data. A timeout or a dropped connection is the actual failure. Large bodies do not belong in the model context; stream them to a file the agent can point at.
Caution: if the operation declares multipart/form-data, assemble the parts in the server. A hand-built multipart body with a Content-Type whose boundary does not match the bytes arrives upstream as zero parts, which surfaces as a 400 blaming the caller.
§The Catalog Is Not the Credential
An OpenAPI document describes the API. A connection is a base_url plus a credential pointed at one deployment of that API. A Salesforce sandbox and a Salesforce production org are two connections and one catalog. Paste the spec twice and the copies drift. Index it once.
flowchart TB
subgraph cat["catalog: salesforce-rest / 2024-10"]
S["the OpenAPI document"]
V[("one vector per operation")]
end
SB["sandbox connection"] --> cat
PR["production connection"] --> cat
Q["search: list customers"] --> SB
Q --> PR
Version the catalog. salesforce-rest / 2024-10 stays for connections that have not moved. A breaking schema change is a new row, 2025-01, not an overwrite. The index follows the document; connections move when the operator is ready.
Vendor specs are messy. kin-openapi will reject documents that Swagger UI, Postman, and Insomnia accept: example-versus-schema drift, ECMA regex lookahead, PascalCase String from .NET generators, arrays with no items. Parse strictly enough that operation ids and path templates are real. Relax the documentation-only checks. Do not follow external $ref at parse time; a pasted spec that points at a private URL is an SSRF. Fetching a spec from a URL is a separate path with the usual guards: HTTPS, no private ranges, no redirects, a body cap.
The connection still authenticates: bearer, API key, basic, OAuth, or mTLS. The agent never handles that credential. How the connection is chosen, and how OAuth is brokered without turning the MCP server into an IdP, are later notes.
§Summary
One function per endpoint is the generator default. One list of every endpoint is the next default, and it still fills the window once a deployment has dozens of APIs. Index each OpenAPI document. Search that index. “list customers” returns retrieve operations across every catalog the caller can see. Four calls inspect and invoke after the hit. They do not grow with the spec. The id that ranked is the id that calls, including templates like /points/{latitude},{longitude}. The catalog is shared across environments; the credential is not.
In mcp-data-platform that is pkg/toolkits/apigateway and the endpoints group on search. The calls still need a credential model for the connection they run against, and some of those connections need OAuth.