send_message never collides
with anyone else’s.
Descriptions are routing rules
The normal agent uses tool descriptions to discover relevant actions and decide what to call. Write each description in two parts:- What it does — one sentence.
- When to use it — literally “Use when the user asks …”, with the phrasings people actually say.
name must be snake_case starting with a letter. Keep title short — it’s
what users see in the UI.
Faster selection with intents
Declare optional fast intents alongsidetools for
common, short Agent Mode commands. An intent supplies example utterances,
slots or fixed arguments, and a mapping to an existing tool. VoiceOS can select
that action in parallel with the normal agent, then use the same handler,
hooks, confirmation policy, and result card.
Keep useful tool descriptions: the normal agent handles requests that do not
qualify for an intent or whose selection does not win the race. Intents do not
require additional MCP tools.
Action availability and visibility
Use these optional manifest fields when an app exposes capabilities that should not all appear as editable actions in its detail modal:disabledReason is optional, trimmed, and limited to 200 characters. Without
it, the action displays “Unavailable.” Users cannot enable a disabled action
through a permission override. Remove disabled or set it to false in the
manifest and reload the app to restore access.
For an internal helper such as resolve_customer_id, set
visibility: "agent" on its normal tool definition. Keep its description and
input schema: the agent still needs them. Hiding an action does not hide a
required confirmation, grant access, or automatically opt it into
uiCallable.
These fields are additive within manifest schema version 1 and require a
VoiceOS host that supports them. Older hosts may ignore unknown fields; enforce
sensitive backend authorization in your handler as well.
Input schemas
inputSchema is standard JSON Schema (draft 2020-12).
- Describe every property. Descriptions steer the model’s argument-filling the same way the tool description steers routing.
- Mark true requirements in
required. - Validate again in your handler. Treat arguments as user input.
- For acting tools, the user can edit any bound argument on the confirmation card before it reaches you.
Handlers
Your server is a standard MCP server. Handlers receive the validated arguments and return an MCP result:- Return data and a card. The model reads the JSON; the user sees the glance card. A result the user can’t see isn’t finished — and a card the model can’t read from isn’t either.
- Configuration arrives as environment variables named after your setup fields. There is no context object.
- Throw on failure. Never fabricate data or claim success. A tool error ends
the agent’s attempt and your message is what the model explains to the user,
so make it actionable:
"ELEVENLABS_API_KEY was rejected (401). Check the key in Configure."
Sync vs background
Background tools declare both the mode and the permission:
glanceResult([...]) or widgetResult(html, { data }) in the final
MCP result, exactly as with a synchronous tool. A custom widget’s HTML and
data survive completion and reopening from the side notch. See
background completion cards.
VoiceOS creates the task handle itself. If your service starts an external
job, await or poll it in the handler until it finishes, then return the final
card. Returning only a started/job-id response completes the VoiceOS task at
that point.
Reminders with buttons
UsetriggerReminder to show a message from your app even when no agent turn
is active. Add { "kind": "notify" } to the manifest’s permissions.
Register button handlers once, before connecting your MCP server:
The click lifecycle
VoiceOS saves the message, button IDs, labels, and optionaldata before
returning { notificationId }. The receipt means queued, not seen. The card
appears below the notch when the user is free, with the installed app’s name
and icon. Buttons remain visible without hovering over the card.
A click invokes the matching handler in your MCP server process. It does
not evaluate a JavaScript string inside VoiceOS. The click authorizes that
specific button’s behavior without a separate tool-confirmation prompt;
choose a label that clearly describes the effect and keep the handler limited
to that effect.
After the handler resolves, VoiceOS marks the notification read and closes it.
If the handler fails or the app is unavailable, the card remains and displays
an error. VoiceOS never retries an action automatically.
Keep handler IDs stable and register them on every server startup. The
notification is persisted; a closure captured only when the message was sent
would be lost after reconnecting. Put the record ID in
data and load current
state in the handler. Do not put secrets in data: it is stored in notification
history.
Failures and duplicate delivery
VoiceOS permits one in-flight action per notification.defineReminderActions
also reuses the first outcome for a repeated notificationId/actionId pair
within that server process, including a rejected outcome. Up to 10,000 pairs
are retained; further new actions fail until the connection’s server restarts.
Use the same pair as a durable idempotency key in your backend for effects
that must survive process restarts.
The host waits up to 30 seconds. A timeout does not prove the operation failed:
the handler may still finish. Check the app’s state before retrying an
uncertain effect. A failed acknowledgement after a successful handler is
retried without calling the handler again while VoiceOS remains running.
Delivery and clicks require a signed-in user and a saved, enabled, connected
app with notify permission. A reminder survives app or VoiceOS restarts, but
its buttons cannot wake an offline server. Reconnect the same app to use them.
Removing the permission or disabling the app blocks clicks. Omitting
actions preserves the existing message-only behavior.
Calling the wire protocol directly
Without the SDK helper, sendvoiceos/reminders/trigger with the same options
and validate the notificationId receipt. Register a request handler on
server.server for voiceos/reminders/action. Its params are
{ notificationId, actionId, data? }; validate them, dispatch only registered
IDs, deduplicate effects, and return { ok: true } after success. Throw an MCP
error on failure. Keep callback data and routing out of the renderer; the
host chooses the originating connection from the persisted notification.
Network
Declare an egress allowlist and keep it minimal:Acting vs reading
The most important tool-design rule:Next: Result cards
Make every result worth glancing at.

