claude-notify
A remote MCP server that sends push notifications to your phone via Pushover, exposing a single notify_me tool for notifying you when long-running tasks complete.
README
claude-notify
A remote MCP server exposing exactly one tool, notify_me, that sends a push
notification to your phone through Pushover.
Write "notify me when the migration finishes" or "ping me if the test suite fails" in a Claude Code prompt, and the session calls the tool when it gets there. Sessions run for hours on several machines; this is how they reach you.
It is a remote server rather than a local one so that adding a machine is one
claude mcp add line with nothing to install, and so the Pushover credentials
live in exactly one place.
Claude Code (any machine)
│ remote MCP over HTTPS, Authorization: Bearer <per-machine token>
▼
Cloudflare Worker ──► auth → rate limit → resolve label → truncate → POST ──► Pushover → iPhone
│
└─ KV: token hashes, rate counters, quota state
The tool
| Param | Type | Required | Notes |
|---|---|---|---|
message |
string | yes | The notification body |
urgent |
boolean | no, default false |
Pushover priority 1 — bypasses quiet hours |
There is deliberately no source or machine parameter. The label in the
title comes from the bearer token, resolved server-side, so a session cannot
mislabel itself and the model has one less argument to get wrong. Notifications
arrive titled [vps-01] Claude Code.
First-time setup
You need a Cloudflare account, a Pushover account, and a Healthchecks.io check.
1. Pushover
- Install Pushover on your phone and sign in. Your user key is on the dashboard.
- Register an application at https://pushover.net/apps/build to get an API token.
Since 1 May 2026 the monthly allowance is per account, not per application — every app you register shares one pool of 10,000 messages a month.
2. Cloudflare
git clone <this repo> && cd claude-notify
npm install
cp wrangler.toml.example wrangler.toml
wrangler kv namespace create NOTIFY_KV
Put your account_id (from wrangler whoami) and the namespace id printed by
that command into wrangler.toml. That file is gitignored.
3. Healthchecks.io
Create a check at https://healthchecks.io with a period of 1 hour and a grace of 20 minutes, and copy its ping URL.
Treat that URL as a credential. Anyone holding it can ping your dead-man's switch and permanently suppress the alert that tells you the Worker died.
4. Secrets and deploy
wrangler secret put PUSHOVER_TOKEN # the application API token
wrangler secret put PUSHOVER_USER # your user key
wrangler secret put HEALTHCHECKS_URL # https://hc-ping.com/<uuid>
wrangler deploy
Secrets are only ever set this way. They are never reachable by any client and never appear in a file in this repo.
Adding a machine
One token per machine, so a leak costs you one machine rather than all of them.
node scripts/token.mjs add vps-01
The token is printed once and cannot be recovered — only its SHA-256 hash is stored. The command prints the registration line to run on that machine:
claude mcp add --transport http --scope user claude-notify \
https://YOUR-WORKER.workers.dev/mcp \
--header "Authorization: Bearer <the token printed by the CLI>"
Then check it:
claude mcp list # claude-notify ... ✔ Connected
Managing machines:
node scripts/token.mjs list # labels and status, never the tokens
node scripts/token.mjs revoke laptop # that machine gets 401 on its next call
Add --local to any of these to work against the wrangler dev KV simulation
instead of production.
<details> <summary>Equivalent raw wrangler commands</summary>
Only the hash is stored, so hash the token before writing it — a mistake here produces a token that silently never authenticates.
TOKEN=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
HASH=$(printf '%s' "$TOKEN" | sha256sum | cut -d' ' -f1)
wrangler kv key put "token:$HASH" '{"label":"ci-runner","enabled":true}' \
--binding NOTIFY_KV --remote
echo "$TOKEN" # shown once
# Revoke:
wrangler kv key delete "token:$HASH" --binding NOTIFY_KV --remote
</details>
Using it
Just say so in the prompt:
Run the full test suite and notify me if anything fails.
This migration will take a while — ping me when it's done.
The session decides when to call the tool. It reports the real outcome back: on success, how many Pushover messages remain this month; on failure, the actual HTTP status and Pushover's own error text. It never retries and never reports success it did not get, so if the pipe is broken the session says so.
Health monitoring
A system cannot report its own death, so detection comes from outside.
Hourly the Worker reads KV and then pings Healthchecks.io. If the Worker
dies, Healthchecks notices the silence and emails you over a path that touches
neither Cloudflare nor Pushover. If KV is unreachable it pings /fail instead,
so a storage outage is reported rather than papered over.
Daily at 08:00 Europe/Athens it sends a real notification: Pipe OK, N remaining. This is the only thing that exercises the entire chain, including
APNs and your iOS notification settings — which can break silently after an OS
update, in a way no server-side check can detect.
Cloudflare cron triggers are UTC-only, so the daily job is registered at both
05:00 and 06:00 UTC and the Worker checks the wall-clock hour in
DAILY_PING_TZ. One of the two is 08:00 in Athens whether or not daylight
saving is in effect, so exactly one firing per day gets through and the
notification lands at 08:00 local year-round.
Rate limits and quota
The only power this endpoint has is making your phone buzz, so spam is the
threat and rate limiting is the control. Both limits are [vars] in
wrangler.toml:
RATE_LIMIT_PER_TOKEN— default 60/hour per machineRATE_LIMIT_GLOBAL— default 200/hour across all machines, so one compromised machine cannot burn the whole Pushover quota
A breach comes back as a tool error naming the limit and when it resets, so the calling session sees it.
Counters live in KV, which is eventually consistent and last-write-wins. Under a concurrent burst some increments are lost and a few extra messages get through. That is a deliberate trade: this is a spam guard on a doorbell, not an exact quota.
Every Pushover response carries the account's remaining monthly allowance, which
is persisted. When it drops below QUOTA_WARN_THRESHOLD (default 500) a warning
is appended to the next notification — reserved before the message is
truncated, so a long message cannot push the warning off the end.
Pushover caps messages at 1024 characters and titles at 250. Both are truncated, never rejected: a truncated notification beats a silent failure.
Development
npm run typecheck
# Order matters. Local KV is a single SQLite database that only one Wrangler
# process may hold open, and each process carries its own workerd runtime — so
# create the token *before* starting the dev server, never alongside it.
# `token.mjs --local` refuses if it sees a dev server on port 8787.
node scripts/token.mjs add laptop --local
npm run dev # wrangler dev, with .dev.vars for secrets
NOTIFY_URL=http://localhost:8787/mcp \
NOTIFY_TOKEN=<the token printed above> \
./test/smoke.sh
Point NOTIFY_URL at https://YOUR-WORKER.workers.dev/mcp instead to smoke-test
the deployed Worker; a --local token will not authenticate against it, so use
one issued without --local.
Budget roughly 550 MB of RAM per wrangler dev (a Node parent plus two
workerd processes). Two Wrangler processes at once need over a gigabyte, which is
enough to livelock a small VM that has no swap.
The smoke test drives the whole path — tools/list, a real tools/call, an
unknown token, and a browser-origin request — and reads its credentials from
the environment rather than from the file.
For local development put secrets in .dev.vars (gitignored):
PUSHOVER_TOKEN=...
PUSHOVER_USER=...
HEALTHCHECKS_URL=...
Cron jobs can be triggered by hand against wrangler dev:
curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=0+*+*+*+*" # Healthchecks ping
curl "http://localhost:8787/cdn-cgi/local/scheduled?cron=0+5,6+*+*+*" # daily pipe test
Protocol notes
MCP revision 2026-07-28 made the protocol stateless: no initialize
handshake, no sessions, no Mcp-Session-Id, no GET stream. Each request
declares its own protocol version and capabilities in a _meta envelope,
mirrored into MCP-Protocol-Version, Mcp-Method and Mcp-Name headers.
This server is dual-era: it serves both the stateless revision and 2025-era
clients that still open with initialize, on the same /mcp endpoint. Claude
Code's rollout of the stateless revision is still in progress across versions,
so serving only one era would strand clients on the other.
Requests carrying an Origin header are refused with 403. Nothing legitimate
calls this from a browser — Claude Code sends no Origin at all — and this is
the transport spec's required DNS-rebinding protection.
Troubleshooting
✘ Failed to connect in claude mcp list — usually a bad or revoked token.
The server returns 401 with no detail, deliberately; check with
node scripts/token.mjs list and reissue if needed.
Connected, but tool calls return Unauthorized — there was a Claude Code
bug where the configured --header was attached on connect but dropped on tool
calls (#50464, fixed
June 2026). If you see this, upgrade Claude Code first.
Notifications stop arriving on the 1st of a month — the Pushover account quota is exhausted; it resets monthly. The daily pipe test reports the remaining count, which is the warning you get before this happens.
The daily test stops but the hourly check stays green — the Worker and KV are fine, so suspect the chain beyond them: Pushover credentials, or iOS notification settings after an update.
Security
- Tokens are stored only as SHA-256 hashes, keyed by the hash. Nothing compares secrets, so there is no timing-attack surface.
- Raw tokens are never logged. What reaches the request context is the hash.
- Unknown and disabled tokens are indistinguishable to the client: same status, same empty body.
PUSHOVER_TOKEN,PUSHOVER_USERandHEALTHCHECKS_URLare Worker secrets and are never reachable by any client.- If a credential leaks, rotate it. Rewriting git history is not sufficient.
.gitignore excludes wrangler.toml (it carries the account id and deployed
URL), .dev.vars, and .env. Every example in this file uses placeholders, and
every machine label is a generic one — vps-01, laptop, ci-runner. Real
labels live in KV, not in the repository.
Non-goals
Deliberately not built, and not to be added: a second transport (Telegram, ntfy); two-way messaging; Claude Code hooks integration; IP allowlisting or Tailscale; scheduled or interval status pings; a local MCP server variant.
License
MIT
推荐服务器
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 模型以安全和受控的方式获取实时的网络信息。