Chataway Developers
Concepts

Cards & approvals

How your app asks the user for things — approval cards, form cards from MCP elicitation, and result cards — and the rule that anything with consequences waits for a tap.

A card is a small piece of UI Chataway draws on your app's behalf: an approval, a form, a gallery of results, a notice. Chataway renders every card itself, in its own design, with your app's icon and name on it — so the user always knows who is asking, and no app can fake another app's (or Chataway's) prompt.

#The one rule: consequences need a gesture

Anything that can't be undone by turning your app off — spending money, uploading, posting, sending, deleting something on your service — happens only after the user taps a button on a card or in your panel. The agent can propose; only the user can approve.

You get this with one line of manifest: confirm on the tool. The review team checks that tools with side effects have it (see Review guidelines).

Your tool…Needs confirm?
Searches, lists, reads, previewsNo
Creates a draft only the user can seeUsually no — say "draft" in the description
Uploads, publishes, sends, posts, deletesYes
Spends money or creditsYes, and say how much in body

#Approval cards (confirm)

json
{
  "id": "publish_post",
  "name": "Publish post",
  "description": "Publish a draft post to the user's blog.",
  "confirm": {
    "title": "Publish this post to your blog?",
    "body": "It will be public immediately and emailed to 1,204 subscribers.",
    "action": "Publish"
  }
}
Field
titleRequired, ≤ 120 characters. {{count}} is replaced with the number of files handed over.
bodyOptional Markdown under the title. Say what will happen, where, and what it costs.
actionLabel of the approve button (default Continue). Use a verb: Upload, Publish, Send.

The card blocks the tool call. Approve → the call proceeds to your server. Cancel → your server is never called and the agent gets "The user did not approve … — nothing was sent". For a tool with file params, the card shows thumbnails of the (already prepared) files; {{count}} works in body too.

#Forms (elicitation)

When you need an answer you can't get from the arguments, ask for it with MCP elicitation from inside your tool handler. Chataway renders the request as a form card; the call continues when the user submits.

ts
app.tool('import_contacts', { input: { csv: fileParam() } }, async ({ csv }, ctx) => {
  const answer = await ctx.elicit('Which workspace should the contacts go into?', {
    type: 'object',
    properties: {
      workspace: { type: 'string', title: 'Workspace', enum: ['sales', 'support', 'ops'], enumNames: ['Sales', 'Support', 'Ops'] },
      tag: { type: 'string', title: 'Tag (optional)' },
      notify: { type: 'boolean', title: 'Notify the team', default: false },
    },
    required: ['workspace'],
  });
  if (answer.action !== 'accept') return 'The user cancelled the import.';
  // answer.content = { workspace: 'sales', tag: '', notify: false }
  // …
});

ctx.elicit is the server helper; with the raw MCP SDK, send an elicitation/create request from your handler.

How the schema maps to form fields:

JSON SchemaForm field
stringText field (description becomes the placeholder when there's also a title)
string with enum (or oneOf of consts)Select; labels from enumNames (or each option's title)
number / integerNumber field (minimum / maximum enforced)
booleanSwitch
requiredRequired fields must be filled before Submit
titleField label (falls back to description, then the key)
defaultPre-filled value

Only flat objects of those types are supported — no nested objects or arrays, per the MCP elicitation spec. The card's buttons are Send and Decline; the message is its title (or its body, when longer than 120 characters). The result is { action: 'accept', content } (values coerced to the schema's types), { action: 'decline' }, or { action: 'cancel' } if the card was dismissed or timed out.

#Result cards (_meta)

A tool result can ask Chataway to show a card next to it by setting _meta["chataway/card"]. Two kinds are available to cloud apps:

ts
// A notice: Markdown text with a title.
_meta: { 'chataway/card': { kind: 'notice', title: 'Deployed', body: 'Preview: https://preview.example.com/p/42' } }

// A gallery: your own title and text for the files this result returned.
_meta: { 'chataway/card': { kind: 'gallery', title: 'Your stickers', body: 'Die-cut, 1024 px, transparent.' } }
Field
kindnotice or gallery
titleShort headline
bodyMarkdown
itemsAccepted by the SDK types but not rendered yet — a gallery card shows the files your result returned.

Result cards never block. Files you return are always shown as a gallery — titled "{App}: 3 files" — with Add to Media library and Save to project buttons; a gallery _meta card just gives that gallery your own title and text. To show images you host without returning them, return resource_links on your domains (Chataway downloads them) or put links in a notice.

Two cards are raised by Chataway itself, on your behalf:

  • Connect — a tool that declares account was called but the account isn't connected (or its refresh failed). The card has your icon and a Connect {label} button that starts the OAuth flow. The tool call waits.
  • File hand-off consent — the first time files move from one owner to another in a project. See Files & hand-off.

#Where cards appear

ChatCards
Chataway chatInline, on the tool call that raised them
Claude Code, Codex, Grok, CursorIn the pending-asks strip above the composer
AnyIn your panel, if it lists them with the bridge

On the phone, cards appear the same way and can be answered there — useful for approving a long-running agent's upload from the couch.

#Answering cards from your panel

Your panel can list your app's cards (listCards), watch for new ones (on('card')) and answer them (respondCard) — for example a queue view of pending uploads. The panel can only see and answer your cards; answering from the panel is a user gesture by definition, because it happened in your UI.

#Writing good cards

  • Say what will happen, not what the tool is called. "Upload 3 images to Acme?" beats "Run upload_images?".
  • Put consequences in the body. Public or private, reversible or not, how much it costs.
  • One card per decision. Don't chain confirm → form → confirm; ask everything in one elicitation, or make the arguments carry it.
  • Use verbs on buttons. Upload, Publish, Send.