Quick links

Give your AI agent a way to reach your phone.

Zona MCP is a small stdio server with six tools. When a long job ends, your agent calls zona_notify and the alert is waiting in your Zona inbox. The zona command in the same download tells your phone when a coding agent needs you. The source token is read from a file on your machine and never enters the conversation.

v1.2.2 · Node 20+ · MIT · 69.5 KB

You

Run the release build, then tell me on Zona when it finishes.

Agent

calls zona_notify

{
  "title": "Build complete",
  "body": "The release finished.",
  "category": "build",
  "severity": "medium"
}
Result

ok · 202idempotentReplay: false · error: null

Inbox
Illustrative exchange. The arguments are the README example; the source name is whatever you call the source in the app.

Six tools, nothing else

The descriptions below are the server’s own text, word for word. It is what your agent reads when it lists the tools. None of them takes a token argument.

zona_notify

Send a Zona notification

Send an inbox notification to the owner's Zona app. Title 1-120 chars, body 1-2000. Optional category, severity (low|medium|high|critical), a todo checklist, a small data object, up to five PNG/JPEG/WebP attachment paths, and an idempotency key. Do not put secrets in title, body, todo or data.

Input

FieldTypeRule
titlerequiredstring1–120 characters
bodyrequiredstring1–2000 characters
categoryoptionalstring1–80 characters
severityoptionalenumlow · medium · high · critical
idempotencyKeyoptionalstring8–128 characters: letters, digits and . _ : -, starting with a letter or digit; defaults to mcp-<uuid>
todooptionalarrayA checklist ticked off on the phone: up to 20 items of 1–200 characters, each text or { id, text }
dataoptionalobjectA small JSON object stored with the alert, at most 4 KiB. Never secrets
attachmentsoptionalstring[]Up to five local PNG, JPEG or WebP file paths

Returns

  • ok
  • status
  • notificationId
  • sourceName
  • idempotencyKey
  • idempotentReplay
  • attachmentAccepted
  • attachmentError
  • pushAttempted
  • pushAccepted
  • error

With attachments the server sends multipart/form-data, one attachment part per file. Without them it sends JSON. Every send’s data carries "sender": "zona-mcp" unless it names a sender of its own. A rejected send, from any tool, also returns the field (path), the rule it broke (reason), the bound (limit) and a requestId to quote, plusretryAfterSeconds when Zona asks it to wait.

zona_done

Report a finished task

Tell the owner a task is finished and what to look at. The files become a review checklist on the phone. Use at the end of a long task, not for every step. Sends only what you pass: keep the summary free of secrets.

Input

summarystring, required
1–120 characters: one line on what was finished
detailsstring, optional
1–2000 characters: what changed and anything to decide
filesstring[], optional
Up to 20 paths worth reviewing, most important first; they become the checklist
severityenum, optional
low · medium · high · critical; defaults to low
idempotencyKeystring, optional
8–128 characters: letters, digits and . _ : -, starting with a letter or digit; defaults to mcp-<uuid>

Returns

  • ok
  • status
  • notificationId
  • sourceName
  • idempotencyKey
  • idempotentReplay
  • attachmentAccepted
  • attachmentError
  • pushAttempted
  • pushAccepted
  • error

zona_checkin

Check in a scheduled job

Report that a scheduled job ran. The owner sets up the check-in in the Zona app first; an unknown name returns CHECKIN_NOT_FOUND.

Input

namestring, required
The check-in’s name in the Zona app: 1–64 lowercase letters, digits and . _ -
statusenum, optional
ok (the default) · start · fail

Returns

  • ok
  • status
  • name
  • recorded
  • state
  • nextDueAt
  • incident
  • onHold
  • sourceName
  • error

zona_curl

Show the request as a command

Return the alert as a curl command and a PowerShell command the owner can paste into a script or a cron job. The token is read from the token file when the command runs and never appears in the text. Sends nothing.

Input

The same fields as zona_notify, without attachments.

Returns

Text: the alert as a curl command and as a PowerShell command. The token is read from the token file when the command runs, so it never appears in the text.

zona_ping

Ping Zona

Send a short test notification to confirm the local token and /notify path work.

Input

titlestring, optional
1–120 characters; defaults to “Zona ping” and the UTC time
bodystring, optional
1–2000 characters; defaults to “MCP ping from this machine.”
categorystring, optional
1–80 characters; defaults to demo

Returns

  • ok
  • status
  • notificationId
  • sourceName
  • idempotencyKey
  • idempotentReplay
  • attachmentAccepted
  • attachmentError
  • pushAttempted
  • pushAccepted
  • error

zona_status

Zona MCP status

Check that the local Zona source token is configured. Never returns the token.

Input

No arguments.

Returns

  • version
  • tokenFile
  • tokenPath
  • notifyUrl
  • problem
  • hint

Download the MCP server

One zip with the server, the zona command, its lockfile, the README, the agent skill and the license. No node_modules inside: npm ci installs the pinned dependencies.

Check the SHA-256 before you run it
# macOS or Linux
shasum -a 256 zona-mcp-1.2.2.zip

# Windows PowerShell
Get-FileHash .\zona-mcp-1.2.2.zip -Algorithm SHA256
Version
1.2.2
Size
71,163 bytes
SHA-256
6a5ca09bd7b712bd565677065ec38d766cd5cad9fee90a4ac52881b81fca2241
Runtime
Node.js 20+
Transport
stdio
Dependencies
  • @modelcontextprotocol/sdk 1.30.0 · MIT
  • yaml 2.9.1 · ISC
  • zod 3.25.76 · MIT
Contents
  • zona-mcp/LICENSE
  • zona-mcp/README.md
  • zona-mcp/SKILL.md
  • zona-mcp/cli.mjs
  • zona-mcp/package-lock.json
  • zona-mcp/package.json
  • zona-mcp/server.mjs
  • zona-mcp/lib/agent-hooks.mjs
  • zona-mcp/lib/agent-installers.mjs
  • zona-mcp/lib/agents.mjs
  • zona-mcp/lib/hermes-config.mjs
  • zona-mcp/lib/secret-mask.mjs
  • zona-mcp/lib/zona-client.mjs

Install in five steps

You need Node.js 20 or later and the Zona app on your iPhone for the source token.

  1. Unzip it somewhere permanent

    Your host starts the server from its absolute path, so keep the zona-mcp folder where it will stay.

    Extract
    unzip zona-mcp-1.2.2.zip
    
    # Windows PowerShell
    Expand-Archive .\zona-mcp-1.2.2.zip -DestinationPath .
  2. Install dependencies

    Node.js 20+ provides fetch, FormData and crypto.randomUUID, which the server uses directly. Without the global install, run node cli.mjs from this folder wherever this page says zona.

    Install from the lockfile
    cd zona-mcp
    npm ci
    npm install --global .   # optional: puts the zona command on your PATH
  3. Save your source token

    In the Zona app, open Sources, create a source (or a new key), copy the zona_live_… token once, and save it as a single line in the token file with your editor. Treat it like a password.

    Token file location
    # macOS or Linux: one zona_live_… line, no quotes
    ~/.zona/token
    
    # Windows
    %USERPROFILE%\.zona\token

    You can also set ZONA_SOURCE_TOKEN. When it is set, the server uses it instead of the file.

  4. Add it to your host

    Register a server named zona that runs node with the absolute path to server.mjs. Restart the CLI after changing MCP config.

    Configuration for your host
  5. Check, then ping

    Ask your agent to run zona_status. It reports tokenFile: true when a token is configured, without returning the token. Then ask for zona_ping: a short test alert with category demo arrives in your inbox.

Configure your host

Replace the path with the absolute path to server.mjs on your machine. Forward slashes work on Windows too. Restart the CLI after changing MCP config.

Claude Code

Add it once with the CLI so it is available in every project, or commit the JSON as .mcp.json at a project root to share it with that project.

Terminal
claude mcp add --scope user zona -- node /absolute/path/to/zona-mcp/server.mjs
.mcp.json
{
  "mcpServers": {
    "zona": {
      "command": "node",
      "args": ["/absolute/path/to/zona-mcp/server.mjs"]
    }
  }
}

Claude Desktop

Open Settings, then Developer, then Edit Config. That opens claude_desktop_config.json: ~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on Windows.

claude_desktop_config.json
{
  "mcpServers": {
    "zona": {
      "command": "node",
      "args": ["/absolute/path/to/zona-mcp/server.mjs"]
    }
  }
}

Merge the zona entry into any mcpServers object that already exists, then quit and reopen the app.

Cursor

Use ~/.cursor/mcp.json for every workspace, or .cursor/mcp.json inside one project.

mcp.json
{
  "mcpServers": {
    "zona": {
      "command": "node",
      "args": ["/absolute/path/to/zona-mcp/server.mjs"]
    }
  }
}

Codex

Codex CLI reads MCP servers from ~/.codex/config.toml.

~/.codex/config.toml
[mcp_servers.zona]
command = "node"
args = ["/absolute/path/to/zona-mcp/server.mjs"]

Grok

Add the server table to your Grok CLI config.toml.

config.toml
[mcp_servers.zona]
command = "node"
args = ["/absolute/path/to/zona-mcp/server.mjs"]
enabled = true

# Windows with MSYS only
[mcp_servers.zona.env]
MSYS = "noglob"

On Windows, add the env table shown above, but only if you use MSYS.

Other stdio hosts

Any MCP host that launches stdio servers from an mcpServers map accepts the same block.

MCP config
{
  "mcpServers": {
    "zona": {
      "command": "node",
      "args": ["/absolute/path/to/zona-mcp/server.mjs"]
    }
  }
}

The server block is the one the Zona MCP README documents. Config file locations belong to each host and can move between versions, so check your host’s MCP documentation if yours differs.

Know when a coding agent needs you

The same download carries the zona command. Any coding agent that can run a command on its events uses it to tell your phone when it waits on you, works or finishes, whether that is Claude Code, Codex, OpenCode or a script of your own. Nothing is sent back to the agent.

waiting
A high alert: the agent waits on you, for a permission or a question.
running
No alert. It keeps the session on the Agent board.
finished
A low alert: the turn is done.
failed
A low alert: the turn ended on an error.

On Zona Plus the Agent board in the app shows every session in one place, and each new state clears the session’s last alert, so your phone shows only what is current. A running session that sends nothing for 20 minutes shows as Went quiet. On other plans, waiting, finished and failed arrive as ordinary alerts, and running updates are refused.

One command, for agents with hooks

Each installer edits only that agent’s own settings. It keeps every other setting and hook, backs the file up once as <file>.zona-backup and refuses a file it cannot parse; --dry-run prints the change and writes nothing.

Terminal
zona hooks install claude-code   # or: codex, grok, opencode, hermes, pi
zona hooks status                # read only: what is installed where
zona hooks uninstall claude-code
Installers and what to do after each one
AgentNameAfter installing
Claude Codeclaude-codeNothing: it picks the change up by itself.
Codex 0.154 or latercodexOpen Codex, type /hooks and trust the Zona hooks.
GrokgrokNew sessions load it. In a running one, type /hooks and press r.
OpenCodeopencodeRestart OpenCode.
HermeshermesRestart Hermes. The hooks go into its allowlist, so they run without a prompt.
PipiType /reload. Waiting needs Pi 0.84.4 or later.

zona agent, for every other agent

Point an agent’s own “run a command on this event” setting at zona agent: a hook, a notify program, a plugin or a wrapper script. Any name works, up to 40 characters, and Zona shows it as you wrote it.

Usage
zona agent <waiting|running|finished|failed> --agent <name>
           [--session <id>] [--project <path>|--no-project] [--turn <id>]
           [--share-question [--message <text>]] [--dry-run]
Around any long job
zona agent running  --agent "Nightly refactor" --session "$RUN_ID"
zona agent waiting  --agent "Nightly refactor" --session "$RUN_ID"
zona agent finished --agent "Nightly refactor" --session "$RUN_ID"
Settings that take Claude-style hooks
{
  "hooks": {
    "Stop": [{
      "hooks": [{ "type": "command", "command": "zona agent finished --agent \"My agent\"" }]
    }],
    "Notification": [{
      "hooks": [{ "type": "command", "command": "zona agent waiting --agent \"My agent\"" }]
    }]
  }
}

A JSON event on stdin fills in the session, the folder and the turn. The command prints nothing and exits 0 even when Zona cannot be reached, so it never gets in the agent’s way.

What leaves the computer

An update carries the agent’s name, its state, its session id (Zona keeps only a hash of it) and the project folder’s name. Never the prompt, the reply, a tool’s input or the transcript.

A hook holds the agent up for at most five seconds, prints nothing and always exits 0. Failures go to~/.zona/hooks.log, which stays on your computer.

Flags for zona hooks install
FlagWhat it changes
--no-projectLeaves the project folder name out.
--share-questionA waiting update also carries the agent’s own short message about what it waits for, with common credential formats masked on your computer and cut to 200 characters. Codex approvals are never shared. Off by default.
--no-boardInstalls only the waiting, finished and failed events, so no running updates are sent.

Try it in plain words

You never name a tool or a token. Say what you want to hear about, and the agent picks the call.

  • Prompt: “Is Zona set up on this machine?”

    Calls zona_status

    Reports whether a token is configured, where the server looks for it and which endpoint it will call. The token itself never comes back.

  • Prompt: “Send me a Zona ping.”

    Calls zona_ping

    A short test alert with category demo, to prove the token and the /notify path work end to end.

  • Prompt: “Run the full test suite. When it finishes, send me a Zona alert with the pass and fail counts, severity high if anything failed.”

    Calls zona_notify

    The agent writes the title and body from the real result and picks the severity you asked for.

  • Prompt: “Render the chart, then send it to Zona with the PNG attached.”

    Calls zona_notify

    The attachments argument takes up to five local PNG, JPEG or WebP paths, sent as multipart.

  • Prompt: “Refactor the settings screen, then tell me on Zona which files to look at.”

    Calls zona_done

    One low alert when the task is finished, with the files as a review checklist you tick off on your phone.

Teach it when to send

The zip includes SKILL.md, the prompt for AI CLIs that answers to /zona. Copy it into your host’s skills directory. It tells the agent:

  • Prefer the MCP tools over hand-written HTTP whenever the zona server is connected.
  • Use the qualified tool name the host exposes, and never invent a token argument.
  • Send only when you asked for an alert, or when a long job you asked to hear about has finished.
  • Keep title and body short, and never put secrets in them.
  • On a missing token file or INVALID_TOKEN, ask you to create a new source key in the app instead of searching the workspace for tokens.
  • If the agent hooks already report a session, do not send a second alert for the same thing.
  • Quiet hours keep the alert in the inbox and hold its push and in-app banner, unless you allow critical alerts through on Zona Plus.
Read SKILL.md
zona-mcp/SKILL.md
---
name: zona
description: >
  Send inbox notifications through Zona and operate the Zona MCP.
  Use when the user wants a Zona ping, phone alert, /notify send, agent
  notification, source token, attachments on a Zona send, or runs /zona.
---

# Zona

Zona is a private notification inbox. Agents send an HTTPS request with a
source token. Keep copy short and never include secrets.

## Secrets

- Token file: `~/.zona/token` (one `zona_live_...` line). Windows:
  `%USERPROFILE%\.zona\token`.
- Optional override: env `ZONA_SOURCE_TOKEN`.
- Never print, log, commit, or put the token in URLs, README, or tool args.
- If the file is missing or `/notify` returns `INVALID_TOKEN`, ask the owner to
  create a new source key in the app and replace the file. Do not search the
  workspace for tokens.

## Prefer the MCP

When the `zona` MCP server is connected, use its tools instead of curling:

| Tool | When |
| --- | --- |
| `zona_status` | Check a token is configured (it does not return the token). |
| `zona_notify` | Real send: title, body, optional category, severity, a `todo` checklist (up to 20 short items the owner ticks off on the phone), a small `data` object, up to five image paths. |
| `zona_done` | End of a long task: a one-line summary, optional details, and the files worth reviewing (they become the checklist). Once per task, not per step. |
| `zona_curl` | The owner wants the alert as a command for a script or cron job. Returns curl and PowerShell text with no token in it; sends nothing. |
| `zona_checkin` | A scheduled job reports in: the check-in `name` the owner set up in the app, and `status` `ok` (default), `start` or `fail`. `CHECKIN_NOT_FOUND` means the owner has not added that check-in yet; say so, do not retry. |
| `zona_ping` | Connectivity check; optional category. |

Call the host's MCP tool with the `zona__…` qualified name it exposes. Do not
invent a token argument.

A rejected send returns `error` with `path`, `reason` and `limit`: fix that
field and send again. `retryAfterSeconds` means wait that long; do not loop.
If Zona did not answer in time, retry once with the `idempotencyKey` the error
names, so the owner gets one alert, not two.

If the owner set up agent updates (`zona hooks install <agent>`, or
`zona agent` in the agent's own hook settings), the session's state already
goes out by itself: "needs you" and "finished"
alerts, and "running" updates that feed the Agent board on Zona Plus. Do not
also send a `zona_notify` for the same thing. Use `zona_done` when there is
something specific to review. Nothing comes back from Zona: there is no way
to answer or approve an agent from the phone.

## Direct HTTP (MCP unavailable)

```
POST https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify
Authorization: Bearer <token from the file, never logged>
Idempotency-Key: <required unique key>
```

JSON body: `title` (1–120), `body` (1–2000), optional `category` (≤80),
`severity` (`low|medium|high|critical`), `todo` array (≤20 items of 1–200
characters), `data` object ≤4 KiB.

Images: `multipart/form-data`, repeat the `attachment` part once per PNG/JPEG/WebP
file, max five.

Check-ins use the same address and token, JSON only, with nothing else in the
body: `{"checkin": {"name": "backup", "status": "ok"}}` (`status` optional:
`ok`, `start` or `fail`). No `Idempotency-Key` is needed. A plain `ok` writes
no alert; Zona alerts when a check-in is missed, fails or starts and never
finishes. From a shell: `zona checkin <name> [--start|--fail]`.

Quiet hours still save the alert in the inbox; only the phone banner is skipped.

## When to send

Send after a user-asked Zona alert or a completed long job the owner asked to
be notified about. Keep copy short and non-secret.

Built so the token never leaks

The server is server.mjs, the command is cli.mjs, and both send through one shared file,lib/zona-client.mjs. Read them before you run them; these are the rules they keep.

  • The token stays on your disk

    The server reads ZONA_SOURCE_TOKEN or ~/.zona/token each time it sends. It never writes the token to stdout, stderr or a tool result, and it refuses a value that does not start with zona_live_.

  • Never paste a token into a prompt

    No tool takes a token argument, so there is never a reason to put one in the conversation, a URL, a README or a commit. If a token has been exposed, revoke that key in the app, create a new one and replace the file.

  • Errors are scrubbed

    When a send fails locally, the error message is cleaned before the agent sees it: anything shaped like zona_live_… becomes zona_live_[redacted], and your home directory is shortened to ~.

  • Nothing secret in the alert

    Title and body appear in push banners and are stored in your inbox. Tell your agent to keep them short and free of credentials, keys and personal data.

  • Attachments are local files

    The server reads each attachment path from your disk and uploads it. Only let the agent attach images you mean to send: up to five PNG, JPEG or WebP files.

  • Retries do not duplicate

    Each send carries an Idempotency-Key. Pass your own durable idempotencyKey, such as a job ID, and a retried call with the same content returns the stored alert instead of creating a second one. Without one, the server generates mcp-<uuid> per call. A network failure or a 500, 502 or 504 is tried once more with the same key, so it is still one alert.

  • A hook never gets in the agent’s way

    An agent hook waits at most five seconds for Zona, prints nothing and always exits 0. Failures are recorded, redacted, in ~/.zona/hooks.log, and each session’s last state is kept in ~/.zona/agent-sessions; both stay on your computer.

  • Installers keep your settings

    zona hooks install edits only the agent’s own settings and keeps every other setting and hook. It backs an edited file up once as <file>.zona-backup, writes through a temporary file, refuses a file it cannot parse, and with --dry-run writes nothing.

Environment

VariableBehaviour
ZONA_SOURCE_TOKENOptional. Used instead of the token file when set.
ZONA_NOTIFY_URLOptional. Overrides the endpoint, which defaults to https://gerncrjtrdjtjvybvseb.supabase.co/functions/v1/notify.
ZONA_HOOKS_LOGOptional. Where the agent hooks record failures; defaults to ~/.zona/hooks.log.
ZONA_AGENT_SESSIONS_DIROptional. Where the agent hooks keep each session’s last state; defaults to ~/.zona/agent-sessions.

Requests time out after 15 seconds, and after five for an agent hook. Zona MCP 1.2.2 runs on Node.js 20+. For how Zona handles data on the server, read the security overview.

Every token starts in the app.

Create a source in Zona on your iPhone, save its token, and your agent can reach you.