Skip to main content

Notification bridge for Claude Code (Telegram + ntfy.sh) — send notifications, ask short questions, set reminders via Telegram bot or ntfy.sh topic.

Project description

mcgram logo

mcgram

Notification bridge for Claude Code — Telegram and ntfy.sh. Claude can ping you, ask you, and remind you on your phone.

CI PyPI Python 3.11+ License: MIT Tests Coverage

mcgram hero

What it does

Walk away from a long-running task. mcgram lets Claude tap you on the shoulder through Telegram or ntfy.sh — and lets you tap back (Telegram only).

📣 Notify "Build passed in 3m12s ✅" / send the failing log as attachment
Ask & wait Inline buttons (Approve / Cancel) with a timeout — Telegram only
Remind "nhắc tôi sau 30 phút check container logs"
🎥 Send video Demo recordings play right in-chat
🛰 Multi-channel Route messages to named destinations across both transports
🌐 No-token mode Use ntfy.sh on machines where api.telegram.org is blocked

Single process, no daemon, no VPS, no webhook setup. Lives inside the MCP server while Claude Code is open.

How it fits together

Architecture flow

Claude Code spawns mcgram over stdio. mcgram opens a long-poll to the Telegram Bot API. When you tap a button or type a reply, the update lands on your phone-out / mcgram-in side, the operator allowlist filters non-you traffic, and the answer flows back to Claude as the tool result.

Quickstart

Prereqs: Python 3.11+, uv or pipx. Pick ONE transport below (or set up both).

Option A — ntfy.sh (fastest, no token, ~30 seconds)

Best for: machines where api.telegram.org is blocked, or you just want 1-way pings to your phone.

# 1) Install + scaffold (generates a random ntfy topic for you)
uv tool install git+https://github.com/tvtdev94/mcgram     # or: pipx install ...
mcgram init                                                # writes ~/.mcgram/config.yaml with a unique topic

# 2) Install the ntfy mobile app: https://ntfy.sh/  (iOS / Android / Web)
#    In the app, subscribe to the topic printed by `mcgram init` (e.g. mcgram-a1b2c3d4...)

# 3) Verify
mcgram doctor    # ✅ ntfy health, ✅ test message delivered

# 4) Restart Claude Code → /mcp → mcgram appears → done

That's it. No bot, no token, no chat ID. Now in Claude Code: "báo cho tôi qua ntfy khi xong" or "push log to my phone" — Claude knows what to do.

⚠️ ntfy.sh topics are URL-addressable. Anyone who knows the topic name can subscribe and read your messages. Topics generated by mcgram init use 64-bit entropy (16 hex chars), which is fine for casual notifications but don't send secrets / PII unless you self-host ntfy.

Option B — Telegram (required for 2-way ask)

Best for: you want Claude to wait for your Approve/Cancel button before deploying.

# 1) Install + scaffold
uv tool install git+https://github.com/tvtdev94/mcgram
mcgram init

# 2) Create a bot with @BotFather → /newbot → copy token
#    Find your chat ID:  /start the bot, then visit
#    https://api.telegram.org/bot<TOKEN>/getUpdates  → copy chat.id

# 3) Paste credentials and uncomment the bot section
#    Edit ~/.mcgram/.env       → MCGRAM_BOT_TOKEN=...
#    Edit ~/.mcgram/config.yaml → uncomment `bot:` block, set operator_chat_id

# 4) Verify
mcgram doctor    # ✅ get_me OK, ✅ test message delivered

# 5) Restart Claude Code → /mcp → mcgram appears → done

Both transports

Declare both ntfy: and bot: in config — each named channel picks its own transport.

The 7 tools

Tool Telegram ntfy.sh When to use
send_message Notify on completion / send a status update
send_file Send logs, screenshots, generated artifacts
send_video Send a video that plays in-chat (mp4/mov/mkv/webm/m4v)
ask Question with inline buttons OR freetext, blocks until reply. Telegram-only
set_reminder Schedule an in-process reminder (lost on restart)
cancel_reminder Cancel a pending reminder
list_reminders List currently pending reminders

ntfy.sh has no 2-way input, so ask returns transport_unsupported on ntfy channels. The companion Claude Code skill (installed by mcgram init) teaches Claude when to call which tool — both English and Vietnamese trigger phrases are recognized.

Channels

Route messages to different destinations across both transports:

mcgram channel add oncall -1001234567890 -d "Pager rotation"        # Telegram
mcgram channel add-ntfy alerts -d "Build alerts"                     # ntfy.sh — auto-generates topic
mcgram channel add-ntfy alerts --topic mcgram-myalerts -d "..."      # explicit topic
mcgram channel list
#   alerts   ntfy      topic=mcgram-a1b2c3d4e5f6g7h8   Build alerts
#   default  ntfy      topic=mcgram-xxxxxxxxxxxxxxxx   Auto-created from ntfy.default_topic
#   oncall   telegram  chat_id=-1001234567890           Pager rotation

Then in Claude Code: "send the failing log to the alerts channel"send_file(path="…", channel="alerts").

CLI

mcgram                          # MCP stdio server (Claude Code calls this)
mcgram init [--force]           # scaffold ~/.mcgram/ + skill + auto-register MCP (generates ntfy topic)
mcgram doctor                   # config + connectivity check (per-transport)
mcgram audit [opts]             # analyze audit.jsonl: --since 1h, --tool ask, --rejected, --tail
mcgram channel <action>         # list | add NAME CHAT_ID | add-ntfy NAME [--topic T] | remove NAME
mcgram install-skill [--force]  # reinstall ~/.claude/skills/mcgram/SKILL.md

Config example

~/.mcgram/config.yaml — declare at least one of ntfy / bot:

ntfy:                              # Transport A — fast, no token
  server: https://ntfy.sh
  default_topic: mcgram-a1b2c3d4e5f6g7h8     # generated by `mcgram init`
  # access_token_env: NTFY_TOKEN   # optional, for paid/self-hosted

bot:                               # Transport B — required for `ask`
  token_env: MCGRAM_BOT_TOKEN      # token loaded from ~/.mcgram/.env
  operator_chat_id: 123456789      # your personal chat ID

defaults:
  parse_mode: plain                # plain | markdown_v2
  ask_timeout_s: 120
  rate_limit_per_min: 20

limits:
  ask_timeout_max_s: 600
  reminder_max_delay_s: 86400      # 24h
  reminder_max_pending: 10
  file_max_bytes: 52428800         # 50 MB
  ask_options_max: 6

channels:                          # optional named destinations
  oncall:                          # Telegram channel
    transport: telegram
    chat_id: -1001234567890
    description: Pager rotation
  alerts:                          # ntfy.sh channel
    transport: ntfy
    ntfy_topic: mcgram-alerts-x9k2
    description: Build alerts

audit:
  path: ~/.mcgram/audit.jsonl
  rotate_mb: 25
  redact_text: false               # true → outbound `text` masked in audit
  retention_days: null             # e.g. 14
  timezone: UTC

allow_outside_cwd: false           # send_file restricted to CWD by default

Security model

Layer Mitigation
🔐 Token Loaded from env var; never logged; masked in mcgram doctor
🚪 Operator filter Non-operator_chat_id updates rejected at dispatcher entry — never reach tool handlers
🛡 send_file traversal Path resolved + checked against CWD; size capped at file_max_bytes
Rate limit Per-tool token bucket (default 20/min)
🔒 Single instance PID-file lock at ~/.mcgram/.lock; second mcgram exits with LockHeldError
📜 Audit trail Every call logged JSONL with fsync; survives kill -9
🛌 Reminder spam Max 10 pending, 24h delay cap, 1000-char text cap
ask DoS Hard timeout cap (600s) so a forgetful user can't freeze Claude forever
🔁 Polling conflict Telegram 409 caught with clean error — no crash loop

Full STRIDE analysis: docs/security-threat-model.md.

Audit

mcgram audit                     # summary: counts by status + by tool
mcgram audit --since 1h          # last hour only
mcgram audit --tool send_file    # filter by tool
mcgram audit --rejected          # group rejections by reason
mcgram audit --tail              # follow new entries (Ctrl-C to stop)

Sample lines:

{"ts":"2026-05-21T10:00:00+00:00","tool":"send_message","status":"ok","chat_id":123,"channel":"default","text":"build passed","text_len":12,"ms":150}
{"ts":"2026-05-21T10:00:05+00:00","tool":"ask","status":"ok","channel":"oncall","question_id":"q_a1","source":"button","ms":3200}
{"ts":"2026-05-21T10:00:10+00:00","tool":"send_file","status":"rejected","reason":"file_too_large","bytes":62914560}

Known limitations

  • No persistent reminders — schedules live in process memory. Restart = lost.
  • ask blocks the MCP call — keep timeouts short or Claude waits idle.
  • ask is Telegram-only — ntfy.sh has no 2-way input. On ntfy channels, ask returns transport_unsupported without contacting the network.
  • ntfy.sh public topics are URL-discoverable — anyone with the topic name can subscribe. mcgram generates 64-bit-entropy topics, but don't send secrets / PII unless you self-host ntfy with auth.
  • ntfy.sh file size cap — public free tier ~15 MB (vs Telegram's 50 MB). Self-hosted ntfy can raise the cap.
  • No remote control — Claude can send, but the bot doesn't accept arbitrary commands FROM Telegram. (Future v0.2.)
  • No webhook / VPS support — Telegram uses long-poll only.
  • Same Telegram token on 2 machines → 409 Conflict (only one poller per bot). mcgram backs off cleanly; use different bots for different machines. (When bot: is omitted, Telegram polling is fully disabled — no 409, no log spam.)

Architecture

Full module map, lifecycle diagram, and data flow: docs/architecture.md.

src/mcgram/
├── cli.py · cli_init · cli_doctor · cli_audit · cli_channel · skill_installer
├── config · errors · audit · lock · rate_limiter
├── tg_client · ntfy_client · dispatch · update_dispatcher · polling · server · runtime
├── ask_registry · reminders
└── tools/  send_message · send_file · send_video · ask · set_reminder · cancel_reminder · list_reminders

All modules <200 LOC, 153 tests, ruff clean, py3.11/3.12 × ubuntu/windows in CI.

Update / uninstall

uv tool upgrade mcgram          # or: pipx upgrade mcgram
mcgram install-skill --force    # refresh bundled Claude skill if changed
claude mcp remove mcgram        # unregister from Claude Code
uv tool uninstall mcgram        # remove binary
rm -rf ~/.mcgram ~/.claude/skills/mcgram   # remove config + skill

Develop

git clone https://github.com/tvtdev94/mcgram && cd mcgram
uv sync --extra dev
uv run pytest -q
uv run ruff check src/ tests/

Credits

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcgram-0.2.0.tar.gz (5.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mcgram-0.2.0-py3-none-any.whl (55.3 kB view details)

Uploaded Python 3

File details

Details for the file mcgram-0.2.0.tar.gz.

File metadata

  • Download URL: mcgram-0.2.0.tar.gz
  • Upload date:
  • Size: 5.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for mcgram-0.2.0.tar.gz
Algorithm Hash digest
SHA256 010b4388336a0e0f62d2d421b4c0acba6c6febd7cdbd208c9c68ae06ae78d9d0
MD5 d5a67d9000a8ca6220838160da6c6bf6
BLAKE2b-256 71b36d3f73bbd50a343322bc24e157d0ca88e718a5ecaf2e6b507a0e223fe5a5

See more details on using hashes here.

File details

Details for the file mcgram-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: mcgram-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 55.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.0

File hashes

Hashes for mcgram-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15ceb134f907790075fe5b5dd191f6aed35dd2750b0983e76d8bdf1d8552c5e6
MD5 99c4e2fc651ede094a561d2cb6c91003
BLAKE2b-256 a45c4d93f4521a3487b1ce37e4ac35547643f8bd41766732129a894039598650

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page