notify-hub
Exposes tools to send notifications, list channels, and check gateway health via the Model Context Protocol.
README
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.
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.
Architecture
client (curl / hook script)
-> POST /notify (Bearer token) [Fastify API]
-> enqueue dispatch job [Redis / BullMQ]
-> dispatch worker resolves channel set
-> one delivery job per channel [Redis / BullMQ]
-> delivery worker sends via the channel'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 gets its own job so retries/failures are isolated per channel (one channel down doesn't block the others).
- Channels are pluggable: each one implements the same tiny interface
(
send(notification)); enabling one is a config toggle + credentials, and adding a brand-new one is a small adapter file (see the genericwebhookadapter as the reference example).
Quickstart
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 --build
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 |
|---|---|---|
message |
yes | non-empty string |
title |
no | defaults to "Notification" |
priority |
no | one of low, default, high, urgent |
tags |
no | string array, passed through to channels that support it (e.g. ntfy) |
channels |
no | subset of your enabled channels to target this send to; omit to use the token's default channels |
metadata |
no | free-form object, passed through to channel adapters (e.g. the webhook channel) |
Responses: 202 {jobId} (enqueued) · 400 (invalid body / unknown channel
name) · 401 (missing/unknown token) · 503 (queue unreachable, so the
caller never hangs).
Configuration
All config is env vars (see .env.example for every key).
Key ones:
PORT-- API listen port (compose maps${PORT:-8080}on the host).TOKENS--;-separatedname:token:defaultChannel1,defaultChannel2entries. Example:phone:supersecrettoken:ntfy,telegram;desktop:othertoken:discord.CHANNELS_ENABLED-- comma-separated list of channels to activate, e.g.ntfy,telegram,discord. A channel not listed here is never attempted, even if a request asks for it. A listed channel missing its required credentials makes the service refuse to start, naming the channel and the missing key -- so misconfiguration is caught immediately, not as a silent drop later.
Channels
Each row is the env keys a channel needs once it's in CHANNELS_ENABLED.
| Channel | Env keys | Setup notes |
|---|---|---|
ntfy |
NTFY_URL, NTFY_TOPIC |
Use https://ntfy.sh (public) or your own self-hosted ntfy server; subscribe to the topic in the ntfy app |
telegram |
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID |
Create a bot via @BotFather; get your chat id by messaging the bot then hitting getUpdates |
email |
SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, EMAIL_TO |
Any SMTP provider (Gmail app password, SendGrid, etc.) |
slack |
SLACK_WEBHOOK_URL |
Slack app -> Incoming Webhooks -> add to a channel |
discord |
DISCORD_WEBHOOK_URL |
Server channel settings -> Integrations -> Webhooks |
whatsapp |
WHATSAPP_PHONE, WHATSAPP_APIKEY |
Free personal API via CallMeBot -- message their bot to activate, rate-limited |
webhook |
WEBHOOK_URL |
Reference extensibility adapter: POSTs the full notification JSON to any URL you control (Gotify, a custom listener, etc.) |
Adding a brand-new channel: implement the NotificationChannel interface
(one send() method) in src/channels/adapters/, export a
ChannelRegistryEntry (factory + required env keys), and add one line to
src/channels/channel-registry.ts. No other core changes needed.
Claude Code hook
A zero-dependency hook script pushes "start" / "end" / "needs-input" events
to notify-hub globally, across every Claude Code project. See
clients/claude-code/install.md for the
full setup (settings.json snippet + env vars).
MCP server
notify-hub also ships as an MCP (Model Context Protocol) server over stdio,
exposing three tools -- send_notification, list_channels,
check_gateway_health -- so an agent (Claude Code, Claude Desktop, any MCP
client) can push notifications and check the gateway as tool calls instead
of hand-rolled HTTP. It's a thin client of the already-running gateway (no
direct Redis access). See
clients/mcp/install.md for the full setup
(claude mcp add command + generic mcpServers JSON config).
Admin panel
A local, dark-themed dashboard for managing everything above without hand-
editing .env:
- View every channel (ntfy, Telegram, Email, Slack, Discord, WhatsApp, webhook), toggle it on/off, and edit its credentials -- masked by default, revealed with one click.
- Manage token profiles (
TOKENS): add/remove, edit the token, and pick each profile's default channels. - Save & Apply validates, backs up
.env(timestamped), writes the new file, and runsdocker compose up -d --no-build api worker-- one click, no terminal. - Send test per channel posts a real notification and shows the actual delivery outcome (✅ sent, or the real failure reason ❌), not just "enqueued".
- Live gateway status (health, redis, active channels) 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:8081
npm 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:8081
Reachability: 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: the admin service mounts
/var/run/docker.sock so Save & Apply can run
docker compose up -d --no-build api worker against the real stack from
inside the container (it never recreates the admin service itself --
that would kill the container mid-request). This gives the admin
container control of the host's Docker daemon, the same pattern used by
tools like Portainer. 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".
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:worker
Verified
docker compose up -d --build was run end-to-end against a real ntfy.sh
topic:
docker compose ps-- all 3 services (redis,api,worker) up,apihealthcheckhealthy.curl http://localhost:8080/health->200 {"status":"ok","redis":true}.curl -X POST http://localhost:8080/notify -H "Authorization: Bearer <token>" -d '{"title":"notify-hub","message":"smoke test"}'->202 {"jobId":"1"}.- Worker log:
{"channel":"ntfy","msg":"sending notification"}followed by{"channel":"ntfy","msg":"notification sent"}. - Confirmed on the ntfy side too:
curl "https://ntfy.sh/<topic>/json?poll=1"returned the exact message:{"title":"notify-hub","message":"smoke test", ...}.
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
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。