Skip to content

tutorial

Chapter 5 of 5

Chapter 5 — Shaping for the channel

by Rod Rivera Published

The tool returns rows; the caller hears a sentence. Formatting is a skill decision made once, which is why the transport swap did not disturb it.

integrations.yml has declared two channels since the REST version:

channels:
  rest:
    enabled: true
  inspector:
    enabled: true

The agent answers on both, and the tool result is identical on both. What differs is what a reader can actually take in — and that difference is settled in the skill, not in the channel.

The case that shows it

list_open_tickets returns structured rows:

{
  "ok": true,
  "count": 2,
  "tickets": [
    {
      "id": "9001",
      "subject": "Invoice 4471 shows the wrong VAT rate",
      "stage": "waiting_on_us",
      "created": "2026-08-11"
    },
    {
      "id": "9002",
      "subject": "Add a second admin to the account",
      "stage": "waiting_on_contact",
      "created": "2026-08-19"
    }
  ]
}

In the Inspector that renders as a tidy little table and looks finished. Pushed through the REST channel it arrives as one run of text. Read aloud by a voice channel it would be unusable: nobody can hold waiting_on_contact and a ticket id in their head while the next row starts.

The instruction that fixes it

check_tickets does not say “return the ticket list”. It says:

- Success with tickets: read back each subject and its stage in plain language.
  `waiting_on_us` means Meridian owes them a reply; `waiting_on_contact` means
  the ticket is waiting on the customer.

Three decisions are packed into that, and each one is about the listener rather than the data:

  1. Read back the subject, not the id. 9001 means nothing to the caller.
  2. Translate the stage. waiting_on_us is a database value; “we owe you a reply” is an answer.
  3. Plain language, not a table. The narrowest channel the agent serves decides the shape, and a table does not survive being spoken.

The result is a sentence that works everywhere:

bot  Two. Invoice 4471 shows the wrong VAT rate, waiting on us. And a request to
     add a second admin, waiting on you.

Shape once, in the skill

The tempting alternative is a per-channel template: a table for the Inspector, a sentence for voice, something else for REST. Resist it for as long as you can. Every channel you add multiplies the surfaces where the wording can drift, and the version a reader complains about is always the one nobody remembered to update.

Writing the constraint into the instructions once means the model produces an answer shaped for the narrowest consumer, and the wider ones display it fine. You lose the tidy table in the Inspector. You gain one description of what a good answer sounds like.

The connection to the transport swap

This is the same property as chapter 4’s constraint, seen from another angle.

Formatting was decided against the result shape — the tool returns tickets with a subject and a stage — and not against the transport that carried the result. So when the transport changed, the formatting instruction did not have to, because nothing it depended on moved. subject and stage are still there, still spelled the same way, still meaning the same thing.

Three things in this project turned out to be portable across the swap: instruction prose, tool constraints, and output shaping. All three were written against the interface. The one thing that was not portable — the tool reading project.contact_id out of memory — was written against the implementation.

That is the whole lesson, and it is not really about MCP:

Write against the interface and the transport is an implementation detail. Write against the implementation and it is not.

MCP is just the change that made the difference visible.

Where to go next

  • Run make mcp-prove on your own fork and break it three ways, as in chapter 1. A proof you have not seen fail is not one yet.
  • Point mcp_servers: at a real server. HubSpot’s own is two lines of YAML away, and the three skills do not change for it either.
  • Audit your own tools for chapter 3’s rule: which of them depend on a value the caller must not be able to influence? Those stay local.