Blog

Voice Assistants and the 200-Tool Bottleneck — Why Your Agent Needs a Curated Capability, Not a Tool Dump

Jerome Louvel ·July 23, 2026
Table of contents

Recently we shipped Ikanos v1.0.0-beta1 — the first release stable enough for real integrations. The headline: build the MCP server your agent needs, not the one your vendor ships. In the last post we fused two APIs into one governed tool. This post pushes the same idea into the hardest place to run it: a real-time voice assistant with access to hundreds of tools.

The framing comes straight from a conversation with an AI consultant who builds voice-calling agents. His words: YAML-based specifications can help developers avoid the “200-tool bottleneck” — letting the AI identify and call the right tools without hand-building an integration per tool. That’s exactly the problem below.

The use case

A customer-support voice assistant answers a live phone call. Depending on where the conversation goes, it might need to look up an order, issue a refund, check a delivery ETA, book a callback, update a CRM record, open a support ticket, check a knowledge-base article, or escalate to a human. Across the CRM, the order system, the shipping API, the ticketing system, and the KB, that’s easily 150–200 tools wired into the agent “just in case.”

The golden path for a single utterance — “Where’s my order? It was supposed to arrive yesterday.”:

Identify the caller, find their most recent order, get the live shipping status, and — if it’s late — offer a callback slot. One spoken turn. Four systems. Sub-second budget.

Every one of those tools exists. The APIs support all of it. And yet the “give the agent everything” approach makes this slower, more expensive, and less reliable — precisely where voice can least afford it.

Why the tool dump breaks voice assistants

  • Tool selection is on the latency critical path. Text chat can hide a slow tool-selection step behind a “typing…” indicator. A voice call cannot — dead air is the UX. Forcing the model to reason over 200 tool schemas before it can act adds exactly the kind of turn-level latency that makes a caller say “hello? are you there?”
  • The 200-tool bottleneck degrades accuracy. More near-duplicate tools (getOrder, getOrderById, getOrderDetails, lookupOrderV2…) means more chances to pick the wrong one, or to hallucinate an argument shape. Selection accuracy falls as the catalog grows — the opposite of what a live call needs.
  • Every schema is re-sent on every turn. In a multi-turn call, the full tool catalog is re-injected into the context window on each model turn. Two hundred schemas is a large fixed tax paid before the model reasons about a single word the caller said — multiplied by every turn in the call.
  • Multi-system sequencing leaks into the transcript. With raw tools, the model itself has to call the CRM, carry the customer ID to the order API, carry the order ID to the shipping API, and stitch the result — several round-trips of transient JSON flowing through the context (and the latency budget) mid-sentence.
  • No single governance perimeter. Five upstream systems means five sets of credentials, five audit surfaces, and five ways for a voice agent to do something it shouldn’t — with no single answer to “which agent, which identity, which cost, which call?”

None of this is intrinsic to voice, or to MCP. It’s intrinsic to handing an agent a raw, un-curated tool catalog and asking it to be the integration layer in real time.

The Ikanos answer: a handful of task-shaped tools

Ikanos is spec-driven: you declare the capability in YAML, and the engine serves it as an MCP server (and REST, and Skill) with no code generation. Crafter, our VS Code extension, validates and lints the spec as you type.

The move is simple: instead of exposing 200 low-level CRUD tools, expose a small set of tools shaped like what the assistant actually does on a call — and let each one fuse the underlying systems server-side.

First, consume the systems — each a consumes adapter with its own auth. The agent never sees these operations directly:

ikanos: "1.0.0-beta2"
info:
  display: "Support Voice Assistant"
  description: "Task-shaped tools for a real-time support voice agent"
  tags: [Voice, Support, DevEx]

capability:
  consumes:
    - namespace: crm
      type: http
      baseUri: https://crm.internal/v2
      authentication:
        type: bearer
        token: "{{CRM_TOKEN}}"
      resources:
        customers:
          path: /customers
          operations:
            find-by-phone:
              method: GET
              inputParameters:
                phone: { in: query }

    - namespace: orders
      type: http
      baseUri: https://orders.internal/v1
      authentication:
        type: bearer
        token: "{{ORDERS_TOKEN}}"
      resources:
        orders:
          path: /orders
          operations:
            latest-for-customer:
              method: GET
              inputParameters:
                customerId: { in: query }
                limit:      { in: query, value: "1" }

    - namespace: shipping
      type: http
      baseUri: https://ship.internal/v1
      authentication:
        type: bearer
        token: "{{SHIP_TOKEN}}"
      resources:
        tracking:
          path: /tracking/{{trackingId}}
          operations:
            get-status:
              method: GET
              inputParameters:
                trackingId: { in: path }

    - namespace: scheduling
      type: http
      baseUri: https://calls.internal/v1
      authentication:
        type: bearer
        token: "{{CALLS_TOKEN}}"
      resources:
        callbacks:
          path: /callbacks
          operations:
            offer-slots:
              method: GET
            book-slot:
              method: POST
              body: |
                { "customerId": "{{customerId}}", "slot": "{{slot}}" }

Now the orchestration — one flow that turns “where’s my order?” into a single, spoken-ready answer. This is the sequencing that used to leak into the transcript, now running server-side:

  aggregates:
    - display: Order Status
      namespace: support
      flows:
        where-is-my-order:
          description: Identify caller, find latest order, get live shipping status, offer a callback if late.
          semantics:
            safe: true          # read-mostly; the callback offer is a proposal, not a booking
            idempotent: true
          inputParameters:
            phone: { type: string, required: true }
          steps:
            identify:
              type: call
              call: crm.find-by-phone
              with: { phone: support.phone }
            latest-order:
              type: call
              call: orders.latest-for-customer
              with: { customerId: $.identify.id }
            track:
              type: call
              call: shipping.get-status
              with: { trackingId: $.latest-order[0].trackingId }
            offer-callback:
              type: call
              # Only reach for slots when the order is actually late.
              when: $.track.status == "delayed"
              call: scheduling.offer-slots
          outputParameters:
            # A flat, speakable shape — the model doesn't re-derive it mid-call.
            orderId:     { type: string,  mapping: $.latest-order[0].id }
            status:      { type: string,  mapping: $.track.status }
            etaText:     { type: string,  mapping: $.track.humanEta }
            isLate:      { type: boolean, mapping: $.track.status == "delayed" }
            callbackSlots: { type: array, mapping: $.offer-callback.slots }

Finally, expose only the handful of task tools the assistant needs — with hints that tell the model exactly how safe each one is, so it can act without a confirmation round-trip:

  exposes:
    - type: mcp
      address: 0.0.0.0
      port: 9096
      namespace: voice
      authentication:
        type: oauth2          # Your identity, on a transport your gateway understands
      tools:
        order-status:
          display: Check order status for a caller
          description: >
            Given a caller's phone number, returns their latest order's live
            shipping status and ETA, plus callback slots if the order is late.
          call: support.where-is-my-order
          hints:
            readOnly: true      # safe to call speculatively while the caller talks
            idempotent: true
        book-callback:
          display: Book a callback slot
          description: Books a specific callback slot for the caller.
          call: support.book-callback   # a second small flow, omitted for brevity
          hints:
            destructive: false           # a write, but non-destructive
            idempotent: true
    - type: control           # Health, metrics, traces — governance out of the box
      address: 0.0.0.0
      port: 9199
      management:
        health: true

Run it:

ikanos validate ./support-voice-assistant.yaml
ikanos serve    ./support-voice-assistant.yaml

The voice agent now sees two toolsorder-status and book-callback — not two hundred. Every painpoint is answered:

  • Tiny selection surface, off the critical path. With a handful of task-shaped tools, the model picks one in a single, cheap decision — no reasoning over 200 near-duplicate schemas mid-utterance.
  • Sequencing runs server-side. The CRM → orders → shipping → scheduling chain executes inside one flow; the IDs never touch the context window or the latency budget.
  • Speakable output shapes. The flow returns a flat { status, etaText, isLate, callbackSlots } — the model reads it aloud instead of re-deriving JSONPath while the caller waits.
  • Hints unlock confident action. readOnly + idempotent tell the agent order-status is safe to call speculatively; it can start the lookup the instant it hears “my order” instead of asking permission.
  • One governance perimeter. OAuth 2.1 on the exposure, a control port emitting health and traces, credentials pulled from your own secrets — one surface across five systems, not five audit gaps.

The latency-and-token math: why “a few tools” is also faster and cheaper

For voice, latency and cost move together, because both are dominated by what’s in the context window on every turn.

1. Tool-schema tokens — the per-turn tax you pay before hearing a word. Every tool’s name, description, and schema is re-injected on each model turn. Two hundred tools is a large constant overhead paid before any reasoning about the caller’s utterance. If a full catalog runs ~40k tokens and two task tools ~600, that’s roughly a 60× reduction on the per-turn floor — and in voice that floor is also time-to-first-token, i.e. dead air.

2. Orchestration tokens — round-trips that never needed the model. When the agent is the integration layer, the customer record, the order list, and the tracking payload all flow through its context so it can forward IDs — several thousand tokens and several sequential model round-trips per utterance. Inside an Ikanos flow that data moves server-side in one hop; it never enters the context window and never adds a turn.

3. Retry-and-repair tokens — the worst offender, and audible. A mis-picked tool or a wrong argument shape is paid for twice — the failed call plus the retry — and on a phone line the caller hears the extra pause. A curated, hint-annotated tool set drives that repair rate toward zero, removing both the tokens and the silence.

Net: the tool-dump path spends most of its budget on schema overhead and shuttled payloads on the latency critical path; the capability path spends it on the one decision that matters — calling order-status. For voice this is a double win: fewer tokens per turn and fewer turns, which is the difference between a snappy assistant and one the caller talks over.

And because the capability owns the spec, not the server, the stateless MCP transport revision expected this summer is a one-line engine bump — no change to your YAML.

The mini-spike: “tame the tool catalog in an afternoon”

Want to prove it before committing? A scoped spike — one engineer, half a day.

Goal. Ship an MCP server exposing two task-shaped tools (order-status, book-callback) that a voice agent can drive in real time, replacing a sprawling multi-system tool catalog.

Timebox. 4 hours.

Prerequisites.

  • Ikanos v1.0.0-beta2 (the prebuilt native binary or the Docker image).
  • Read access to two or three back-end systems (a CRM, an order API, a tracking API) — mocks are fine for the spike.
  • A voice or chat client that reports tool count and, ideally, token usage per turn.

Steps.

  1. Baseline the pain (30 min). Point your agent at the raw tools from all systems. Note the tool count, the per-turn token floor, and eyeball selection mistakes on a few utterances. This is your before-picture.
  2. Consume (60 min). Write the consumes adapters for each system. ikanos validate, then confirm each low-level operation returns what you expect.
  3. Fuse (60 min). Write the where-is-my-order aggregate. Get the JSONPath right against your real payload shapes; confirm the flat, speakable output shape comes back in one call.
  4. Curate + hint (45 min). Expose only order-status (and a stub book-callback) as MCP tools with readOnly/idempotent hints. Add the control port.
  5. Serve + drive (30 min). ikanos serve, then run the same utterances from step 1. Count tools now: two, not two hundred.
  6. Compare (15 min). Two hundred tools vs. two, N sequential round-trips vs. one flow, five audit gaps vs. one control port. Record the per-turn token count for both paths — the schema tax is visible before the first real tool call.

Definition of done. A voice or chat client resolves “where’s my order?” through a single order-status tool call — with the CRM/order/shipping sequencing hidden server-side — and the control port answers which agent, which identity, which cost.

Stretch goals. A book-callback write flow with a destructive: false hint; the same flow exposed as REST for an IVR webhook; a pre-built template for a common system (a “Notion export” tool is a great candidate) so the next capability starts from a curated shape instead of a raw tool dump.

The takeaway

The problem was never that the voice assistant couldn’t do the task — every API supported it. The problem is that a raw catalog of 200 tools puts the integration in the model’s context window, on the latency critical path, exactly where a live call can least afford it: slow to select, expensive per turn, and audible when it goes wrong.

A capability puts it back where it belongs: a versioned spec exposing a handful of task-shaped tools, fusing as many systems as the job needs server-side, governed on one surface. That’s the difference between an agent drowning in tools and an agent with the few tools a conversation actually needs.