notify
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
notify
Self-hosted Web Push notifications with three interfaces over one Bun process:
HTTP API for the PWA, CLI, and automation.
CLI for registration, sending, and administration.
MCP for AI agents that need to notify a human when work finishes.
One Bun process, one SQLite database, one Docker image. No Redis, no broker, no worker container.
Browser/PWA ── HTTP ─┐
CLI ──────── HTTP ───┤
MCP clients ─ HTTP ───┼── Bun.serve ── core services ── SQLite
│ └── Web Push
└── /mcpQuick start (local)
bun install
cp .env.example .env
# edit .envFrom a checkout, either prefix commands with bun run src/index.ts, or link the CLI once so the notify examples below work:
bun link
notify --helpThe example keeps HOST=127.0.0.1 and stores the database at ./data/notify.sqlite. That is the safe default for self-hosting. Put a reverse proxy on the same host if you need a public hostname.
Generate VAPID keys once:
bun run src/index.ts vapid generatePut the three printed lines in .env. Use a real contact URI for VAPID_SUBJECT (https://your-domain or mailto:you@your-domain); Apple rejects localhost and example values.
Uncomment NOTIFY_BOOTSTRAP_TOKEN in .env and set it to at least 32 random characters:
openssl rand -base64 32Start the server:
bun run src/index.ts serve --devOn iPhone, Web Push only works after adding the page to the Home Screen (Share → Add to Home Screen). Safari tabs cannot subscribe.
In another terminal, use the bootstrap token:
export NOTIFY_URL=http://127.0.0.1:8787
export NOTIFY_TOKEN='<your bootstrap token>'
bun run src/index.ts channel list
bun run src/index.ts register --channel alice --label "MacBook Chrome"Open the printed enrollment URL on the device, grant notification permission, and send a test:
bun run src/index.ts send "Hello from notify" --channel aliceCreate a real token and retire bootstrap
bun run src/index.ts token create --name admin --scope admin
# save the printed token, then
bun run src/index.ts token revoke bootstrapUse a separate send-only token for agents:
bun run src/index.ts token create --name agent --scope notify:sendRelated MCP server: BotBell MCP Server
Docker
A public image is published to GHCR by .github/workflows/publish.yml when you publish a GitHub release (and on manual dispatch):
docker pull ghcr.io/moureau-dev/notify:latest
docker pull ghcr.io/moureau-dev/notify:0.1.0The workflow builds linux/amd64 and linux/arm64, emits an SBOM, and tags the release (0.1.0, 0.1, 0, v0.1.0, latest, and sha-...). If you fork the repo, the image is ghcr.io/<your-owner>/notify; if GHCR leaves the package private, set Packages → notify → Package settings → Change visibility → Public.
This is the local Docker path. For a public hostname, see Deployment.
cp .env.example .env
# set VAPID_* and NOTIFY_BOOTSTRAP_TOKEN
docker compose up -d --buildCompose publishes 127.0.0.1:8787:8787, not every interface. Change the port mapping only if you understand the exposure, or put a TLS reverse proxy in front.
The database lives in the notify-data volume at /data/notify.sqlite. Compose overrides HOST=0.0.0.0 and DATABASE_PATH=/data/notify.sqlite for the container; .env stays written for bare-metal self-hosting. The container runs as a non-root user with a read-only root filesystem (except /data and /tmp). For a public hostname, put the container behind a TLS reverse proxy and set PUBLIC_URL to the https:// URL.
Tailscale helper (optional)
For local or phone testing without certificates, serve can create a tailnet-only HTTPS proxy:
PORT=8890 notify serve --tailscaleIt:
detects the Tailscale CLI and MagicDNS name,
sets
PUBLIC_URLtohttps://<magicdns>:8443,binds the Bun server to
127.0.0.1,runs
tailscale serve --bg --https=8443 http://127.0.0.1:<port>,removes the proxy on Ctrl-C (unless
--tailscale-keep).
Options:
--tailscale-port 8443 # HTTPS port on the tailnet
--tailscale-force # replace an existing handler on that port
--tailscale-keep # leave the proxy after the server exitsIt is tailnet-only (tailscale serve), never public Funnel. If 8443 is busy, pick another port. If Tailscale is not installed or HTTPS/MagicDNS is disabled, it fails with an explanation instead of starting a half-configured server.
Deployment
docker compose up -d --build is the reference deployment. It sets HOST=0.0.0.0 and DATABASE_PATH=/data/notify.sqlite inside the container, publishes only 127.0.0.1:8787, and persists /data.
For a public hostname:
Set
NODE_ENV=productionandPUBLIC_URL=https://notify.example.com.Terminate TLS in Caddy, nginx, Traefik, or Cloudflare Tunnel.
Set
TRUST_PROXY=trueonly when that proxy is trusted.Proxy to
127.0.0.1:8787. The proxy must run on the host (or share a Docker network with the container);127.0.0.1is not reachable from another container.Supply
VAPID_PUBLIC_KEY,VAPID_PRIVATE_KEY,VAPID_SUBJECT, andNOTIFY_BOOTSTRAP_TOKENfrom your secret manager.Back up
/data; it contains the SQLite database, token hashes, and push subscriptions.
Need tailnet-only access instead of a public hostname? Use notify serve --tailscale and skip the public proxy.
Web Push requires the public HTTPS origin to match PUBLIC_URL.
CLI
notify serve [--dev] [--tailscale] [--tailscale-port 8443] [--tailscale-force] [--tailscale-keep]
notify vapid generate [--json]
notify login --url <url> --token <token>
notify register [--channel <slug>] [--label <label>] [--ttl 15m] [--no-create] [--json]
notify send <message> [--title <title>] [--channel <slug>] [--link <url>] [--tag <tag>] [--json]
notify channel list|create|delete|set-default
notify device list
notify subscription list|remove
notify notifications list|show
notify setup
notify token list|create|revoke
notify doctorregister --channel alice uses get-or-create when authenticated with a global token. A channel-scoped token can only enroll into its own channel and cannot create channels. --ttl accepts 15m, 24h, or 7d (minimum 1m, maximum 24h); the HTTP API takes the same value as ttlMs.
subscription list shows the subscription IDs used by subscription remove. notifications show <id> prints the stored notification and every delivery attempt.
notify setup verifies the server/token and prints a portable contract: MCP URL, auth shape, OpenCode config, Claude Code command, and the live tool list. Use --json for agents.
Environment: NOTIFY_URL, NOTIFY_TOKEN. Config: ~/.config/notify/config.json (or NOTIFY_CONFIG).
MCP
The endpoint is POST /mcp on the same host. It uses Bearer authentication and per-tool scopes.
Agents do not need the commands hardcoded: after connecting, they call tools/list and receive tool names, descriptions, and JSON input schemas. You still configure the URL and Authorization header per client. Any client that supports remote Streamable HTTP MCP with custom headers works; stdio-only clients need a bridge, and OAuth-only clients without custom headers cannot use static Bearer tokens. Autocompletion, when a client offers it, is driven by those schemas.
notify setup --json prints a portable contract, and the running server serves a plain-text agent contract at /llms.txt. Repo-level guidance for coding agents is in AGENTS.md and llms.txt.
Tools:
Tool | Scope | Admin gate |
|
| no |
|
| no |
|
| no |
|
| no |
|
|
|
|
|
|
|
|
|
Admin tools are not registered unless MCP_ADMIN_ENABLED=1, and even then they require the right scope. Give agents a notify:send token, not the admin token. list_devices includes subscription IDs so an agent can call manage_subscription.
OpenCode example:
{
"mcp": {
"notify": {
"type": "remote",
"url": "https://notify.example.com/mcp",
"enabled": true,
"headers": {
"Authorization": "Bearer ntk_..."
}
}
}
}Claude Code example:
claude mcp add --transport http notify https://notify.example.com/mcp \
--header "Authorization: Bearer ntk_..."The MCP server is stateless and uses the Streamable HTTP transport. A stdio-only client needs a bridge; none is included in v1.
HTTP API
Method | Path | Auth |
GET |
| public |
GET |
| public |
GET |
| public page (token in URL) |
POST |
| one-time enrollment token |
GET |
| device cookie |
DELETE |
| device cookie |
POST |
| device cookie |
POST |
|
|
GET |
|
|
GET |
|
|
GET |
| public |
GET/POST |
|
|
DELETE |
|
|
POST |
|
|
POST |
|
|
GET |
|
|
GET |
|
|
DELETE |
|
|
GET/POST |
|
|
DELETE |
|
|
POST |
| Bearer + per-tool scope |
Scopes: notify:send, notify:read, channels:read, channels:write, devices:read, devices:write, admin. Tokens are stored as hashes and shown once.
Creating or deleting channels requires a global token; channel-scoped tokens are read/enroll/send-only within their channel. POST /api/enrollments accepts ttlMs from 60000 to 86400000.
Data and migrations
SQLite uses WAL, foreign keys, and a busy timeout. Migrations are pure SQL files:
src/db/migrations/
├── 0001_initial_schema.up.sql
└── 0002_example.up.sqlThe runner creates _migrations, reads *.up.sql files in alphabetical order, skips already-applied files, executes each in a transaction, and records it. Never edit a migration that has run in production; add a new one.
Tables: channels, devices, subscriptions, enrollment_tokens, api_tokens, notifications, deliveries, audit_events.
Every send creates a notifications row, even when the channel has no subscriptions. Each subscription attempt creates a deliveries row with status (sent, failed, expired), HTTP status code, and error. RETENTION_DAYS (default 90) prunes old notifications, deliveries, and audit rows.
A device can have subscriptions in multiple channels. A channel-scoped token is restricted to one channel; a global token can act across channels.
Security
No default credentials. The first boot fails closed without
NOTIFY_BOOTSTRAP_TOKEN.API and MCP requests require Bearer tokens.
/mcprequires a valid token before tool discovery.Admin MCP tools are opt-in and scope-gated.
Device management uses an HttpOnly, SameSite=Strict cookie; no device token in localStorage.
Enrollment tokens are high-entropy, single-use, short-lived, and stripped from the URL.
Push endpoints are HTTPS-only, host-allowlisted, and validated for key sizes (SSRF protection).
Notification URLs are scheme-validated; the service worker never injects HTML.
CSP, HSTS (on HTTPS), no CORS, no directory listing, body limits, and auth/send rate limits.
Tokens and push endpoints are redacted from logs. Sensitive actions are audited.
Production refuses to bind a public host over plaintext HTTP.
The image runs as a non-root user; the compose file adds a read-only root filesystem.
See SECURITY.md for the threat model and disclosure process.
Development
bun install
bun test
bunx tsc --noEmit
bun run buildbun run src/index.ts serve --dev generates a persistent development VAPID keypair at data/dev-vapid.json if real keys are not configured. Do not use those keys in production.
See CONTRIBUTING.md for migration and test conventions.
Changelog
See CHANGELOG.md. Release notes are written from the matching version section.
License
MIT. See LICENSE.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
This server cannot be deployed
Maintenance
Related MCP Connectors
Push notifications for AI agents - send instant iPhone notifications from any MCP client.
Reach your own phone from an AI agent: notifications, approval questions, reminders, ring, files.
Let your AI agent notify you by email, Slack, Discord, or webhook. One tool: send_notification.
Send, search, and manage notifications, accounts, and push preferences
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables AI assistants to send push notifications through the kweenkl service. Allows users to receive contextual notifications from their AI when tasks are complete or important events occur.19 npmMIT
- AlicenseAqualityDmaintenanceEnables AI assistants to send push notifications and interactive alerts to iPhone and Mac devices via the BotBell app. It allows AI to receive user replies and manage notification bots for tasks like alerts, reminders, and remote approvals.218 npm1MIT
- AlicenseNot gradedqualityDmaintenanceEnables sending real-time and webhook notifications when AI agents complete tasks, with support for Discord, Slack, Teams, Feishu, Ntfy, and custom webhooks.6MIT
- AlicenseAqualityDmaintenanceEnables AI agents to send cross-platform desktop notifications with priority levels, categories, and configurable duration.1MIT