agent-notify
Enables AI agents and applications to send audio notifications with text-to-speech, message streaming, agent-to-agent conversations, and web push notifications through a persistent message store and MCP integration.
README
<p align="center"> <img src="logo.png" alt="Agent Notify" width="600"> </p>
<p align="center"> π Audio notifications Β Β·Β π£οΈ Text-to-speech with per-agent voices Β Β·Β π MCP integration<br> π¬ Message stream Β Β·Β π€ Agent-to-agent conversations Β Β·Β ποΈ Multi-window watch mode<br> π¦ App & CI notifications Β Β·Β π Sequential queue Β Β·Β β¨οΈ Remote keyboard controls </p>
π Table of Contents
- β¨ Features
- ποΈ Architecture
- π Notification Types
- π₯ Installation
- βοΈ Configuration
- π Usage
- π¦ App Notifications
- π Notification Queue
- πͺ Multi-Window & Multi-Agent Support
- π¬ Message Stream
- π€ Agent-to-Agent Conversations
- ποΈ Voice System
- ποΈ Watch Mode
- β¨οΈ Keyboard Controls
- π΅ Sound Files
- πΎ Message Persistence
- π οΈ Development
- π Requirements
- π License
- π€ Author
<a id="features"></a>
β¨ Features
- π΅ Audio Notifications - Plays distinct sounds for different notification types
- π£οΈ Text-to-Speech - Vocalizes notification messages using macOS
saycommand - ποΈ Multi-Agent Voice System - Distinct TTS voices per agent role or number
- π Project Identification - Identifies which project/workspace a notification came from
- π¨ Visual Feedback - Clean console output with emoji-led metadata and dim message text
- π MCP Integration - Works seamlessly with Cursor AI and other MCP-compatible tools
- π¦ App Notifications - Build tools, CI scripts, and deploy pipelines can fire notifications
- π Notification Queue - Sequential playback β notifications never overlap
- π¬ Message Stream - Persistent message store with incremental polling and playback tracking
- π€ Agent Conversations - Orchestrator-driven agent-to-agent audio conversations with turn-taking
- π Web UI - Phone-friendly dashboard at
localhost:8881with dark/light theme toggle - π² Web Push Notifications - Native OS notifications via service worker, even with browser closed (requires
localhostor HTTPS) - π§βπ» Operator Messaging - Human-in-the-loop messages to agents via web UI or API
- π Log Levels - Configurable audio thresholds for app notifications (console always shows all)
- β¨οΈ Keyboard Control - Spacebar to stop all, S to skip current, M to mute all audio
- ποΈ Watch Mode - Display-only panels that mirror notifications without playing audio
- π Synced Controls - Mute, stop, and skip sync across all panels via remote control endpoints
- π HTTP API - RESTful endpoints for external integrations
- πΎ Disk Persistence - Message store survives server restarts
<a id="architecture"></a>
ποΈ Architecture
Agent (MCP) βββΆ MCP tool "notify" βββΆ HTTP /notify/agent βββ
ββββΆ message store βββΆ notification queue βββΆ sequential playback
Agent (HTTP/CLI) βββΆ HTTP /notify/agent βββββββββββββββββββββββββββ€
β
App (HTTP/CLI) βββΆ HTTP /notify/app βββββββββββββββββββββββββββββ
Agent (MCP) βββΆ MCP tool "get_messages" βββΆ HTTP /messages βββΆ message store (read)
Agent (MCP) βββΆ MCP tool "check_message_status" βββΆ HTTP /messages/status βββΆ counters only (read)
Agent (MCP) βββΆ MCP tool "check_responses_available" βββΆ HTTP /responses/available/for/id/:id βββΆ message store (count)
Agent (MCP) βββΆ MCP tool "check_responses_observed" βββΆ HTTP /responses/observed/for/id/:id βββΆ message store (count)
/notify/agentβ for all AI agent notifications (MCP, HTTP, or CLI). Always plays audio and logs to console./notify/appβ for all application notifications (HTTP or CLI). Subject to log level thresholds./messagesβ query the persistent message stream. Supports incremental polling and playback tracking./responses/available/for/id/:idβ count responses to a message (bus mode, ~5 tokens)/responses/observed/for/id/:idβ count observed responses (conversational mode, ~5 tokens)- Five MCP tools β
notify,get_messages,check_message_status,check_responses_available,check_responses_observed. - One CLI β
notifycommand. If--appflag is present β/notify/app; otherwise β/notify/agent. - One queue β both endpoints feed into the same FIFO queue. Sequential playback, no overlap.
- One message store β every notification is persisted. Survives server restarts.
<a id="notification-types"></a>
π Notification Types
π€ Agent Types
| Type | Emoji | Description | Use Case |
|---|---|---|---|
done |
β | Task completion | Successful operations |
error |
β | Error occurred | Failed operations |
question |
β | Need user input | Waiting for decisions |
permission |
π | Need authorization | Requiring user approval |
status |
π‘ | Progress update | Ongoing operations |
waiting |
β³ | Processing | Long-running tasks |
review |
ποΈ | Code review needed | File changes ready |
message |
π¬ | Agent conversation | Agent-to-agent dialogue |
π¦ App Log Levels
| Level | Emoji | Sound | Use Case |
|---|---|---|---|
debug |
π | (none) | Verbose debug info |
info |
βΉοΈ | status.mp3 | General information, progress updates |
warn |
β οΈ | waiting.mp3 | Warnings, deprecations, non-critical issues |
error |
β | error.mp3 | Failures, crashes, critical issues |
success |
β | done.mp3 | Build complete, tests passed, deploy finished |
<a id="installation"></a>
π₯ Installation
# Clone the repository
git clone <repository-url>
cd agent-notify
# Install globally
npm install -g
# Link globally for customization
npm link
<a id="configuration"></a>
βοΈ Configuration
<a id="server-connection-url"></a>
π Server Connection URL
By default, the notification clients (CLI and MCP) connect to http://localhost:8881. To use a different server address, set the AGENT_NOTIFY_URL environment variable.
For CLI Usage
# Set for current shell session
export AGENT_NOTIFY_URL="http://192.168.0.6:8881"
notify done "Task complete"
# Set for single command
AGENT_NOTIFY_URL="http://192.168.0.6:8881" notify done "Task complete"
# Add to ~/.bashrc or ~/.zshrc for persistence
echo 'export AGENT_NOTIFY_URL="http://192.168.0.6:8881"' >> ~/.bashrc
For MCP (Cursor) Usage
Add the env block to your Cursor settings.json:
{
"mcpServers": {
"agent-notify": {
"command": "notify-mcp",
"env": {
"AGENT_NOTIFY_URL": "http://192.168.0.6:8881"
}
}
}
}
Finding Your Server's IP Address
# macOS
ipconfig getifaddr en0 # WiFi
ipconfig getifaddr en1 # Ethernet
# Linux
hostname -I
# The server prints its address on startup:
# π‘ Listening on http://0.0.0.0:8881
Troubleshooting
| Issue | Solution |
|---|---|
| Connection refused | Check that the server is running (npm start) and the URL is correct |
| Wrong IP address | Use the commands above to find your server's IP, then set AGENT_NOTIFY_URL |
| Port already in use | The server auto-switches to watch mode. Or use a different port: node lib/server.mjs --address 0.0.0.0:9000 |
| Cross-machine access | Ensure the server uses 0.0.0.0 (default) not localhost |
<a id="notification-links-app-only"></a>
π Notification Links (App Only)
App notifications can include an optional clickable link (e.g., to a CI build, deploy dashboard, or health check endpoint). The link appears as a third line in the server terminal output and is not spoken via TTS.
Security Note: Links are deliberately excluded from agent notifications. AI agents are untrusted URL sources β allowing models to inject arbitrary clickable URLs creates a phishing/malicious link surface. Links are only available for app notifications, which come from user-controlled code.
CLI Usage
# Attach a dashboard link
notify success "Deploy complete" --app my-api --link https://my-api.example.com/health
# Attach a CI build link
notify error "Build failed" --app github-actions --link https://github.com/user/repo/actions/runs/12345
# Links are optional
notify info "Starting deploy..." --app deploy
HTTP API Usage
curl "http://localhost:8881/notify/app?type=success&message=Deploy%20complete&app=my-api&url=https://my-api.example.com/health"
Server Terminal Output
β
SUCCESS π¦ my-api
"Deploy complete"
π https://my-api.example.com/health
The URL is automatically clickable in most terminals (iTerm2, VS Code terminal, Hyper, etc.).
<a id="usage"></a>
π Usage
<a id="command-line-interface"></a>
π» Command Line Interface
# Agent notification (type and message only)
notify done "Task completed successfully"
notify error "Something went wrong"
notify question "Do you want to continue?"
# Agent with project identification
notify done "Build complete" --workspace-dir /Users/user/repos/my-app
# Agent multi-agent (orchestrator)
notify done "All tasks complete" --workspace-dir /Users/user/repos/my-app --agent-role Orchestrator --agent-number 0
# Agent subagent with full context
notify done "Build complete" --workspace-dir /Users/user/repos/my-app --agent-role Coder --agent-number 2 --model claude-4.6-sonnet
# Agent override TTS voice
notify status "Processing..." --voice Nathan
# App notification
notify success "Build complete" --app webpack
notify error "3 tests failed" --app jest
notify info "Starting deploy..." --app deploy
notify debug "Cache hit ratio 95%" --app webpack
π CLI Flags
| Flag | HTTP Query Param | Description |
|---|---|---|
| (positional 1) | type |
Notification type or app log level (required) |
| (positional 2) | message |
Message text (required) |
--workspace-dir |
workspaceDir |
Full workspace path β project name derived from last segment (agent notifications only) |
--agent-role |
agentRole |
Agent role name (e.g., "Coder", "Orchestrator") (agent notifications only) |
--agent-number |
agentNumber |
Agent number (Orchestrator = 0, subagents = 1, 2, 3...) (agent notifications only) |
--voice |
voice |
TTS voice override |
--model |
model |
Your exact model identifier (e.g., "claude-4.6-opus-high") (agent notifications only) |
--app |
app |
App name β routes to /notify/app endpoint |
--project |
project |
Project name (app notifications only) |
--detail |
detail |
Short context info, e.g. file path or count (app notifications only) |
--link |
url |
Attach a clickable link to app notification (app notifications only, not spoken) |
<a id="mcp-integration-cursor-ai"></a>
π MCP Integration (Cursor AI)
Add to your Cursor settings (settings.json):
{
"mcpServers": {
"agent-notify": {
"command": "notify-mcp"
}
}
}
Then configure the notification rules:
Option 1: Project-specific - Copy the rules from .cursorrules to your project's .cursorrules file
Option 2: Global - Add the rules from .cursorrules globally in: Settings > Rules & Commands > Add to use across all projects
π MCP Tool Schema
mcp_agent-notify_notify({
type: "done", // Required: notification type
message: "Build complete", // Required: message text
workspaceDir: "/Users/user/repos/my-app", // Optional: Workspace Path from <user_info>
agentRole: "Coder", // Optional: agent role name
agentNumber: 2, // Optional: agent number (0 = orchestrator)
voice: "Nathan", // Optional: TTS voice override
model: "claude-4.6-sonnet", // Required: exact model identifier (console log only)
to: "Reviewer", // Optional: recipient for agent conversations
response_to: 225 // Optional: message ID this is a reply to
})
Note: The MCP tool is exclusively for agents. App notifications should use the CLI (--app flag) or HTTP API (/notify/app) directly.
π MCP Parameter Descriptions
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Notification type: question, permission, done, error, status, waiting, review, message |
message |
string | Yes | Message to vocalize |
workspaceDir |
string | No | The Workspace Path from <user_info>. Used to identify which project this notification is from. |
agentRole |
string | No | Agent role name assigned by orchestrator (e.g., "Coder", "Reviewer"). The orchestrator itself should use "Orchestrator". |
agentNumber |
integer | No | Agent number assigned by orchestrator. Orchestrator = 0, subagents = 1, 2, 3, etc. |
voice |
string | No | Override the TTS voice for this notification. If omitted, the server selects a voice based on agentRole or agentNumber. |
model |
string | Yes | Your exact model identifier as shown in system info (e.g., "claude-4.6-opus-high", "gpt-4o-2025-03"). Console log only. |
to |
string | No | Agent role or name this message is directed to (e.g., "Reviewer", "Coder"). Used for agent-to-agent conversations. Display/filtering only β does not route messages. |
response_to |
integer | No | Message ID this is a reply to. Enables lightweight response polling via check_responses_available and check_responses_observed. |
π¬ MCP get_messages Tool
Poll the persistent message stream for notifications. Supports incremental polling via since_id.
mcp_agent-notify_get_messages({
since_id: 42, // Optional: only messages after this ID (0 for initial fetch)
limit: 50, // Optional: max messages to return (default 50, max 200)
type: "message", // Optional: filter by notification type
to: "Coder", // Optional: filter by recipient
project: "my-app", // Optional: filter by project name
source: "agent", // Optional: filter by source ("agent" or "app")
agentRole: "Reviewer", // Optional: filter by agent role
agentNumber: 2, // Optional: filter by agent number
model: "claude-opus", // Optional: filter by model
voice: "Samantha", // Optional: filter by TTS voice
app: "webpack" // Optional: filter by app name
})
Response:
{
"messages": [
{
"id": 47,
"timestamp": "2025-03-01T04:40:07.000Z",
"prevHash": "a3f2c1e809b7d4f2",
"source": "agent",
"type": "message",
"message": "Build complete",
"project": "my-app",
"agentRole": "Coder",
"agentNumber": 1,
"model": "claude-opus-4-6",
"voice": "Nathan",
"to": "Reviewer"
}
],
"latest_id": 47,
"last_played_id": 47
}
latest_idβ highest message ID in the store (use assince_idfor next poll)last_played_idβ highest message ID whose audio has finished playing (derived from internal played events)- Played events (
type: "played") are filtered out of results β they are internal bookkeeping
β‘ MCP check_message_status Tool
ALWAYS use check_message_status for turn-taking and playback polling β NEVER use get_messages for this. Returns ~30 tokens instead of ~400-600, saving thousands of tokens over a conversation.
mcp_agent-notify_check_message_status({
since_id: 46 // Optional: check for messages newer than this ID
})
Response:
{
"latest_id": 47,
"last_played_id": 45,
"muted": false,
"has_new": true,
"queue_length": 2,
"agents": [
{
"project": "my-app",
"agentRole": "Coder",
"agentNumber": 1,
"model": "claude-opus-4-6",
"voice": "Nathan",
"to": "Reviewer",
"latestId": 47,
"played": false
}
]
}
latest_idβ highest message ID in the storelast_played_idβ highest message ID whose audio has finished playinghas_newβ true iflatest_id > since_idqueue_lengthβ number of notifications waiting in the audio queueagentsβ deduplicated array of agents that have posted sincesince_id. Each agent is identified by the composite keyproject + agentRole + agentNumber. Fieldsmodel,voice, andtoreflect the agent's most recent message.latestIdis that message's ID, andplayedis true if its audio has finished.
Only use get_messages when you need actual message content.
β‘ MCP check_responses_available Tool
Check if any responses have been sent to a specific message. Returns a count. Use for bus-mode polling β you only need to know if a reply exists, regardless of whether the human has heard it.
mcp_agent-notify_check_responses_available({
id: 225 // Required: the message ID to check for responses to
})
Response: {"n":1} (~5 tokens)
β‘ MCP check_responses_observed Tool
Check if any responses to a specific message have been heard by the human (audio played). Returns a count. Use for conversational-mode polling β wait for the human to actually hear the reply before proceeding.
mcp_agent-notify_check_responses_observed({
id: 225 // Required: the message ID to check for observed responses to
})
Response: {"n":0} (~5 tokens)
<a id="http-api"></a>
π HTTP API
Start the notification server:
# Default (listens on 0.0.0.0:8881 - accessible from network)
npm start
# With custom log levels for app notifications
node lib/server.mjs --log-level debug --log-level-audio warn
# Cross-network access (recommended for SSH/remote projects)
node lib/server.mjs --address 0.0.0.0:8881
# Custom IP and port
node lib/server.mjs --address 192.168.1.100:8881
# Custom port only (uses 0.0.0.0 as host)
node lib/server.mjs --address 9000
# Localhost only (NOT accessible from other machines)
node lib/server.mjs --address localhost:8881
# Watch mode β display only, no audio (auto-detects or explicit)
node lib/server.mjs --watch
# Custom store directory
node lib/server.mjs --store /path/to/store-dir
# Skip startup confirmation prompt
node lib/server.mjs --yes
# Clear message history and start fresh
node lib/server.mjs --clear
π Network Access:
0.0.0.0- π Accessible from any machine on your network (recommended)localhost/127.0.0.1- π Only accessible from the same machine- Specific IP - π― Only accessible via that network interface
Send notifications via HTTP:
# Agent notification
curl "http://localhost:8881/notify/agent?type=done&message=Build%20complete&model=claude-4.6-opus-high"
# Agent with full context
curl "http://localhost:8881/notify/agent?type=done&message=Build%20complete&workspaceDir=/Users/user/repos/my-app&agentRole=Coder&agentNumber=2&model=claude-4.6-sonnet"
# App notification
curl "http://localhost:8881/notify/app?type=success&message=Build%20complete&app=webpack"
# App notification with link
curl "http://localhost:8881/notify/app?type=success&message=Deploy%20complete&app=my-api&url=https://my-api.example.com/health"
# App debug (only shown if --log-level allows it)
curl "http://localhost:8881/notify/app?type=debug&message=Cache%20hit%20ratio%2095%25&app=webpack"
π€ /notify/agent Parameters
| Parameter | Required | Description |
|---|---|---|
type |
Yes | Notification type (question, permission, done, error, status, waiting, review, message) |
message |
Yes | Message text |
model |
Yes | Exact model identifier (e.g., "claude-4.6-opus-high") |
workspaceDir |
No | Full workspace path (project name derived from last segment) |
agentRole |
No | Agent role name |
agentNumber |
No | Agent number |
voice |
No | TTS voice override |
to |
No | Recipient agent role/name (for agent conversations, display/filtering only) |
response_to |
No | Message ID this is a reply to. Enables lightweight response polling. |
π¦ /notify/app Parameters
| Parameter | Required | Description |
|---|---|---|
type |
Yes | Log level (trace, debug, info, warn, error, success) |
message |
Yes | Message text |
app |
Yes | App name (e.g., "webpack", "jest", "github-actions") |
project |
No | Project name (e.g., "my-app") |
detail |
No | Short context that doesn't belong in the message (e.g., "src/auth", "3 files") |
voice |
No | TTS voice override |
url |
No | URL to attach as clickable link (not spoken, visual only) |
π§βπ» POST /notify/operator (JSON body)
| Field | Required | Description |
|---|---|---|
message |
Yes | Message text |
to |
No | Target agent role (e.g., "Coder") |
project |
No | Target project name |
voice |
No | TTS voice override (default: Daniel) |
curl -X POST http://localhost:8881/notify/operator \
-H 'Content-Type: application/json' \
-d '{"message":"Focus on auth","to":"Coder"}'
π¬ /messages Parameters
| Parameter | Required | Description |
|---|---|---|
since_id |
No | Return messages with ID greater than this (0 for initial fetch) |
limit |
No | Max messages to return (default 50, max 2000) |
type |
No | Filter by notification type |
to |
No | Filter by recipient agent role/name |
project |
No | Filter by project name |
source |
No | Filter by source ("agent", "app", or "operator") |
agentRole |
No | Filter by agent role |
agentNumber |
No | Filter by agent number |
model |
No | Filter by model identifier |
voice |
No | Filter by TTS voice |
app |
No | Filter by app name |
response_to |
No | Filter to messages that are replies to this message ID |
# Get all recent messages
curl "http://localhost:8881/messages"
# Incremental poll (only new messages since ID 42)
curl "http://localhost:8881/messages?since_id=42"
# Filter by type and recipient
curl "http://localhost:8881/messages?type=message&to=Coder"
π /responses/available/for/id/:id
Count responses to a message (bus mode β all sent, regardless of playback).
| Parameter | Required | Description |
|---|---|---|
:id (path) |
Yes | Message ID to check for responses to |
curl "http://localhost:8881/responses/available/for/id/225"
# β {"n":1}
π /responses/observed/for/id/:id
Count observed responses to a message (conversational mode β only those whose audio has been played).
| Parameter | Required | Description |
|---|---|---|
:id (path) |
Yes | Message ID to check for observed responses to |
curl "http://localhost:8881/responses/observed/for/id/225"
# β {"n":0}
<a id="programmatic-usage"></a>
βοΈ Programmatic Usage
import { execSync } from 'child_process';
// Agent notification
execSync('notify done "Operation completed" --model claude-4.6-opus-high');
// Agent with workspace context
execSync('notify done "Build finished" --workspace-dir /Users/user/repos/my-app --model claude-4.6-opus-high');
// App notification
execSync('notify success "Build complete" --app webpack');
<a id="app-notifications"></a>
π¦ App Notifications
App notifications allow build tools, CI scripts, deploy pipelines, test runners, and any other application to fire notifications alongside agent notifications.
<a id="app-log-levels"></a>
π App Log Levels
Apps use logger-style levels instead of agent notification types:
| Level | Sound | Emoji | Use Case |
|---|---|---|---|
trace |
(none) | π¬ | Fine-grained tracing, function entry/exit |
debug |
(none) | π | Verbose debug info |
info |
status.mp3 | βΉοΈ | General information, progress updates |
warn |
waiting.mp3 | β οΈ | Warnings, deprecations, non-critical issues |
error |
error.mp3 | β | Failures, crashes, critical issues |
success |
done.mp3 | β | Build complete, tests passed, deploy finished |
Hierarchy (lowest to highest): trace < debug < info < warn < error < success
<a id="log-level-configuration"></a>
ποΈ Log Level Configuration
Two server flags control which app notifications get audio (sound + TTS). All app messages are always shown in the terminal console and the web UI regardless of these flags.
| Flag | Default | Description |
|---|---|---|
--log-level |
info |
Minimum level for audio playback. Below this, the notification is logged and stored but silent. |
--log-level-audio |
info |
Secondary audio threshold. Both flags must be met for audio to play. |
π‘ Examples:
# Default: all levels visible, info+ gets audio
node lib/server.mjs
# Only hear warnings and above
node lib/server.mjs --log-level warn
# Hear everything including trace and debug
node lib/server.mjs --log-level trace --log-level-audio trace
# Only hear errors and successes
node lib/server.mjs --log-level error
β οΈ Important: Log level flags only control audio for app notifications. Console output and web UI always show all levels. Agent notifications always play audio regardless of these settings.
π Example Integrations
π¦ npm scripts (package.json)
{
"scripts": {
"build": "webpack --mode production",
"postbuild": "notify success 'Build complete' --app webpack",
"test": "jest",
"posttest": "notify success 'Tests passed' --app jest"
}
}
π Shell script
#!/bin/bash
notify info "Starting deploy..." --app deploy
npm run build
if [ $? -eq 0 ]; then
notify success "Deploy successful" --app deploy --link https://my-api.example.com/health
else
notify error "Deploy failed" --app deploy --link https://github.com/user/repo/actions/runs/12345
fi
π curl (HTTP)
# App notification with project, detail, and link
curl "http://localhost:8881/notify/app?\
type=success&message=Pipeline%20complete\
&app=github-actions\
&project=my-app\
&detail=deploy-prod\
&url=https://github.com/user/repo/actions/runs/12345"
# App notification without project
curl "http://localhost:8881/notify/app?\
type=success&message=Pipeline%20complete\
&app=github-actions"
# Agent notification (links not supported for security)
curl "http://localhost:8881/notify/agent?type=done&message=Build%20complete&model=claude-4.6-opus-high"
<a id="notification-queue"></a>
π Notification Queue
When multiple notifications arrive simultaneously (from parallel agents, apps, or a mix), a server-side FIFO queue ensures they play sequentially β one at a time, never overlapping. All callers receive an immediate response.
βοΈ Queue Behavior
- Every notification is queued β when a request arrives, it's added to the end of the queue
- Sequential playback β only one notification plays at a time (sound + TTS). The next one starts only after the previous one completes
- Immediate response β the server always responds immediately with
{ success: true, queued: true, position: N } - Log level filtering β app notifications below the
--log-level-audiothreshold are logged to console but not enqueued (no audio)
<a id="multi-window--multi-agent-support"></a>
πͺ Multi-Window & Multi-Agent Support
When running multiple Cursor windows and parallel agents, the notification system identifies the source of each notification through project name, agent role, and agent number.
<a id="console-log-format"></a>
π Console Log Format
π€ Agent Notifications
Emoji-led format with notification type capitalized. Message displayed in dim text on its own line, followed by a blank line separator. Optional fields omitted when not provided:
# Orchestrator (full):
β
DONE π my-app π€ Orchestrator #0 π§ claude-4.6-opus-high
"All tasks complete"
# Subagent (full):
β
DONE π my-app π€ Coder #2 π§ claude-4.6-sonnet
"Build complete"
# Solo agent (with workspaceDir):
β
DONE π my-app π§ claude-4.6-opus-high
"Build complete"
# Solo agent (no workspaceDir):
β
DONE π§ claude-4.6-opus-high
"Build complete"
π¦ App Notifications
βΉοΈ INFO π¦ webpack π my-app βοΈ src/index.ts
"Build started"
β
SUCCESS π¦ webpack π my-app
"Build complete in 4.2s"
β ERROR π¦ jest π my-app βοΈ auth.test.ts
"3 tests failed"
β οΈ WARN π¦ eslint
"12 warnings found"
π DEBUG π¦ webpack
"Module resolution: ./src/index.ts β ./dist/index.js"
# With optional link (3-line format):
β
SUCCESS π¦ my-api π my-api
"Deploy complete"
π https://my-api.example.com/health
π¦app name always shownπproject folder shown whenworkspaceDiris providedβοΈdetail shown whendetailis provided- No model field (apps don't have models)
<a id="tts-spoken-order"></a>
π£οΈ TTS Spoken Order
The notification sound and TTS speech run independently and in parallel. The sound fires immediately, and TTS begins after a 500ms delay. The spoken order matches the screen reading order β parts are omitted when not provided:
π€ Agent Spoken Order
- Message type (always included, e.g., "done", "question")
- Project name (from
workspaceDirlast segment β omitted if not provided) - Agent role (if provided, e.g., "Coder")
- Agent number (if provided, e.g., "Agent 2" or "Agent Zero" for orchestrator)
- Message text (always included)
Examples:
"done, my-app, Coder, Agent 2, Build complete"β full context"done, my-app, Build complete"β solo agent with workspaceDir"done, Build complete"β solo agent, no workspaceDir
π¦ App Spoken Order
- Log level (e.g., "success", "error")
- App name (e.g., "webpack")
- Project name (omitted if not provided)
- Detail (omitted if not provided)
- Message text
Examples:
"success, webpack, my-app, src/auth, Build complete"β full context"success, webpack, my-app, Build complete"β with project, no detail"success, webpack, Build complete"β minimal
Note: The optional url parameter is not spoken via TTS. URLs are visual-only in the console output.
Examples:
"success, webpack, Build complete in 4.2 seconds""error, jest, 3 tests failed"
<a id="agent-zero-convention"></a>
π€ Agent Zero Convention
When using an orchestrator with multiple subagents:
- Orchestrator =
agentRole="Orchestrator",agentNumber=0β spoken as "Orchestrator, Agent Zero" - Subagent 1 =
agentRole="Coder",agentNumber=1β spoken as "Coder, Agent 1" - Subagent 2 =
agentRole="Reviewer",agentNumber=2β spoken as "Reviewer, Agent 2"
<a id="message-stream"></a>
π¬ Message Stream
Every notification sent via notify is stored in a persistent message stream. Use the /messages endpoint (or get_messages MCP tool) to query the stream for monitoring, polling, or reading conversation history.
<a id="incremental-polling"></a>
π Incremental Polling
Use since_id for efficient incremental polling:
- First fetch β pass
since_id=0to get recent messages - Note
latest_idfrom the response - Subsequent polls β pass
since_id=<latest_id>to get only new messages
# Initial fetch
curl "http://localhost:8881/messages?since_id=0"
# β { "messages": [...], "latest_id": 42, "last_played_id": 42 }
# Next poll β only new messages
curl "http://localhost:8881/messages?since_id=42"
<a id="playback-tracking"></a>
π§ Playback Tracking
The message stream tracks audio playback state:
last_played_idβ the highest message ID whose audio has finished playing (derived from internal played events)
Use this to know when a message has been heard before sending the next one. This is the foundation for the turn-taking protocol.
<a id="agent-to-agent-conversations"></a>
π€ Agent-to-Agent Conversations
The orchestrator creates audio conversations by sending notify on behalf of different agents. The user hears each agent in a distinct TTS voice β the conversation unfolds live through audio.
The orchestrator drives the conversation. Individual agents don't need to independently poll the stream β the orchestrator:
- Decides what each agent says and when
- Sends
notifyusing each agent'sagentRoleandagentNumber - Waits for each message to finish playing before sending the next
Agents can independently poll get_messages for cross-tool scenarios (e.g., bridging Cursor and Claude Code agents via the shared message stream).
<a id="turn-taking-protocol"></a>
β³ Turn-Taking Protocol
The orchestrator must wait for each message to finish playing before sending the next. Without this, messages queue up faster than audio can play and the conversation loses its natural pacing.
Flow:
-
Send on behalf of an agent β note the returned
id:notify(type="message", to="Reviewer", message="...", agentRole="Coder", agentNumber=1) β id: 47 -
Wait for audio to finish β poll
check_message_statusuntillast_played_id >= 47:check_message_status(since_id=46) β { last_played_id: 46, has_new: true } # still playing check_message_status(since_id=46) β { last_played_id: 47, has_new: true } # done β send next turn -
Send the next turn on behalf of the other agent, only after the previous message has been played.
Key details:
- The orchestrator waits for each message's
id, not for the queue to be empty. Multiple conversations can run simultaneously without blocking each other. - When the user skips audio (spacebar), all queued messages are marked as played immediately, so the orchestrator proceeds without getting stuck.
- Use
type="message"for conversation turns; reserve other types for their intended purpose. - The
toparameter indicates who the message is addressed to (for display/filtering) β it does not route or deliver messages.
Lightweight Turn-Taking (response polling)
For agent-to-agent conversations where each agent independently polls for replies:
-
Agent A sends with
response_topointing at the message it's replying to:notify(type="message", message="...", response_to=225) β id: 226 -
Agent B polls for available or observed responses:
check_responses_observed(id=226) β {"n":0} # not heard yet check_responses_observed(id=226) β {"n":1} # reply heard β proceed -
Agent B reads the reply (if content needed):
get_messages(response_to=226)
This uses ~5 tokens per poll instead of ~50-100 with check_message_status.
<a id="voice-system"></a>
ποΈ Voice System
The server selects a TTS voice using a triple fallback strategy:
- Voice override β If
voiceparam is provided, use it directly (highest priority) - Role-based map β If
agentRolematches a role in the map, use that voice - Index-based map β If
agentNumbermatches an index in the map, use that voice - System default β Use macOS default voice
<a id="voice-maps"></a>
πΊοΈ Voice Maps
| Agent Role | Voice | Region |
|---|---|---|
| Orchestrator | System default | - |
| Coder | Nathan | America |
| Reviewer | Samantha | America |
| Tester | Karen | Australia |
| Designer | Zoe | America |
| Researcher | Serena | America |
| Debugger | Lee | America |
| DevOps | Evan | America |
| Writer | Matilda | America |
| Planner | Catherine | Australia |
| Security | Ava | America |
| Refactorer | Siri 1 | America |
| Analyst | Siri 2 | America |
| Migrator | Siri 3 | America |
| Agent Number | Voice | Region |
|---|---|---|
| 0 | System default | - |
| 1 | Nathan | America |
| 2 | Samantha | America |
| 3 | Karen | Australia |
| 4 | Zoe | America |
| 5 | Serena | America |
| 6 | Lee | America |
| 7 | Evan | America |
| 8 | Matilda | America |
| 9 | Catherine | Australia |
| 10 | Ava | America |
| 11 | Siri 1 | America |
| 12 | Siri 2 | America |
| 13 | Siri 3 | America |
Voice maps are configured server-side in lib/server.mjs for centralized management.
App notifications use Lee (Australian male) as the default voice to distinguish them from the predominantly American agent voices. This can be overridden with the voice parameter.
<a id="watch-mode"></a>
ποΈ Watch Mode
Watch mode lets you open additional terminal panels that mirror all notifications without playing audio. Useful for monitoring from multiple windows or screens.
Starting Watch Mode
Watch mode activates automatically or explicitly:
# Auto-detect β if port is already in use, switches to watch mode
npm start
# Explicit β skip port binding, go straight to watch mode
node lib/server.mjs --watch
When auto-detected, you'll see:
β οΈ Port 8881 already in use β switching to watch mode
Watch mode polls the primary server's /messages endpoint every second and renders new notifications with the same colored formatting.
Synced Controls
Keyboard controls work from any panel β watch mode sends commands to the primary server via POST /controls/* endpoints, and the action is broadcast to all panels through the message stream:
| Endpoint | Action |
|---|---|
POST /controls/stop |
Stop all audio and clear the queue |
POST /controls/skip |
Skip the current notification |
POST /controls/mute |
Toggle mute for all audio |
The /messages response includes a muted field so all panels stay in sync with the current mute state.
What Watch Mode Does NOT Do
- No audio playback β display only
- No notification queue β read-only polling
- Never loads, reads, or writes the message store β completely passive
- Never shows store confirmation prompts β store decisions are server-only
- Never writes to
/notify/*β read-only
<a id="keyboard-controls"></a>
β¨οΈ Keyboard Controls
These controls work on both the primary server and any watch mode panel. In watch mode, keypresses are forwarded to the primary server and the resulting action syncs to all connected panels.
| Key | Action |
|---|---|
| Spacebar | Stop current audio AND clear the entire queue (discard all pending notifications) |
| S | Skip current notification, move to the next one in the queue |
| M | Toggle mute for all audio (agents and apps). Notifications still logged to console. |
| Ctrl+C | Exit the server (or watch mode panel) |
<a id="sound-files"></a>
π΅ Sound Files
The system uses predefined sound files located in the sounds/ directory:
- π΅
done.mp3- Success sound (also used for appsuccess) - π
error.mp3- Error alert (also used for apperror) - β
question.mp3- Question prompt - π
permission.mp3- Authorization request - π‘
status.mp3- Status update (also used for appinfo) - β³
waiting.mp3- Processing sound (also used for appwarn)
<a id="message-persistence"></a>
πΎ Message Persistence
The message store lives in a .agent-notify/ directory as an append-only JSONL file (messages.jsonl). Each message is written immediately β zero data loss on crash.
- Messages survive server restarts β the last 10,000 lines are loaded into memory on startup
- Hash chain integrity β each message stores a
prevHashlinking to the previous message; verified on startup to detect corruption - Playback as events β playback state is tracked via append-only
playedevents (no mutations) - Startup safety β timestamped backup created on each startup; CLI confirmation prompt before accepting the store (
--yesto skip) - Crash-safe β no periodic flush, no full-file rewrites; every message is appended immediately
- Store directory β defaults to
.agent-notify/in project root; configurable via--store <dir>or$AGENT_NOTIFY_STOREenv var - Clear history β
--clearflag deletes the store after confirmation (--yes --clearto skip prompt) - Auto-migration β both
.message-store.json(old blob format) and.message-store.jsonl(old flat file) are migrated into.agent-notify/automatically on first run - Watch mode safety β only the primary server (port-bound process) loads or writes the store; watch mode and EADDRINUSE fallbacks never touch the store, never show prompts
- Sticky store link β clickable OSC8 link to the store file always visible at the bottom of the terminal
<a id="development"></a>
π οΈ Development
π Project Structure
agent-notify/
βββ lib/
β βββ notify.mjs # CLI interface
β βββ mcp.mjs # MCP server (notify, get_messages, check_message_status, check_responses_available, check_responses_observed)
β βββ server.mjs # HTTP server (queue, endpoints, message store, TTS)
βββ sounds/ # Audio files
βββ .agent-notify/ # Store directory (auto-generated, gitignored)
β βββ messages.jsonl # Append-only message stream
β βββ messages.jsonl.meta # Sidecar metadata
βββ package.json
βββ README.md
π Running the Server
# Start the notification server (default settings)
npm start
# Server runs on http://0.0.0.0:8881
# With custom log levels
node lib/server.mjs --log-level debug --log-level-audio warn
π§ͺ Testing
# Test agent notification
notify done "Test complete" --model claude-4.6-opus-high
# Test agent with project context
notify done "Test complete" --workspace-dir /Users/user/repos/test-project --model claude-4.6-opus-high
# Test agent multi-agent
notify done "Task finished" --workspace-dir /Users/user/repos/test-project --agent-role Coder --agent-number 1 --model claude-4.6-sonnet
# Test app notification
notify success "Build complete" --app webpack
notify error "Tests failed" --app jest
notify info "Deploying..." --app deploy
notify warn "Deprecation warning" --app eslint
notify debug "Verbose output" --app webpack
# Test all agent notification types
notify done "Test complete" --model claude-4.6-opus-high
notify error "Test error" --model claude-4.6-opus-high
notify question "Test question" --model claude-4.6-opus-high
notify permission "Test permission" --model claude-4.6-opus-high
notify status "Test status" --model claude-4.6-opus-high
notify waiting "Test waiting" --model claude-4.6-opus-high
# Test via HTTP
curl "http://localhost:8881/notify/agent?type=done&message=Test&model=test"
curl "http://localhost:8881/notify/app?type=success&message=Test&app=test"
<a id="requirements"></a>
π Requirements
- π macOS (uses
afplayandsaycommands) - π’ Node.js 18+
- π Audio output capability
<a id="license"></a>
π License
See LICENSE.md for details.
<a id="author"></a>
π€ Author
F1LT3R
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.