There is a command for it now. Point a generator at an OpenAPI document, get an MCP server, connect an agent. Stainless, Speakeasy, APIMatic and Mintlify all ship it; runtime translators do the same without codegen. It takes under a minute and it demos beautifully.
It is also, most of the time, the wrong artifact — and the reason is one sentence: a projection that faithfully mirrors an API inherits the API’s size and shape.
The import itself is the correct instinct. Your OpenAPI document is the most accurate machine-readable description of your system that exists, and re-typing it by hand would be absurd. It is the right input. The mistake is treating it as the output.
Size is the complaint everyone makes
A 200-operation API becomes 200 tools, all loaded before the model reads a word of the request. Anthropic measured it: “In cases where agents are connected to thousands of tools, they’ll need to process hundreds of thousands of tokens before reading a request.” And it is only the first bill — definitions are re-sent every loop, so an oversized catalog compounds quadratically with the number of turns.
Cloudflare’s engineering team put the consequence plainly: “MCP server designers are encouraged to present greatly simplified APIs as compared to the more traditional API they might expose to developers.” That is the argument against one-click import, stated by the people building the runtime. The recommendation is to simplify; the importer’s whole value proposition is that it doesn’t.
Shape is the one that breaks deployments
Cut the catalog to twenty tools and size is manageable. Shape is untouched, because it was never about the count.
An OpenAPI document describes resources. An agent needs jobs. Anthropic states the target almost as a recipe: “Instead of implementing a list_users, list_events, and create_event tools, consider implementing a schedule_event tool which finds availability and schedules an event.”
Read that as a spec for a generator and it becomes obvious why none can satisfy it. schedule_event is not a transformation of three endpoints. It is a decision — that these three, in this order, with this conflict rule, are one unit of work worth exposing. That lives in your product knowledge, not in the document.
“But we only import the twelve we need”
Nobody defends importing all 200. The mature version is selective import: hand-pick the dozen operations the agent plausibly needs. Twelve tools, and the context bill drops by an order of magnitude.
It is a real improvement, and the right thing to do with the upstream half. It also leaves the problem untouched:
Selection is a subtraction operator. The tools an agent needs are not a subset of your endpoints.
You cannot select your way to get_customer_context, because it is not in the list. It is not endpoint #7 you forgot to tick — it is three endpoints and the decision to fuse them. Subtraction gets you a shorter list of the same kind of thing; the agent needed a different kind of thing. So:
- The unit is still a resource, not a job. The agent still plans the sequence, holds intermediate IDs and joins results. Fewer tools does not mean fewer turns, and turns are the expensive axis.
- Payloads are untouched. You selected which endpoints to expose, not what they return. The sixty-field response is still sixty fields.
- Cross-system jobs stay impossible. Selection operates inside one document; the job spans three.
- The selection has no home. It lives as a CLI flag or ticked boxes. Upstream renames an operation and nothing tells you — and the valuable part, why these twelve, is discarded.
Choosing which upstream operations to bring in is a decision about consumes — what you may reach. The agent-facing surface is exposes, and selection has no opinion about it. Selective import answers the upstream question well, then presents its answer as though it were the downstream one.
“Then we’ll annotate the document”
The next move is cleverer: if the document lacks agent metadata, add it. OpenAPI sanctions this — specification extensions, any x- field — so teams attach a better description, a readOnly hint, a friendlier name, and generate from that.
For metadata this genuinely works. But an extension hangs off an operation, so it can describe an endpoint better or flag it read-only. It cannot say “these three, in this order, returning only these five fields.” There is no operation to hang that on: it is a statement about a relationship between operations, and the document has no place to put one.
The OpenAPI Initiative reached that conclusion itself. Rather than extend OpenAPI to express call sequences, it published Arazzo — a separate specification for defining “sequences of calls and their dependencies… in the context of delivering a particular outcome.” The people who own the extension mechanism looked at composition and concluded it needed its own document.
The composition an agent needs is not annotation-shaped. It is a second artifact — and once you are writing one, the only question is whether it also carries the credential scope, the response shaping and the exposed surface, or whether you maintain those in three more places.
Two costs come free with the approach. Extensions are advisory, so tooling that does not know your prefix ignores them and the same document yields different tools per generator. And they put agent concerns inside the API’s contract, reviewed on the API’s cycle — where a spec regenerated from code annotations silently drops every hand-written block.
The escape hatches
Dynamic tool discovery helps the token bill, but selection still happens at runtime inside a probabilistic model, and search needs a query — so the agent must already know your vocabulary. Retrieval doesn’t remove curation, it relocates it.
Code execution makes an oversized, badly shaped surface cheaper to traverse; it does not make it a good surface. The joins still happen in model-authored code, written fresh each run. Ask which systems can this reach? and the answer is still “whatever it writes next time.”
What a designed surface looks like
This is where the analysts land from the architecture side too — scope the surface to the agent’s task, not the product, as argued in Applied Capabilities.
An Ikanos capability is that surface written down as one declarative file: consumes (which systems, under which credentials), aggregates (how calls compose into one unit of work, off the model’s critical path), exposes (the agent-facing surface, as MCP, REST or Agent Skill), and binds (credentials, separated from logic).
aggregates is the section a generator cannot write, and it is the entire point. Because output parameters are declared per operation, the sixty-field payload becomes the five the task needs — projected at the boundary rather than compressed after arrival.
Concretely, the get_customer_context that no importer could produce:
capability:
consumes:
- import: crm
from: ./shared/crm.yml # generated by `ikanos import openapi`
- import: billing
from: ./shared/billing.yml # a different document entirely
aggregates:
- display: "Support Desk"
namespace: support
flows:
get-customer-context:
description: "Everything an agent needs before answering a billing question"
semantics:
safe: true
idempotent: true
inputParameters:
customer-id:
type: string
required: true
steps: # three upstream calls, one tool call
get-customer:
type: call
call: crm.get-customer
with:
id: "{{customer-id}}"
list-invoices:
type: call
call: billing.list-invoices
with:
customer: "{{customer-id}}"
list-tickets:
type: call
call: crm.list-open-tickets
with:
customer: "{{customer-id}}"
mappings: # five fields, not sixty
- target: name
value: "$.get-customer.display_name"
- target: plan
value: "$.get-customer.subscription.plan_code"
- target: unpaid-invoices
value: "$.list-invoices.data[?(@.status=='open')]"
- target: open-tickets
value: "$.list-tickets.data.length"
- target: account-owner
value: "$.get-customer.owner.email"
exposes:
- type: mcp
port: 3001
namespace: support-copilot
tools:
get-customer-context:
description: "Fetch a customer's plan, unpaid invoices and open tickets"
ref: support.get-customer-context
Three endpoints across two OpenAPI documents become one tool call, returning five fields. The consumes half was imported; everything below it is the decision an importer cannot make.
There is no control flow in the file, which is what makes it reviewable: a reader can enumerate every operation it can reach before anything runs.
What Ikanos’s importer will not do
Ikanos ships ikanos import openapi. Its own description is “Import an OpenAPI specification into an Ikanos consumes YAML file.” It writes consumes — the upstream half. It does not emit tools, it does not emit exposes, and it will not hand you an MCP server.
That is not a missing feature; it is the position. Import saves you from re-typing a contract that already exists, then stops exactly where judgement begins.
When the mirror is right
Exploration in a sandbox, to learn which operations matter. Genuinely small APIs whose endpoints already correspond to jobs. Exhaustive coverage as the real requirement, like an internal typed SDK — a generator is right there and Ikanos is wrong. None of the three is “we have an agent in production and this was the fastest way to give it tools.”
The rule
The document is the input. The capability is the output. Import is where the work starts, not where it finishes — and every minute it appears to save is repaid in tokens, retries, and the audit conversation you cannot have because the surface only exists at runtime.
Start here: the Ikanos documentation covers consumes, aggregates and exposes in full, and the playground runs a capability in the browser with nothing to install.
Related reading: Applied Capabilities on scoping a surface per agent, Agents Undid Twenty Years of API Curation on why the design-time step disappeared, and Beyond iPaaS on the four questions to ask any integration layer.