notify-hub
Sends messages to Discord channels via webhook for community or team notifications.
Sends email notifications via Gmail SMTP using app passwords.
Sends push notifications to ntfy.sh topics for real-time alerts.
Sends email notifications via SendGrid SMTP for reliable email delivery.
Sends messages to Slack channels via incoming webhook for team collaboration.
Sends messages via Telegram bot for instant notifications.
Sends WhatsApp messages via CallMeBot API for personal or mobile alerts.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@notify-hubsend a test notification to all channels"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
notify-hub
Self-hosted, free, multi-channel notification gateway. POST one message
with a token and it fans out -- asynchronously, via a durable queue -- to
every channel you've enabled: ntfy, Telegram, Email, Slack, Discord,
WhatsApp, or a generic webhook. Built so a Claude Code
hook can push you "task started" / "task finished" / "Claude needs you"
notifications across every project, without ever blocking Claude.
The admin panel -- named channel instances (ntfy, email, slack, telegram,
discord), token profiles with their default channels, and live gateway
status, all editable with zero restart.
Why
Claude Code (or any long-running script) leaves you watching a terminal for
minutes with no way to know when it's done or needs input. notify-hub is a
tiny, 100% free, self-hosted service you run once (docker compose up) that
turns "send me a push" into a one-line HTTP call, decoupled from delivery by
a Redis-backed queue with retries and per-channel dead-lettering.
Related MCP server: Trello Universal MCP Server
Architecture
client (curl / hook script)
-> POST /notify (Bearer token) [Fastify API]
-> enqueue dispatch job [Redis / BullMQ]
-> dispatch worker resolves the profile's default (or requested)
channel instance ids against SQLite [config store]
-> one delivery job per resolved instance [Redis / BullMQ]
-> delivery worker loads that instance's type + config from SQLite
at send time and builds/sends via the type's adapter
(ntfy / telegram / email / slack / discord / whatsapp / webhook)API only validates + enqueues; it never sends inline, so a slow/broken channel can't make
/notifyhang.Worker does the actual fan-out and delivery; each channel instance gets its own job so retries/failures are isolated per instance (one company's Slack going down doesn't block another's).
Config lives in SQLite, read fresh at request/delivery time -- there is no in-memory snapshot to restart. Add a channel, flip enabled, edit a webhook URL, or repoint a profile's defaults in the admin panel and the very next
/notifycall already uses it (hot-reload, nodocker compose restart).Channels are typed, instances are named: the six adapter types (ntfy/telegram/email/slack/discord/webhook) are the pluggable interface (
send(notification)); on top of a type you can create any number of named instances --acme-slack,globex-slack,pessoal-discord-- each with its own id, label and credentials. Profiles pick which instance ids they route to by default;POST /notify'schannelsfield targets instance ids directly. Adding a brand-new type is still a small adapter file (see the genericwebhookadapter as the reference example); adding a new instance of an existing type is just a panel click, no code.
Quickstart
Two ways to get going -- pick whichever fits how you like to configure things.
Option A -- configure everything in the panel (recommended, no .env
needed at all):
git clone https://github.com/richardfcampos/notify-hub.git && cd notify-hub
docker compose up -d --buildOpen http://localhost:8081, add a profile (pick a token) and add your
channels, then hit save -- it's live immediately, no restart. .env is
entirely optional; an absent one boots fine.
Option B -- seed from .env on first boot (faster if you already have
credentials handy and want the initial channels/token pre-populated):
git clone https://github.com/richardfcampos/notify-hub.git && cd notify-hub
./scripts/setup-env.sh # guided setup: prompts for each channel's credentials
# (hidden input), generates your gateway token, writes
# .env with chmod 600. Or do it by hand:
# cp .env.example .env && $EDITOR .env
docker compose up -d --buildThis only seeds the database on its very first boot (while it's still
empty) -- from then on, .env is ignored and every change happens in the
admin panel, not by re-editing .env.
Either way, confirm the gateway is up:
curl http://localhost:8080/health
# => {"status":"ok","redis":true}Send a notification:
curl -X POST http://localhost:8080/notify \
-H "Authorization: Bearer <your-token-from-TOKENS>" \
-H "Content-Type: application/json" \
-d '{"title":"notify-hub","message":"hello from notify-hub"}'
# => 202 {"jobId":"..."}POST /notify accepts:
Field | Required | Notes |
| yes | non-empty string |
| no | defaults to |
| no | one of |
| no | string array, passed through to channels that support it (e.g. ntfy) |
| no | subset of channel instance ids (from |
| no | free-form object, passed through to channel adapters (e.g. the |
Responses: 202 {jobId} (enqueued) · 400 (invalid body / unknown channel
instance id) · 401 (missing/unknown token) · 503 (queue unreachable, so
the caller never hangs).
GET /channels (same Bearer auth) lists every configured instance and the
calling token's profile defaults:
{
"channels": [
{ "id": "acme-slack", "label": "Acme Slack", "type": "slack", "enabled": true },
{ "id": "ntfy", "label": "Ntfy", "type": "ntfy", "enabled": true }
],
"defaultChannels": ["ntfy"]
}Configuration
Channels and token profiles live in SQLite and are managed live in the
admin panel (http://127.0.0.1:8081) -- add, edit, enable/
disable, or delete a channel instance, or change a profile's default
instances, and it takes effect on the very next /notify call. There is no
"apply" or restart step.
.env (see .env.example) only carries two kinds of
vars now:
Infra:
PORT(API listen port),REDIS_URL,DB_PATH(SQLite file location; compose points this at a named volume so it survives container recreates),RETRY_ATTEMPTS/RETRY_BACKOFF_MS,ADMIN_PORT/ADMIN_BIND.First-boot seed:
TOKENSandCHANNELS_ENABLEDplus each channel's credential vars (NTFY_URL,SLACK_WEBHOOK_URL, etc.) are read once, only when the service starts against an empty database -- each enabled channel becomes a named instance (id = its type, e.g.slack), andTOKENSentries become profiles. Once the database has any channel in it, these vars are ignored entirely; from then on, manage everything in the panel. Missing credentials for an otherwise-enabled seed channel seed it as a disabled instance rather than blocking boot -- fix its config in the panel and flip it on.
Telemetry
notify-hub includes optional, anonymous, usage telemetry that is off by
default on every install -- it only turns on if you explicitly opt in via
./scripts/setup-env.sh or by hand-setting TELEMETRY_ENABLED=true. See
TELEMETRY.md for the exact field list, why it exists, and
both ways to opt in or out.
Channels
Each row is the config keys a channel instance of that type needs (set
per-instance in the admin panel; the same keys the seed reads from .env
on first boot).
Type | Config keys | Setup notes |
|
| Use |
|
| Create a bot via @BotFather; get your chat id by messaging the bot then hitting |
|
| Any SMTP provider (Gmail app password, SendGrid, etc.) |
|
| Slack app -> Incoming Webhooks -> add to a channel |
|
| Server channel settings -> Integrations -> Webhooks |
|
| Free personal API via CallMeBot -- message their bot to activate, rate-limited |
|
| Makes an Echo device speak the notification out loud via Voice Monkey -- sign up, link your Echo as a device in their console, create an Announcement-capable "monkey" for it, and copy its API token |
|
| Speaks the notification out loud through a speaker on a host machine (e.g. this Mac) via macOS |
|
| Reference extensibility adapter: POSTs the full notification JSON to any URL you control (Gotify, a custom listener, etc.) |
voicemonkey is the practical way to get spoken Alexa notifications: the
official Amazon Proactive Events API was evaluated separately and only
supports 8 fixed light/banner schemas -- no arbitrary spoken text -- so
this third-party webhook is the recommended integration for "Alexa says
it out loud" use cases.
You can create any number of named instances per type -- e.g. an
acme-slack and a globex-slack, each with its own webhook URL -- from the
"Add channel" flow in the admin panel: pick the type, give it an id (slug)
and a label, fill in that type's config keys, save. Each profile then picks
which instance ids it defaults to.
Adding a brand-new channel type: implement the NotificationChannel
interface (one send() method) in src/channels/adapters/, export a
ChannelRegistryEntry (factory + required config keys), and add one line to
src/channels/channel-registry.ts. No other core changes needed -- the
admin panel picks up new types automatically via GET /api/channel-types.
Local TTS (your own speaker)
local-tts speaks a notification out loud through a speaker attached to a
host machine you own (e.g. this Mac), using macOS's built-in say command
-- zero cost, zero request quota, zero Amazon/third-party account
dependency (contrast with the voicemonkey row above, which needs an
account, a linked Echo device, and is rate-limited).
The catch: Docker Desktop for Mac gives containers no access to the host's
CoreAudio subsystem, so say can't run inside the worker container. A
small standalone player (clients/local-tts-player/) runs directly on the
host instead, outside Docker -- either as a plain foreground process
(fast path, no persistence) or via a launchd agent for auto-start on
login/reboot; see
clients/local-tts-player/install.md
for both paths, plus a known, unresolved troubleshooting caveat: the
launchd agent has failed to actually start (EX_CONFIG, exit 78) on at
least one real machine in this project's own history, for a cause not yet
confirmed. notify-hub's worker reaches the player over
http://host.docker.internal:8082 -- the standard Docker Desktop for Mac
mechanism for routing a container back to a host-bound loopback service
(this only works when the player and the docker compose stack are on
the same machine -- see the install guide for why a remote speaker isn't
supported out of the box).
Claude Code hook
A zero-dependency hook script pushes rich "end" (project, times, duration,
status, headline) and "needs-input" events to notify-hub globally, across
every Claude Code project -- zero-setup via a ~/.config/notify-hub/hook.env
config file (env vars still supported and take precedence). See
clients/claude-code/install.md for the
full setup (settings.json snippet + config options).
MCP server
notify-hub ships as an MCP (Model Context Protocol) server over two
transports: stdio (three send tools -- send_notification,
list_channels, check_gateway_health -- for Claude Code, Claude Desktop,
or any MCP client that spawns a process) and Streamable HTTP at /mcp on
the admin service (the same three tools plus seven config management
tools -- create/edit/delete channels and profiles, test-send, status -- for
MCP gateways like mcp-manager that register servers by URL). Both are thin
clients of the already-running gateway/admin service (no direct Redis/DB
access from the stdio transport). See
clients/mcp/install.md for the full setup
(claude mcp add command, generic mcpServers JSON config, and gateway
registration URL).
Admin panel
A local, dark-themed dashboard for managing everything above directly in
SQLite -- no .env hand-editing, no restart:
Named channel instances: add any number of instances of a type (e.g.
acme-slack+globex-slack, both typeslack, each its own webhook) -- pick a type from the liveGET /api/channel-typeslist, give it an id (slug) + label, fill in that type's credentials (masked by default, revealed with one click), toggle enabled, or delete it.Manage token profiles: add/remove, edit the token, and pick each profile's default channel instances by label (chips).
Save validates (dup id, unknown type, an enabled instance missing a required key, a profile referencing a disabled/missing instance) and writes straight to the DB as an upsert+delete diff against the current state -- live for the very next
/notifycall, nodocker composeapply/restart step.Send test per instance posts a real notification and shows the actual delivery outcome (✅ sent, or the real failure reason ❌), not just "enqueued".
Live gateway status (health, redis, configured instances) and a tail of recent worker deliveries.
Comes up automatically as part of the stack -- no extra step:
docker compose up -d
# => http://127.0.0.1:8081npm run admin still works as a host-side dev alternative (no Docker
rebuild needed while iterating on the panel itself):
npm run admin
# => admin panel: http://127.0.0.1:8081Reachability: in compose the host-side bind defaults to 0.0.0.0
(like the other services on a typical homelab host), so the panel is
reachable from your other devices -- e.g. over a Tailscale tailnet as
http://<machine>:8081. The panel has no auth and displays secrets,
so anyone who can reach the port can read and rewrite your config: keep
that surface to networks you trust (a WireGuard/Tailscale tailnet is a
good fit; an untrusted LAN is not). Set ADMIN_BIND=127.0.0.1 in .env
to make it localhost-only. The explicit bind template is asserted by
src/admin/compose-invariants.test.ts. The host-side dev mode
(npm run admin) binds 127.0.0.1 by default regardless.
Docker-socket trade-off: config CRUD never touches Docker -- a Save
writes straight to the shared SQLite volume and is live immediately. The
admin service still mounts /var/run/docker.sock (plus a read-only copy
of docker-compose.yml) for one narrower purpose: the status page and
"Send test" outcome both tail docker compose logs worker to show real
delivery results instead of just "enqueued". This gives the admin
container control of the host's Docker daemon, the same pattern used by
tools like Portainer, scoped in code to log reads only (no compose
up/down/restart is ever invoked). Accepted because the panel is a personal
tool on a trusted network -- combined with the reachability note above,
treat "who can open the panel" as "who can administer this Docker host".
Surviving a reboot (macOS)
Every container in docker-compose.yml already sets restart: unless-stopped, but that alone isn't enough on macOS: it only reactivates
containers once the Docker daemon comes back, and Docker Desktop itself
doesn't always relaunch on login even when its own "start on login"
preference is checked (its internal settings-store.json can say
AutoStart: true while the system-level login-item registration is
silently missing -- check with sfltool dumpbtm | grep -i docker). A
forced Docker Desktop restart was also observed leaving containers in an
Exited (0) state instead of auto-recovering, purely from the VM
Desktop runs its daemon inside.
scripts/docker-autostart.sh is the deterministic fix: it opens Docker
Desktop, polls until the daemon actually responds, then explicitly runs
docker compose up -d. Wire it to a launchd user agent so it runs on
every login:
mkdir -p ~/.local-scripts
cp scripts/docker-autostart.sh ~/.local-scripts/notify-hub-docker-autostart.sh
chmod +x ~/.local-scripts/notify-hub-docker-autostart.sh<!-- ~/Library/LaunchAgents/com.notify-hub.docker-autostart.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.notify-hub.docker-autostart</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/YOU/.local-scripts/notify-hub-docker-autostart.sh</string>
</array>
<key>RunAtLoad</key><true/>
</dict></plist>launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.notify-hub.docker-autostart.plistIf this repo lives on an external/Thunderbolt volume (not the boot
disk), copy docker-autostart.sh itself to ~/.local-scripts/ too (as
shown above) rather than pointing the plist at the repo's own copy --
launchd-spawned processes were found to get Operation not permitted
reading a script located on a non-boot volume, a macOS TCC restriction
that doesn't affect the same command run from an interactive Terminal.
The script's own cd into the repo at runtime is unaffected; only the
script's own file location has to be on the boot disk. The
local TTS player's own launchd setup
hits the identical restriction -- see
clients/local-tts-player/install.md
for the same fix applied there.
Development
npm install
npm run build # tsc -> dist/bin/{api,worker,admin}.js, copies
# src/admin/ui -> dist/admin/ui (static UI assets)
npm run test # full suite -- REQUIRES Docker (spins up redis:7-alpine
# via testcontainers for the BullMQ retry/dead-letter
# integration test); set REDIS_TEST_URL to reuse a
# running Redis instead of a container
npm run test:unit # no-Docker fast subset (src unit tests only)
npm run test:integration # just the Redis-backed queue integration test
npm run dev:api # tsx, no build step
npm run dev:workerVerified
The full 4-service stack (redis, api, worker, admin) has been
smoke-tested end-to-end via docker compose up -d --build plus curl/the
admin panel and worker log inspection, covering:
Boot & health:
docker compose psshows all 4 services up,apihealthcheckhealthy;curl http://localhost:8080/healthreturns200 {"status":"ok","redis":true}.Delivery:
POST /notifyagainst a real channel (ntfy.sh) returns202 {jobId}, and the worker log shows the send/success pair ("sending notification"->"notification sent") with the message confirmed as received on the channel side.SQLite-backed hot-reload: adding or editing a channel through the admin panel (or the MCP config tools) takes effect on the very next
/notifycall with nodocker compose restart-- verified by adding a channel instance live and sending immediately after.Claude Code hook: the zero-dependency hook script's rich "end"/ "needs-input" payloads (project, duration, status, headline) arrive correctly, and the idle debounce suppresses duplicate notifications during a burst of tool calls.
If your environment can't reach the public internet (ntfy.sh), point
NTFY_URL at a self-hosted ntfy instance instead and repeat the same smoke
steps.
Contributing
Contributions are welcome — the most useful one is a new channel adapter. Each channel is a small self-contained file implementing one interface:
Add
src/channels/adapters/<name>-channel.tsimplementingNotificationChannel(a singlesend(notification)method) plus itsChannelRegistryEntry(factory + required env keys).Register it with one line in
src/channels/channel-registry.ts.Add unit tests next to it (happy path + error path, using the fakes in
test/helpers/fakes.ts— no real network in tests).npm run test:unitmust pass; open a PR.
Ideas: Gotify, Matrix, Pushover, Signal, Mattermost, Rocket.Chat, SMS gateways. Bug reports and docs fixes are equally appreciated — open an issue.
License
MIT © Richard Campos
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Flicense-qualityDmaintenanceProvides tools for managing todo lists, checking weather, and accessing user information through the Model Context Protocol. It supports local execution and remote deployment as a web-based MCP gateway integrated with Gemini.1
- Alicense-qualityDmaintenanceProvides a standardized interface for interacting with Trello's tools and services through the Model Context Protocol.MIT
- Alicense-qualityDmaintenanceModel Context Protocol server that standardizes tool discovery, execution, and context management for AI applications.MIT
- Alicense-qualityDmaintenanceProvides a standardized interface for interacting with Microsoft Teams tools and services through the Model Context Protocol.1MIT
Related MCP Connectors
A Model Context Protocol server for Wix AI tools
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Connect AI agents to Replynodes over the Model Context Protocol.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/richardfcampos/notify-hub'
If you have feedback or need assistance with the MCP directory API, please join our Discord server