omni-notify-mcp 1.0.1 → 1.1.3

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 menih
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.
package/README.md CHANGED
@@ -1,14 +1,47 @@
1
- # omni-notify-mcp
1
+ <p align="center">
2
+ <img src="assets/logo.svg" width="128" height="128" alt="omni-notify-mcp">
3
+ </p>
2
4
 
3
- A Model Context Protocol (MCP) server that lets AI assistants (Claude, Cursor, etc.) send notifications through multiple channels: **desktop**, **Telegram**, **WhatsApp**, **SMS**, and **email**.
5
+ <h1 align="center">omni-notify-mcp</h1>
4
6
 
5
- ## Quick Start
7
+ <p align="center">
8
+ <em>Reach me on any channel. Ask me anything. Get out of my way when I'm busy.</em><br>
9
+ An MCP server that gives AI agents (Claude, Cursor, etc.) a single
10
+ <code>notify</code> / <code>ask</code> interface — desktop, Telegram, SMS, email —
11
+ with two-way replies, idle gating, and Do Not Disturb.
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://www.npmjs.com/package/omni-notify-mcp"><img src="https://img.shields.io/npm/v/omni-notify-mcp.svg" alt="npm"></a>
16
+ <a href="https://marketplace.visualstudio.com/items?itemName=MeniHillel.omni-notify-mcp"><img src="https://img.shields.io/visual-studio-marketplace/v/MeniHillel.omni-notify-mcp?label=marketplace" alt="VS Code Marketplace"></a>
17
+ <img src="https://img.shields.io/badge/license-MIT-4ea3ff.svg" alt="MIT license">
18
+ <img src="https://img.shields.io/badge/MCP-compatible-4ea3ff.svg" alt="MCP compatible">
19
+ </p>
20
+
21
+ <p align="center">
22
+ <img src="assets/screenshots/main-ui.png" width="900" alt="omni-notify-mcp config UI — channels and policies side by side, with live activity log">
23
+ </p>
24
+
25
+ ---
26
+
27
+ ## Why
28
+
29
+ You step away from your machine and the AI is still working. **It needs to**:
30
+
31
+ - tell you something important happened (`notify`)
32
+ - ask you a question and wait for your answer (`ask`)
33
+ - check whether you've sent it an unsolicited message (`poll`, or live SSE)
34
+ - **not buzz your phone every 90 seconds** while you're sitting at the keyboard
35
+
36
+ `omni-notify-mcp` is the one MCP server that does all of that, on whatever channels you've configured, with the right level of "shut up" built in.
37
+
38
+ ## Quick start
6
39
 
7
40
  ```bash
8
41
  npx omni-notify-mcp
9
42
  ```
10
43
 
11
- Add to your MCP config (`~/.claude.json`, `.vscode/mcp.json`, or `claude_desktop_config.json`):
44
+ Add to your MCP config (`~/.claude.json`, `.vscode/mcp.json`, `claude_desktop_config.json`, etc.):
12
45
 
13
46
  ```json
14
47
  {
@@ -21,89 +54,138 @@ Add to your MCP config (`~/.claude.json`, `.vscode/mcp.json`, or `claude_desktop
21
54
  }
22
55
  ```
23
56
 
24
- Then create `~/.notify-mcp/config.json` (see [Configuration](#configuration)).
57
+ Then run the config UI to wire up your channels:
25
58
 
26
- ## Tools
59
+ ```bash
60
+ npx omni-notify-mcp ui
61
+ ```
27
62
 
28
- ### `notify`
29
- Send a notification. Priority controls which channels fire:
63
+ Open <http://localhost:3737>, toggle the channels you want, and hit Save. The MCP server picks up changes immediately — no restart.
64
+
65
+ ## What the agent gets
66
+
67
+ Six tools, all server-configured (the agent never names a channel):
68
+
69
+ | Tool | What it does |
70
+ |---|---|
71
+ | **`notify`** | Send a message to the user. Priority controls fan-out (see below). |
72
+ | **`ask`** | Send a question and **wait** for the user's reply (Telegram, or web link via email). |
73
+ | **`poll`** | Drain any unsolicited messages the user sent. |
74
+ | **`get_idle_seconds`** | Seconds since last keyboard/mouse input. -1 if unsupported. |
75
+ | **`get_idle_config`** | The server's idle-gating policy `{ enabled, thresholdSeconds }`. |
76
+ | **`get_dnd_status`** | Current DND state `{ active, reason }`. |
77
+
78
+ Priority routing for `notify`:
30
79
 
31
80
  | Priority | Channels |
32
- |----------|----------|
33
- | `low` | email only |
81
+ |---|---|
82
+ | `low` | email only |
34
83
  | `normal` | desktop + Telegram + email |
35
- | `high` | desktop + Telegram + WhatsApp + SMS + email |
84
+ | `high` | desktop + Telegram + SMS + email — **bypasses DND and idle gating** |
85
+
86
+ ## Features
87
+
88
+ ### Channels
89
+ - **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle**.
90
+ - **Telegram** — bidirectional. The bot **replies in-thread** to user messages and acknowledges every inbound message so the user knows it landed.
91
+ - **SMS** — Twilio.
92
+ - **Email** — Gmail App Password (one click) or any SMTP. `ask` over email sends a reply link the user clicks to answer.
93
+
94
+ ### Two-way (`ask`)
95
+ The agent calls `ask`, the question goes out on Telegram and email, and the call **blocks until the user replies** (or times out). Reply on Telegram → agent gets the text. Click the email reply link → agent gets the text. No glue code, no polling loop.
96
+
97
+ ### Real-time inbox push (SSE)
98
+ Subscribe to `GET /api/inbox/stream` (text/event-stream) to receive unsolicited user messages **the moment they arrive** — no polling. Ideal for an always-on agent that wants to react instantly. Per-session tag filtering supported (`?tag=alphawave`). Falls back gracefully to `poll` for clients without SSE.
99
+
100
+ ### Multi-session tagging
101
+ Run multiple agents against the same notify server (e.g. one Claude session in `repo-a`, another in `repo-b`). Each connects with `?tag=<name>` and the user can route a Telegram message to a specific agent by prefixing `@<name>`. Untagged messages broadcast to every session.
102
+
103
+ ### Do Not Disturb
104
+ - **Manual toggle** — flip it on, all `priority < high` notifs drop on the floor.
105
+ - **Scheduled quiet hours** — e.g. 22:00 → 08:00, configurable per-day.
106
+ - `priority='high'` always punches through.
107
+ - Agents can pre-flight with `get_dnd_status` to skip the round-trip when DND is on.
36
108
 
37
- ### `ask`
38
- Send a question and wait for a reply via Telegram.
109
+ ### Idle gating (anti-buzz)
110
+ The server publishes a policy `{ enabled, thresholdSeconds }`. Agents are **instructed** (via the MCP `instructions` field, surfaced to every connecting client) to call `get_idle_seconds` first, and **skip** sending a notification if you're actively at the keyboard. They can already see what they'd send. Only fire when you've stepped away. `priority='high'` always fires.
39
111
 
40
- ### `poll`
41
- Check the inbox for pending messages from the user.
112
+ Cross-platform idle detection: Windows (PowerShell + `GetLastInputInfo`), macOS (`ioreg`), Linux (`xprintidle`).
113
+
114
+ ### Web config UI
115
+ One page, dark theme, live activity log streaming over SSE, one-click test buttons per channel, secrets masked at rest. Plus a copy-paste help page that walks any AI client through registration in 30 seconds:
116
+
117
+ <p align="center">
118
+ <img src="assets/screenshots/help-page.png" width="800" alt="Help page — copy-paste snippets for Claude Code, Cursor, VS Code, Claude Desktop, Windsurf, Zed">
119
+ </p>
120
+
121
+ ### Activity log
122
+ Every notify, ask, reply, and inbox event is logged with timestamp, direction (`→` `←` `·`), channel, and (color-coded) client/session id. Visible live in the UI; last 500 entries replayed on connect.
123
+
124
+ ### Behavioral rules baked in
125
+ The MCP server ships with `instructions` that tell every connecting client:
126
+ 1. Pre-flight with `get_idle_seconds` and skip if user is active.
127
+ 2. Echo the message in chat too — don't trust the user is checking their phone.
128
+ 3. Use channel-agnostic wording ("notif", not "Telegram").
129
+ 4. Reply to inbox messages **through `notify`**, not just in chat.
130
+ 5. `priority='high'` is for blockers, not noise.
131
+
132
+ This means well-behaved agents get the right behavior automatically — no per-prompt nagging required.
42
133
 
43
134
  ## Configuration
44
135
 
45
- Create `~/.notify-mcp/config.json`:
136
+ Default location: `~/.notify-mcp/config.json`. The web UI manages this file for you, but the schema is straightforward:
46
137
 
47
138
  ```json
48
139
  {
49
- "desktop": {
50
- "enabled": true
51
- },
52
- "telegram": {
53
- "enabled": true,
54
- "token": "YOUR_BOT_TOKEN",
55
- "chatId": "YOUR_CHAT_ID"
56
- },
57
- "whatsapp": {
58
- "enabled": false,
59
- "instanceId": "YOUR_GREEN_API_INSTANCE_ID",
60
- "apiToken": "YOUR_GREEN_API_TOKEN",
61
- "phone": "+1234567890"
62
- },
140
+ "desktop": { "enabled": true, "sound": true },
141
+ "telegram": { "enabled": true, "token": "BOT_TOKEN", "chatId": "CHAT_ID" },
63
142
  "sms": {
64
143
  "enabled": false,
65
- "accountSid": "YOUR_TWILIO_ACCOUNT_SID",
66
- "authToken": "YOUR_TWILIO_AUTH_TOKEN",
67
- "from": "+1YOUR_TWILIO_NUMBER",
68
- "to": "+1YOUR_PERSONAL_NUMBER"
144
+ "accountSid": "ACxxxx",
145
+ "authToken": "...",
146
+ "from": "+15550000000",
147
+ "to": "+15550000001"
69
148
  },
70
149
  "email": {
71
150
  "enabled": true,
72
- "host": "smtp.gmail.com",
73
- "port": 587,
74
- "secure": false,
75
- "user": "your-email@gmail.com",
76
- "pass": "YOUR_GMAIL_APP_PASSWORD",
77
- "to": "your-email@gmail.com"
78
- }
151
+ "host": "smtp.gmail.com", "port": 587, "secure": false,
152
+ "user": "you@gmail.com", "pass": "GMAIL_APP_PASSWORD",
153
+ "to": "you@gmail.com"
154
+ },
155
+ "dnd": {
156
+ "enabled": false,
157
+ "schedule": {
158
+ "enabled": false,
159
+ "quietStart": "22:00", "quietEnd": "08:00",
160
+ "days": [0, 1, 2, 3, 4, 5, 6]
161
+ }
162
+ },
163
+ "idle": { "enabled": true, "thresholdSeconds": 120 }
79
164
  }
80
165
  ```
81
166
 
82
- Only enable the channels you need disabled channels are silently skipped.
167
+ Disabled channels are silently skipped. Secrets are masked when read back via the API.
83
168
 
84
- ### Channel Setup
169
+ ### Channel setup
85
170
 
86
- **Desktop** — works out of the box on macOS, Windows, and Linux.
171
+ - **Desktop** — works out of the box. Toggle `sound` to mute the system chime.
172
+ - **Telegram** — create a bot via [@BotFather](https://t.me/botfather), then click **Detect** in the UI to auto-fill the chat ID.
173
+ - **SMS** — Twilio account SID + auth token + a Twilio number.
174
+ - **Email** — Gmail [App Password](https://myaccount.google.com/apppasswords) (the UI walks you through it) or any SMTP host/user/pass.
87
175
 
88
- **Telegram**
89
- 1. Create a bot via [@BotFather](https://t.me/botfather) → get a token
90
- 2. Message your bot, then get your chat ID:
91
- ```
92
- https://api.telegram.org/bot<TOKEN>/getUpdates
93
- ```
176
+ ## Endpoints (for power users)
94
177
 
95
- **WhatsApp** uses [Green API](https://green-api.com). Create a free instance and paste the `instanceId` and `apiToken`.
178
+ The UI server (default `:3737`) also exposes:
96
179
 
97
- **SMS** uses [Twilio](https://twilio.com). Requires an account SID, auth token, and a Twilio phone number.
98
-
99
- **Email (SMTP)** works with any SMTP provider. For Gmail, use an [App Password](https://myaccount.google.com/apppasswords).
100
-
101
- **Email (Gmail OAuth)** run the built-in config UI for OAuth setup:
102
- ```bash
103
- npx omni-notify-mcp ui
104
- ```
105
- Then open http://localhost:3737.
180
+ | Path | Purpose |
181
+ |---|---|
182
+ | `POST /mcp[?tag=<name>]` | StreamableHTTP MCP transport. Optional session tag. |
183
+ | `GET /api/inbox/stream[?tag=<name>]` | SSE push of unsolicited user messages. |
184
+ | `GET /api/logs` | SSE stream of the activity log. |
185
+ | `GET/POST /api/config` | Read/write the config (secrets masked on read). |
186
+ | `POST /api/test/<channel>` | One-shot test send for desktop/telegram/sms/email. |
187
+ | `GET /reply/:token` | Web reply page for `ask` over email. |
106
188
 
107
189
  ## License
108
190
 
109
- MIT
191
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,111 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256" role="img" aria-label="omni-notify-mcp">
3
+ <title>omni-notify-mcp</title>
4
+ <defs>
5
+ <!-- Deep slate → bright blue, matches the UI accent -->
6
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
7
+ <stop offset="0%" stop-color="#1a3550"/>
8
+ <stop offset="55%" stop-color="#1f5a96"/>
9
+ <stop offset="100%" stop-color="#4ea3ff"/>
10
+ </linearGradient>
11
+ <linearGradient id="bell" x1="0" y1="0" x2="0" y2="1">
12
+ <stop offset="0%" stop-color="#ffffff"/>
13
+ <stop offset="100%" stop-color="#dbe8ff"/>
14
+ </linearGradient>
15
+ <!-- Sweep wedge: bright leading edge fading to transparent -->
16
+ <linearGradient id="sweep" x1="1" y1="0" x2="0" y2="1">
17
+ <stop offset="0%" stop-color="#a7ffb6" stop-opacity="0.95"/>
18
+ <stop offset="55%" stop-color="#34d399" stop-opacity="0.45"/>
19
+ <stop offset="100%" stop-color="#34d399" stop-opacity="0"/>
20
+ </linearGradient>
21
+ <radialGradient id="glow" cx="0.5" cy="0.4" r="0.6">
22
+ <stop offset="0%" stop-color="#ffffff" stop-opacity="0.18"/>
23
+ <stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
24
+ </radialGradient>
25
+
26
+ <!-- Clip the sweep wedge so it stays inside the outer radar ring -->
27
+ <clipPath id="radarClip">
28
+ <circle cx="128" cy="128" r="118"/>
29
+ </clipPath>
30
+ </defs>
31
+
32
+ <!-- Background fills the entire canvas (was inset 8px before — wasted space) -->
33
+ <rect x="0" y="0" width="256" height="256" rx="48" fill="url(#bg)"/>
34
+ <rect x="0" y="0" width="256" height="256" rx="48" fill="url(#glow)"/>
35
+
36
+ <!-- ── RADAR (full concentric range rings + sweep wedge) ───────────────── -->
37
+ <g>
38
+ <!-- Three full range rings — bigger, stronger -->
39
+ <circle cx="128" cy="128" r="118" fill="none" stroke="#ffffff" stroke-opacity="0.32" stroke-width="3"/>
40
+ <circle cx="128" cy="128" r="86" fill="none" stroke="#ffffff" stroke-opacity="0.42" stroke-width="3"/>
41
+ <circle cx="128" cy="128" r="54" fill="none" stroke="#ffffff" stroke-opacity="0.55" stroke-width="3"/>
42
+
43
+ <!-- Crosshair lines through center, edge-to-edge -->
44
+ <line x1="10" y1="128" x2="246" y2="128" stroke="#ffffff" stroke-opacity="0.22" stroke-width="1.8"/>
45
+ <line x1="128" y1="10" x2="128" y2="246" stroke="#ffffff" stroke-opacity="0.22" stroke-width="1.8"/>
46
+
47
+ <!-- Sweep wedge: 60° pie slice from center, leading edge to upper-right -->
48
+ <g clip-path="url(#radarClip)">
49
+ <path d="M128 128 L128 10 A118 118 0 0 1 230 69 Z"
50
+ fill="url(#sweep)"/>
51
+ </g>
52
+
53
+ <!-- Bright leading edge so the rotation reads -->
54
+ <line x1="128" y1="128" x2="230" y2="69"
55
+ stroke="#a7ffb6" stroke-width="3" stroke-opacity="0.95" stroke-linecap="round"/>
56
+
57
+ <!-- Center "ping" dot -->
58
+ <circle cx="128" cy="128" r="4" fill="#a7ffb6"/>
59
+ </g>
60
+
61
+ <!-- ── BELL (foreground icon, bigger — fills ~50% of the canvas) ──────── -->
62
+ <g>
63
+ <!-- Soft drop shadow -->
64
+ <path d="M128 56
65
+ C 96 56, 80 80, 80 116
66
+ L 80 152
67
+ C 80 162, 72 170, 66 178
68
+ L 190 178
69
+ C 184 170, 176 162, 176 152
70
+ L 176 116
71
+ C 176 80, 160 56, 128 56 Z"
72
+ fill="#0a1426" opacity="0.40" transform="translate(0,4)"/>
73
+ <!-- Bell body -->
74
+ <path d="M128 56
75
+ C 96 56, 80 80, 80 116
76
+ L 80 152
77
+ C 80 162, 72 170, 66 178
78
+ L 190 178
79
+ C 184 170, 176 162, 176 152
80
+ L 176 116
81
+ C 176 80, 160 56, 128 56 Z"
82
+ fill="url(#bell)" stroke="#1a3550" stroke-width="2.5"/>
83
+ <!-- Bell top stud -->
84
+ <circle cx="128" cy="48" r="8" fill="url(#bell)" stroke="#1a3550" stroke-width="2.5"/>
85
+ <!-- Bell clapper -->
86
+ <path d="M118 188
87
+ C 118 200, 138 200, 138 188 Z"
88
+ fill="url(#bell)" stroke="#1a3550" stroke-width="2.5"/>
89
+ </g>
90
+
91
+ <!-- ── CHAT BUBBLE (lower-right, bigger — denotes bidirectional ask/reply) -->
92
+ <g>
93
+ <path d="M158 154
94
+ h 50
95
+ a 16 16 0 0 1 16 16
96
+ v 32
97
+ a 16 16 0 0 1 -16 16
98
+ h -26
99
+ l -16 16
100
+ v -16
101
+ h -8
102
+ a 16 16 0 0 1 -16 -16
103
+ v -32
104
+ a 16 16 0 0 1 16 -16 Z"
105
+ fill="#0a1426" stroke="#ffffff" stroke-width="3.5" stroke-linejoin="round"/>
106
+ <!-- Three bubble dots = "typing / message" -->
107
+ <circle cx="170" cy="186" r="3.8" fill="#ffffff"/>
108
+ <circle cx="186" cy="186" r="3.8" fill="#ffffff"/>
109
+ <circle cx="202" cy="186" r="3.8" fill="#ffffff"/>
110
+ </g>
111
+ </svg>
Binary file
Binary file
@@ -1,26 +1,26 @@
1
- {
2
- "desktop": {
3
- "enabled": true
4
- },
5
- "whatsapp": {
6
- "enabled": true,
7
- "phone": "+1234567890",
8
- "apikey": "YOUR_CALLMEBOT_APIKEY"
9
- },
10
- "sms": {
11
- "enabled": true,
12
- "accountSid": "YOUR_TWILIO_ACCOUNT_SID",
13
- "authToken": "YOUR_TWILIO_AUTH_TOKEN",
14
- "from": "+1YOUR_TWILIO_NUMBER",
15
- "to": "+1YOUR_PERSONAL_NUMBER"
16
- },
17
- "email": {
18
- "enabled": true,
19
- "host": "smtp.gmail.com",
20
- "port": 587,
21
- "secure": false,
22
- "user": "your-email@gmail.com",
23
- "pass": "YOUR_GMAIL_APP_PASSWORD",
24
- "to": "your-email@gmail.com"
25
- }
26
- }
1
+ {
2
+ "desktop": {
3
+ "enabled": true
4
+ },
5
+ "whatsapp": {
6
+ "enabled": true,
7
+ "phone": "+1234567890",
8
+ "apikey": "YOUR_CALLMEBOT_APIKEY"
9
+ },
10
+ "sms": {
11
+ "enabled": true,
12
+ "accountSid": "YOUR_TWILIO_ACCOUNT_SID",
13
+ "authToken": "YOUR_TWILIO_AUTH_TOKEN",
14
+ "from": "+1YOUR_TWILIO_NUMBER",
15
+ "to": "+1YOUR_PERSONAL_NUMBER"
16
+ },
17
+ "email": {
18
+ "enabled": true,
19
+ "host": "smtp.gmail.com",
20
+ "port": 587,
21
+ "secure": false,
22
+ "user": "your-email@gmail.com",
23
+ "pass": "YOUR_GMAIL_APP_PASSWORD",
24
+ "to": "your-email@gmail.com"
25
+ }
26
+ }
@@ -1,13 +1,18 @@
1
1
  import notifier from "node-notifier";
2
+ import { spawn } from "child_process";
2
3
  export async function sendDesktop(config, message) {
3
4
  if (!config.enabled)
4
5
  return;
6
+ const wantSound = config.sound !== false;
7
+ // On Windows, SnoreToast's per-app sound is often muted in Windows settings.
8
+ // Fire a PowerShell beep alongside the toast so audio is reliable.
9
+ if (wantSound && process.platform === "win32") {
10
+ spawn("powershell", [
11
+ "-NoProfile", "-Command",
12
+ "[console]::beep(880,180); Start-Sleep -Milliseconds 60; [console]::beep(660,180)",
13
+ ], { windowsHide: true, stdio: "ignore" });
14
+ }
5
15
  await new Promise((resolve, reject) => {
6
- notifier.notify({ title: "Claude", message, sound: true }, (err) => {
7
- if (err)
8
- reject(err);
9
- else
10
- resolve();
11
- });
16
+ notifier.notify({ title: "Claude", message, sound: wantSound && process.platform !== "win32" }, (err) => err ? reject(err) : resolve());
12
17
  });
13
18
  }
package/dist/index.js CHANGED
File without changes