Skip to main content
Tools are the whole interface between the agent and your code. Each one is declared in the manifest and implemented in your MCP server under the same name. VoiceOS namespaces them per integration, so your 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:
  1. What it does — one sentence.
  2. When to use it — literally “Use when the user asks …”, with the phrasings people actually say.
A vague description doesn’t make your tool a fallback. It makes it invisible.
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 alongside tools 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:
Three conventions:
  • 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:
Declaring the mode without the permission is a validation error. Return 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

Use triggerReminder 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 example uses a placeholder API; replace it with your service, declare its network permission, and collect credentials through setup fields. The same helpers work with remote MCP servers.

The click lifecycle

VoiceOS saves the message, button IDs, labels, and optional data 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, send voiceos/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:
Permissions are shown to the user at install time — a short list reads as trustworthy, a long one reads as a liability.

Acting vs reading

The most important tool-design rule:

Next: Result cards

Make every result worth glancing at.