mcgram 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. mcgram-0.2.0/.env.example +3 -0
  2. mcgram-0.2.0/.github/workflows/ci.yml +50 -0
  3. mcgram-0.2.0/.gitignore +34 -0
  4. mcgram-0.2.0/CHANGELOG.md +127 -0
  5. mcgram-0.2.0/LICENSE +21 -0
  6. mcgram-0.2.0/PKG-INFO +311 -0
  7. mcgram-0.2.0/README.md +271 -0
  8. mcgram-0.2.0/config.example.yaml +34 -0
  9. mcgram-0.2.0/docs/architecture.md +121 -0
  10. mcgram-0.2.0/docs/images/flow.png +0 -0
  11. mcgram-0.2.0/docs/images/hero.png +0 -0
  12. mcgram-0.2.0/docs/images/logo.png +0 -0
  13. mcgram-0.2.0/docs/manual-smoke-test.md +78 -0
  14. mcgram-0.2.0/docs/security-threat-model.md +82 -0
  15. mcgram-0.2.0/plans/20260521-brainstorm-mcgram-telegram-mcp/brainstorm-report.md +240 -0
  16. mcgram-0.2.0/plans/20260521-implement-mcgram-telegram-mcp/phase-01-scaffolding-config-audit.md +144 -0
  17. mcgram-0.2.0/plans/20260521-implement-mcgram-telegram-mcp/phase-02-telegram-client-send-tools.md +154 -0
  18. mcgram-0.2.0/plans/20260521-implement-mcgram-telegram-mcp/phase-03-ask-flow-reminders.md +204 -0
  19. mcgram-0.2.0/plans/20260521-implement-mcgram-telegram-mcp/phase-04-cli-companion-skill.md +194 -0
  20. mcgram-0.2.0/plans/20260521-implement-mcgram-telegram-mcp/phase-05-tests-docs-ci.md +177 -0
  21. mcgram-0.2.0/plans/20260521-implement-mcgram-telegram-mcp/plan.md +69 -0
  22. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/phase-01-ntfy-client.md +95 -0
  23. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/phase-02-config-schema.md +150 -0
  24. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/phase-03-tools-branch.md +79 -0
  25. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/phase-04-ask-and-reminders.md +74 -0
  26. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/phase-05-cli.md +176 -0
  27. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/phase-06-docs-skill.md +79 -0
  28. mcgram-0.2.0/plans/260522-1355-ntfy-transport-integration/plan.md +138 -0
  29. mcgram-0.2.0/pyproject.toml +110 -0
  30. mcgram-0.2.0/scripts/live-smoke.py +101 -0
  31. mcgram-0.2.0/src/mcgram/__init__.py +3 -0
  32. mcgram-0.2.0/src/mcgram/ask_registry.py +140 -0
  33. mcgram-0.2.0/src/mcgram/audit.py +144 -0
  34. mcgram-0.2.0/src/mcgram/cli.py +72 -0
  35. mcgram-0.2.0/src/mcgram/cli_audit.py +191 -0
  36. mcgram-0.2.0/src/mcgram/cli_channel.py +176 -0
  37. mcgram-0.2.0/src/mcgram/cli_doctor.py +180 -0
  38. mcgram-0.2.0/src/mcgram/cli_init.py +216 -0
  39. mcgram-0.2.0/src/mcgram/config.py +275 -0
  40. mcgram-0.2.0/src/mcgram/data/__init__.py +1 -0
  41. mcgram-0.2.0/src/mcgram/data/config.example.yaml +59 -0
  42. mcgram-0.2.0/src/mcgram/data/skill/SKILL.md +131 -0
  43. mcgram-0.2.0/src/mcgram/dispatch.py +101 -0
  44. mcgram-0.2.0/src/mcgram/errors.py +58 -0
  45. mcgram-0.2.0/src/mcgram/lock.py +116 -0
  46. mcgram-0.2.0/src/mcgram/ntfy_client.py +173 -0
  47. mcgram-0.2.0/src/mcgram/polling.py +82 -0
  48. mcgram-0.2.0/src/mcgram/rate_limiter.py +45 -0
  49. mcgram-0.2.0/src/mcgram/reminders.py +136 -0
  50. mcgram-0.2.0/src/mcgram/runtime.py +37 -0
  51. mcgram-0.2.0/src/mcgram/server.py +249 -0
  52. mcgram-0.2.0/src/mcgram/skill_installer.py +37 -0
  53. mcgram-0.2.0/src/mcgram/tg_client.py +166 -0
  54. mcgram-0.2.0/src/mcgram/tools/__init__.py +1 -0
  55. mcgram-0.2.0/src/mcgram/tools/ask.py +127 -0
  56. mcgram-0.2.0/src/mcgram/tools/cancel_reminder.py +33 -0
  57. mcgram-0.2.0/src/mcgram/tools/list_reminders.py +24 -0
  58. mcgram-0.2.0/src/mcgram/tools/send_file.py +123 -0
  59. mcgram-0.2.0/src/mcgram/tools/send_message.py +94 -0
  60. mcgram-0.2.0/src/mcgram/tools/send_video.py +115 -0
  61. mcgram-0.2.0/src/mcgram/tools/set_reminder.py +57 -0
  62. mcgram-0.2.0/src/mcgram/update_dispatcher.py +45 -0
  63. mcgram-0.2.0/tests/__init__.py +0 -0
  64. mcgram-0.2.0/tests/conftest.py +57 -0
  65. mcgram-0.2.0/tests/test_ask_registry.py +194 -0
  66. mcgram-0.2.0/tests/test_audit.py +115 -0
  67. mcgram-0.2.0/tests/test_channels.py +63 -0
  68. mcgram-0.2.0/tests/test_cli.py +66 -0
  69. mcgram-0.2.0/tests/test_cli_audit.py +97 -0
  70. mcgram-0.2.0/tests/test_cli_channel.py +111 -0
  71. mcgram-0.2.0/tests/test_cli_channel_ntfy.py +139 -0
  72. mcgram-0.2.0/tests/test_cli_doctor.py +107 -0
  73. mcgram-0.2.0/tests/test_cli_doctor_ntfy.py +118 -0
  74. mcgram-0.2.0/tests/test_cli_init.py +95 -0
  75. mcgram-0.2.0/tests/test_cli_init_ntfy.py +184 -0
  76. mcgram-0.2.0/tests/test_config.py +126 -0
  77. mcgram-0.2.0/tests/test_config_ntfy.py +265 -0
  78. mcgram-0.2.0/tests/test_lock.py +57 -0
  79. mcgram-0.2.0/tests/test_ntfy_client.py +185 -0
  80. mcgram-0.2.0/tests/test_polling.py +125 -0
  81. mcgram-0.2.0/tests/test_polling_conflict.py +88 -0
  82. mcgram-0.2.0/tests/test_rate_limiter.py +44 -0
  83. mcgram-0.2.0/tests/test_reminders.py +137 -0
  84. mcgram-0.2.0/tests/test_server_main.py +105 -0
  85. mcgram-0.2.0/tests/test_server_smoke.py +86 -0
  86. mcgram-0.2.0/tests/test_skill_installer.py +52 -0
  87. mcgram-0.2.0/tests/test_tg_client.py +160 -0
  88. mcgram-0.2.0/tests/test_tools_ask.py +90 -0
  89. mcgram-0.2.0/tests/test_tools_ntfy.py +238 -0
  90. mcgram-0.2.0/tests/test_tools_reminder.py +49 -0
  91. mcgram-0.2.0/tests/test_tools_send_file.py +121 -0
  92. mcgram-0.2.0/tests/test_tools_send_message.py +89 -0
  93. mcgram-0.2.0/tests/test_tools_send_video.py +66 -0
  94. mcgram-0.2.0/tests/test_update_dispatcher.py +54 -0
  95. mcgram-0.2.0/uv.lock +1254 -0
@@ -0,0 +1,3 @@
1
+ # mcgram credentials — copy to ~/.mcgram/.env (never commit).
2
+ # Get token from @BotFather on Telegram (/newbot).
3
+ MCGRAM_BOT_TOKEN=
@@ -0,0 +1,50 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ test:
15
+ name: py${{ matrix.python }} / ${{ matrix.os }}
16
+ runs-on: ${{ matrix.os }}
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ python: ["3.11", "3.12"]
21
+ os: [ubuntu-latest, windows-latest]
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - name: Install uv
27
+ uses: astral-sh/setup-uv@v3
28
+
29
+ - name: Install Python ${{ matrix.python }}
30
+ run: uv python install ${{ matrix.python }}
31
+
32
+ - name: Sync dependencies
33
+ run: uv sync --extra dev
34
+
35
+ - name: Lint (ruff)
36
+ run: uv run ruff check src/ tests/
37
+
38
+ - name: Type check (mypy)
39
+ run: uv run mypy src/mcgram
40
+ continue-on-error: true # strict mode is opt-in; loosen later
41
+
42
+ - name: Run tests
43
+ run: uv run pytest --cov=mcgram --cov-report=xml --timeout=30
44
+
45
+ - name: Upload coverage to Codecov
46
+ if: matrix.python == '3.12' && matrix.os == 'ubuntu-latest'
47
+ uses: codecov/codecov-action@v4
48
+ with:
49
+ file: ./coverage.xml
50
+ fail_ci_if_error: false
@@ -0,0 +1,34 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ venv/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ .coverage.*
12
+ htmlcov/
13
+ coverage.xml
14
+
15
+ # Build
16
+ dist/
17
+ build/
18
+ *.whl
19
+
20
+ # Local config / runtime
21
+ .env
22
+ *.local.yaml
23
+ config.yaml
24
+ audit.jsonl
25
+ audit.jsonl.*
26
+ .lock
27
+
28
+ # IDE
29
+ .vscode/
30
+ .idea/
31
+
32
+ # OS
33
+ .DS_Store
34
+ Thumbs.db
@@ -0,0 +1,127 @@
1
+ # Changelog
2
+
3
+ All notable changes to mcgram will be documented in this file.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
6
+ this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [0.2.0] — 2026-05-22
9
+
10
+ ### Added
11
+ - **ntfy.sh transport** — one-way push notifications via the [ntfy.sh](https://ntfy.sh) HTTP API.
12
+ No bot, no token: subscribe to a topic in the ntfy mobile app and receive pings.
13
+ Useful for machines where `api.telegram.org` is blocked.
14
+ - `NtfyClient` async HTTP wrapper (`src/mcgram/ntfy_client.py`)
15
+ - `NtfyError` exception type
16
+ - `dispatch.py` — transport-aware send helpers (`send_text` / `send_document` / `send_video_file`)
17
+ - `Settings.ntfy: NtfyConfig | None` — optional `ntfy` config section with
18
+ `server`, `default_topic`, `access_token_env`
19
+ - `ChannelConfig.transport: telegram | ntfy` — every named channel declares its transport
20
+ - `Settings.resolve_destination(name)` — returns a `Destination` dataclass
21
+ carrying chat_id (telegram) or topic+server (ntfy). `resolve_channel()`
22
+ preserved as legacy alias for telegram-only callers.
23
+ - `BotConfig.disable_polling: bool` — keep `bot:` section but skip Telegram
24
+ long-poll on a specific machine (e.g. when api.telegram.org is blocked).
25
+ Eliminates poll-loop log spam in send-only deployments.
26
+ - `LimitsConfig.ntfy_file_max_bytes: int = 15728640` — effective `send_file` /
27
+ `send_video` cap for ntfy channels (15 MB matches public free tier).
28
+ Telegram channels keep using `file_max_bytes` (50 MB).
29
+ - CLI:
30
+ - `mcgram init` now generates a random ntfy topic (`mcgram-<16-hex>`) and writes
31
+ it to `ntfy.default_topic` in the scaffolded config
32
+ - `mcgram channel add-ntfy NAME [--topic T] [-d DESC]` — declare a ntfy channel
33
+ - `mcgram channel list` shows a transport column
34
+ - `mcgram doctor` runs per-transport checks (telegram + ntfy independent),
35
+ catches `httpx.HTTPError` (SSL/network) and reports as FAIL instead of crashing
36
+ - `mcgram clear-lock` — remove `~/.mcgram/.lock` after a crashed instance.
37
+ Use when MCP fails to start with error -32000 due to a stale PID-recycling
38
+ collision (see Known issues).
39
+ - Companion Claude Code skill (`SKILL.md`) rewritten to be transport-aware:
40
+ - Per-tool transport matrix (✅ / ❌ on telegram vs ntfy)
41
+ - Explicit guidance: `ask` is Telegram-only; on ntfy channels, return
42
+ `transport_unsupported` without retry — fall back to `send_message`
43
+ - PII/secrets warning for public ntfy topics
44
+ - Vietnamese trigger phrases added: "gửi qua ntfy", "push to my phone"
45
+ - Tests: +57 unit + integration tests (NtfyClient, ntfy config resolution,
46
+ transport branching in tools, ntfy CLI subcommands, doctor per-transport,
47
+ per-transport file size cap). Suite now at 223 tests.
48
+
49
+ ### Changed
50
+ - `Settings.bot` is now optional. Config must declare AT LEAST ONE of
51
+ `bot` or `ntfy`; both is valid. Telegram-only configs (pre-0.2 format)
52
+ continue to work unchanged.
53
+ - Tool responses now include a `transport` field (`"telegram"` | `"ntfy"`)
54
+ so callers know which backend handled the request.
55
+ - Audit log entries gain a `transport` field for the same reason.
56
+ - README quickstart now leads with ntfy (faster setup, no token);
57
+ Telegram retained as Option B for `ask` 2-way input.
58
+ - `_NEXT_STEPS` printed by `mcgram init` uses ASCII arrows (`->`) instead of
59
+ Unicode `→` for Windows console compatibility on legacy code pages.
60
+ - `mcgram init` idempotent path now reads `ntfy.default_topic` from the
61
+ EXISTING config when skipping, so the printed topic always matches what's
62
+ actually on disk.
63
+
64
+ ### Fixed
65
+ - `mcgram doctor` previously crashed with an uncaught `httpx.ConnectError`
66
+ when `api.telegram.org` was unreachable. Now reports `[FAIL] telegram ...
67
+ network/SSL error` and continues to check ntfy.
68
+
69
+ ### Security
70
+ - ntfy public topics are URL-discoverable. Generated topics use 64-bit
71
+ entropy (16 hex chars). Documentation in README + SKILL.md warns against
72
+ sending PII/secrets unless self-hosting ntfy with auth.
73
+ - `ntfy.access_token_env` supports Bearer token auth for paid/self-hosted
74
+ ntfy servers (unit tests cover the path; integration test against a real
75
+ authed server tracked in `TODO(ntfy-auth-e2e)` in `ntfy_client.py`).
76
+
77
+ ### Known issues
78
+ - **PID-recycling collision on Windows** — the single-instance lock at
79
+ `~/.mcgram/.lock` stores only the owner PID. If mcgram is killed ungracefully
80
+ (e.g. host reboot) and Windows later assigns the same PID to a different
81
+ long-running process (Python, IDE, etc.), the next `mcgram` invocation will
82
+ see the lock as "held" and refuse to start with error -32000 from Claude
83
+ Code's MCP layer. Workaround: run `mcgram clear-lock`. Proper fix (storing
84
+ process creation time alongside the PID) deferred.
85
+
86
+ ### Migration notes
87
+ - 0.1.x configs (`bot:` only) load unchanged on 0.2.0.
88
+ - To add ntfy on top of a working 0.1.x setup: either run `mcgram init --force`
89
+ (regenerates config, **erases your operator_chat_id**), or manually append an
90
+ `ntfy:` section to `~/.mcgram/config.yaml` (preserves bot config).
91
+ - On machines that block Telegram, set `bot.disable_polling: true` to stop the
92
+ poll loop without removing the bot section.
93
+
94
+ [0.2.0]: https://github.com/tvtdev94/mcgram/releases/tag/v0.2.0
95
+
96
+ ## [0.1.0] — 2026-05-22
97
+
98
+ ### Added
99
+ - MCP stdio server (`mcgram`) exposing 6 tools:
100
+ - `send_message(text, silent?, parse_mode?)` — post text to operator chat
101
+ - `send_file(path, caption?, silent?)` — upload file (≤50 MB, CWD-guarded)
102
+ - `ask(question, options?, timeout_s?)` — block until reply (button/freetext/timeout)
103
+ - `set_reminder(text, delay_s)` — in-process scheduler (≤10 pending, ≤24h)
104
+ - `cancel_reminder(reminder_id)`
105
+ - `list_reminders()`
106
+ - Async Telegram Bot API client (`httpx` wrapper, long-poll loop with backoff)
107
+ - Operator allowlist enforced at dispatcher entry (rejects non-`operator_chat_id` updates)
108
+ - Per-tool token-bucket rate limiter (default 20/min)
109
+ - JSONL audit log with `fsync`, 3-backup rotation, optional `redact_text`,
110
+ optional `retention_days` pruning
111
+ - Single-instance PID-file lock (cross-platform: Windows + POSIX)
112
+ - CLI: `mcgram init`, `mcgram doctor`, `mcgram audit`, `mcgram install-skill`
113
+ - Bundled companion Claude Code skill (`SKILL.md`) — installed by `mcgram init`,
114
+ recognizes both English and Vietnamese trigger phrases
115
+ - Docs: `README.md`, `docs/architecture.md`, `docs/security-threat-model.md`,
116
+ `docs/manual-smoke-test.md`
117
+ - GitHub Actions CI matrix: Python 3.11 + 3.12 × Ubuntu + Windows
118
+ - ≥80% test coverage with `pytest` + `pytest-httpx`
119
+
120
+ ### Security
121
+ - Bot token loaded from env var, never written to logs / audit / stdout
122
+ - `mcgram doctor` masks token to `***<last-4>`
123
+ - `send_file` rejects paths outside CWD by default; opt-in `allow_outside_cwd`
124
+ - Hard caps on `ask` timeout, reminder count + delay + text length, file size
125
+ - Full STRIDE analysis published before v0.1 release
126
+
127
+ [0.1.0]: https://github.com/tvtdev94/mcgram/releases/tag/v0.1.0
mcgram-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tvtdev94
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
mcgram-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,311 @@
1
+ Metadata-Version: 2.4
2
+ Name: mcgram
3
+ Version: 0.2.0
4
+ Summary: Notification bridge for Claude Code (Telegram + ntfy.sh) — send notifications, ask short questions, set reminders via Telegram bot or ntfy.sh topic.
5
+ Project-URL: Homepage, https://github.com/tvtdev94/mcgram
6
+ Project-URL: Repository, https://github.com/tvtdev94/mcgram
7
+ Project-URL: Issues, https://github.com/tvtdev94/mcgram/issues
8
+ Project-URL: Documentation, https://github.com/tvtdev94/mcgram#readme
9
+ Author: tvtdev94
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,bot,claude,claude-code,mcp,model-context-protocol,notifications,ntfy,push-notifications,reminders,telegram
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Communications :: Chat
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.11
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: mcp<2.0,>=1.0
26
+ Requires-Dist: pydantic-settings>=2.0
27
+ Requires-Dist: pydantic>=2.5
28
+ Requires-Dist: python-dotenv>=1.0
29
+ Requires-Dist: pyyaml>=6.0
30
+ Requires-Dist: tzdata>=2024.1; sys_platform == 'win32'
31
+ Provides-Extra: dev
32
+ Requires-Dist: mypy>=1.10; extra == 'dev'
33
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
34
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
35
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
36
+ Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
37
+ Requires-Dist: pytest>=8.0; extra == 'dev'
38
+ Requires-Dist: ruff>=0.4; extra == 'dev'
39
+ Description-Content-Type: text/markdown
40
+
41
+ <div align="center">
42
+
43
+ <img src="docs/images/logo.png" alt="mcgram logo" width="120" />
44
+
45
+ # mcgram
46
+
47
+ **Notification bridge for Claude Code** — Telegram **and** ntfy.sh. Claude can ping you, ask you, and remind you on your phone.
48
+
49
+ [![CI](https://github.com/tvtdev94/mcgram/actions/workflows/ci.yml/badge.svg)](https://github.com/tvtdev94/mcgram/actions/workflows/ci.yml)
50
+ [![PyPI](https://img.shields.io/pypi/v/mcgram.svg)](https://pypi.org/project/mcgram/)
51
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11+-blue.svg)](https://www.python.org/downloads/)
52
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
53
+ [![Tests](https://img.shields.io/badge/tests-153%20passing-brightgreen.svg)](#)
54
+ [![Coverage](https://img.shields.io/badge/coverage-84%25-brightgreen.svg)](#)
55
+
56
+ <img src="docs/images/hero.png" alt="mcgram hero" width="720" />
57
+
58
+ </div>
59
+
60
+ ---
61
+
62
+ ## What it does
63
+
64
+ 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).
65
+
66
+ | | |
67
+ |---|---|
68
+ | 📣 **Notify** | "Build passed in 3m12s ✅" / send the failing log as attachment |
69
+ | ❓ **Ask & wait** | Inline buttons (Approve / Cancel) with a timeout — Telegram only |
70
+ | ⏰ **Remind** | "nhắc tôi sau 30 phút check container logs" |
71
+ | 🎥 **Send video** | Demo recordings play right in-chat |
72
+ | 🛰 **Multi-channel** | Route messages to named destinations across both transports |
73
+ | 🌐 **No-token mode** | Use ntfy.sh on machines where `api.telegram.org` is blocked |
74
+
75
+ Single process, no daemon, no VPS, no webhook setup. Lives inside the MCP server while Claude Code is open.
76
+
77
+ ## How it fits together
78
+
79
+ <div align="center">
80
+ <img src="docs/images/flow.png" alt="Architecture flow" width="720" />
81
+ </div>
82
+
83
+ 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.
84
+
85
+ ## Quickstart
86
+
87
+ > **Prereqs:** Python 3.11+, [`uv`](https://docs.astral.sh/uv/) or `pipx`. Pick ONE transport below (or set up both).
88
+
89
+ ### Option A — ntfy.sh (fastest, no token, ~30 seconds)
90
+
91
+ Best for: machines where `api.telegram.org` is blocked, or you just want 1-way pings to your phone.
92
+
93
+ ```bash
94
+ # 1) Install + scaffold (generates a random ntfy topic for you)
95
+ uv tool install git+https://github.com/tvtdev94/mcgram # or: pipx install ...
96
+ mcgram init # writes ~/.mcgram/config.yaml with a unique topic
97
+
98
+ # 2) Install the ntfy mobile app: https://ntfy.sh/ (iOS / Android / Web)
99
+ # In the app, subscribe to the topic printed by `mcgram init` (e.g. mcgram-a1b2c3d4...)
100
+
101
+ # 3) Verify
102
+ mcgram doctor # ✅ ntfy health, ✅ test message delivered
103
+
104
+ # 4) Restart Claude Code → /mcp → mcgram appears → done
105
+ ```
106
+
107
+ 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.
108
+
109
+ > ⚠️ 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.
110
+
111
+ ### Option B — Telegram (required for 2-way `ask`)
112
+
113
+ Best for: you want Claude to wait for your Approve/Cancel button before deploying.
114
+
115
+ ```bash
116
+ # 1) Install + scaffold
117
+ uv tool install git+https://github.com/tvtdev94/mcgram
118
+ mcgram init
119
+
120
+ # 2) Create a bot with @BotFather → /newbot → copy token
121
+ # Find your chat ID: /start the bot, then visit
122
+ # https://api.telegram.org/bot<TOKEN>/getUpdates → copy chat.id
123
+
124
+ # 3) Paste credentials and uncomment the bot section
125
+ # Edit ~/.mcgram/.env → MCGRAM_BOT_TOKEN=...
126
+ # Edit ~/.mcgram/config.yaml → uncomment `bot:` block, set operator_chat_id
127
+
128
+ # 4) Verify
129
+ mcgram doctor # ✅ get_me OK, ✅ test message delivered
130
+
131
+ # 5) Restart Claude Code → /mcp → mcgram appears → done
132
+ ```
133
+
134
+ ### Both transports
135
+
136
+ Declare both `ntfy:` and `bot:` in config — each named channel picks its own transport.
137
+
138
+ ## The 7 tools
139
+
140
+ | Tool | Telegram | ntfy.sh | When to use |
141
+ |---|:---:|:---:|---|
142
+ | **`send_message`** | ✅ | ✅ | Notify on completion / send a status update |
143
+ | **`send_file`** | ✅ | ✅ | Send logs, screenshots, generated artifacts |
144
+ | **`send_video`** | ✅ | ✅ | Send a video that **plays in-chat** (mp4/mov/mkv/webm/m4v) |
145
+ | **`ask`** | ✅ | ❌ | Question with inline buttons OR freetext, blocks until reply. Telegram-only |
146
+ | **`set_reminder`** | ✅ | ✅ | Schedule an in-process reminder (lost on restart) |
147
+ | **`cancel_reminder`** | — | — | Cancel a pending reminder |
148
+ | **`list_reminders`** | — | — | List currently pending reminders |
149
+
150
+ ntfy.sh has no 2-way input, so `ask` returns `transport_unsupported` on ntfy channels. The companion [Claude Code skill](src/mcgram/data/skill/SKILL.md) (installed by `mcgram init`) teaches Claude when to call which tool — both **English** and **Vietnamese** trigger phrases are recognized.
151
+
152
+ ## Channels
153
+
154
+ Route messages to different destinations across both transports:
155
+
156
+ ```bash
157
+ mcgram channel add oncall -1001234567890 -d "Pager rotation" # Telegram
158
+ mcgram channel add-ntfy alerts -d "Build alerts" # ntfy.sh — auto-generates topic
159
+ mcgram channel add-ntfy alerts --topic mcgram-myalerts -d "..." # explicit topic
160
+ mcgram channel list
161
+ # alerts ntfy topic=mcgram-a1b2c3d4e5f6g7h8 Build alerts
162
+ # default ntfy topic=mcgram-xxxxxxxxxxxxxxxx Auto-created from ntfy.default_topic
163
+ # oncall telegram chat_id=-1001234567890 Pager rotation
164
+ ```
165
+
166
+ Then in Claude Code: *"send the failing log to the alerts channel"* → `send_file(path="…", channel="alerts")`.
167
+
168
+ ## CLI
169
+
170
+ ```bash
171
+ mcgram # MCP stdio server (Claude Code calls this)
172
+ mcgram init [--force] # scaffold ~/.mcgram/ + skill + auto-register MCP (generates ntfy topic)
173
+ mcgram doctor # config + connectivity check (per-transport)
174
+ mcgram audit [opts] # analyze audit.jsonl: --since 1h, --tool ask, --rejected, --tail
175
+ mcgram channel <action> # list | add NAME CHAT_ID | add-ntfy NAME [--topic T] | remove NAME
176
+ mcgram install-skill [--force] # reinstall ~/.claude/skills/mcgram/SKILL.md
177
+ ```
178
+
179
+ ## Config example
180
+
181
+ `~/.mcgram/config.yaml` — declare at least one of `ntfy` / `bot`:
182
+
183
+ ```yaml
184
+ ntfy: # Transport A — fast, no token
185
+ server: https://ntfy.sh
186
+ default_topic: mcgram-a1b2c3d4e5f6g7h8 # generated by `mcgram init`
187
+ # access_token_env: NTFY_TOKEN # optional, for paid/self-hosted
188
+
189
+ bot: # Transport B — required for `ask`
190
+ token_env: MCGRAM_BOT_TOKEN # token loaded from ~/.mcgram/.env
191
+ operator_chat_id: 123456789 # your personal chat ID
192
+
193
+ defaults:
194
+ parse_mode: plain # plain | markdown_v2
195
+ ask_timeout_s: 120
196
+ rate_limit_per_min: 20
197
+
198
+ limits:
199
+ ask_timeout_max_s: 600
200
+ reminder_max_delay_s: 86400 # 24h
201
+ reminder_max_pending: 10
202
+ file_max_bytes: 52428800 # 50 MB
203
+ ask_options_max: 6
204
+
205
+ channels: # optional named destinations
206
+ oncall: # Telegram channel
207
+ transport: telegram
208
+ chat_id: -1001234567890
209
+ description: Pager rotation
210
+ alerts: # ntfy.sh channel
211
+ transport: ntfy
212
+ ntfy_topic: mcgram-alerts-x9k2
213
+ description: Build alerts
214
+
215
+ audit:
216
+ path: ~/.mcgram/audit.jsonl
217
+ rotate_mb: 25
218
+ redact_text: false # true → outbound `text` masked in audit
219
+ retention_days: null # e.g. 14
220
+ timezone: UTC
221
+
222
+ allow_outside_cwd: false # send_file restricted to CWD by default
223
+ ```
224
+
225
+ ## Security model
226
+
227
+ | Layer | Mitigation |
228
+ |---|---|
229
+ | 🔐 **Token** | Loaded from env var; never logged; masked in `mcgram doctor` |
230
+ | 🚪 **Operator filter** | Non-`operator_chat_id` updates rejected at dispatcher entry — never reach tool handlers |
231
+ | 🛡 **`send_file` traversal** | Path resolved + checked against CWD; size capped at `file_max_bytes` |
232
+ | ⚡ **Rate limit** | Per-tool token bucket (default 20/min) |
233
+ | 🔒 **Single instance** | PID-file lock at `~/.mcgram/.lock`; second `mcgram` exits with `LockHeldError` |
234
+ | 📜 **Audit trail** | Every call logged JSONL with `fsync`; survives `kill -9` |
235
+ | 🛌 **Reminder spam** | Max 10 pending, 24h delay cap, 1000-char text cap |
236
+ | ⏱ **`ask` DoS** | Hard timeout cap (600s) so a forgetful user can't freeze Claude forever |
237
+ | 🔁 **Polling conflict** | Telegram 409 caught with clean error — no crash loop |
238
+
239
+ Full STRIDE analysis: [docs/security-threat-model.md](docs/security-threat-model.md).
240
+
241
+ ## Audit
242
+
243
+ ```bash
244
+ mcgram audit # summary: counts by status + by tool
245
+ mcgram audit --since 1h # last hour only
246
+ mcgram audit --tool send_file # filter by tool
247
+ mcgram audit --rejected # group rejections by reason
248
+ mcgram audit --tail # follow new entries (Ctrl-C to stop)
249
+ ```
250
+
251
+ Sample lines:
252
+
253
+ ```jsonc
254
+ {"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}
255
+ {"ts":"2026-05-21T10:00:05+00:00","tool":"ask","status":"ok","channel":"oncall","question_id":"q_a1","source":"button","ms":3200}
256
+ {"ts":"2026-05-21T10:00:10+00:00","tool":"send_file","status":"rejected","reason":"file_too_large","bytes":62914560}
257
+ ```
258
+
259
+ ## Known limitations
260
+
261
+ - **No persistent reminders** — schedules live in process memory. Restart = lost.
262
+ - **`ask` blocks the MCP call** — keep timeouts short or Claude waits idle.
263
+ - **`ask` is Telegram-only** — ntfy.sh has no 2-way input. On ntfy channels, `ask` returns `transport_unsupported` without contacting the network.
264
+ - **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.
265
+ - **ntfy.sh file size cap** — public free tier ~15 MB (vs Telegram's 50 MB). Self-hosted ntfy can raise the cap.
266
+ - **No remote control** — Claude can send, but the bot doesn't accept arbitrary commands FROM Telegram. (Future v0.2.)
267
+ - **No webhook / VPS support** — Telegram uses long-poll only.
268
+ - **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.)
269
+
270
+ ## Architecture
271
+
272
+ Full module map, lifecycle diagram, and data flow: [docs/architecture.md](docs/architecture.md).
273
+
274
+ ```
275
+ src/mcgram/
276
+ ├── cli.py · cli_init · cli_doctor · cli_audit · cli_channel · skill_installer
277
+ ├── config · errors · audit · lock · rate_limiter
278
+ ├── tg_client · ntfy_client · dispatch · update_dispatcher · polling · server · runtime
279
+ ├── ask_registry · reminders
280
+ └── tools/ send_message · send_file · send_video · ask · set_reminder · cancel_reminder · list_reminders
281
+ ```
282
+
283
+ All modules <200 LOC, 153 tests, ruff clean, py3.11/3.12 × ubuntu/windows in CI.
284
+
285
+ ## Update / uninstall
286
+
287
+ ```bash
288
+ uv tool upgrade mcgram # or: pipx upgrade mcgram
289
+ mcgram install-skill --force # refresh bundled Claude skill if changed
290
+ claude mcp remove mcgram # unregister from Claude Code
291
+ uv tool uninstall mcgram # remove binary
292
+ rm -rf ~/.mcgram ~/.claude/skills/mcgram # remove config + skill
293
+ ```
294
+
295
+ ## Develop
296
+
297
+ ```bash
298
+ git clone https://github.com/tvtdev94/mcgram && cd mcgram
299
+ uv sync --extra dev
300
+ uv run pytest -q
301
+ uv run ruff check src/ tests/
302
+ ```
303
+
304
+ ## Credits
305
+
306
+ - Patterns mirrored from sister project [dbread](https://github.com/tvtdev94/dbread) (read-only DB MCP).
307
+ - Built around the [Model Context Protocol](https://modelcontextprotocol.io/) and the Telegram Bot API.
308
+
309
+ ## License
310
+
311
+ MIT — see [LICENSE](LICENSE).