omni-notify-mcp 1.0.1 → 1.1.1

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,42 @@
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
+ <img src="https://img.shields.io/badge/license-MIT-7c6dfa.svg" alt="MIT license">
17
+ <img src="https://img.shields.io/badge/MCP-compatible-3b82f6.svg" alt="MCP compatible">
18
+ </p>
19
+
20
+ ---
21
+
22
+ ## Why
23
+
24
+ You step away from your machine and the AI is still working. **It needs to**:
25
+
26
+ - tell you something important happened (`notify`)
27
+ - ask you a question and wait for your answer (`ask`)
28
+ - check whether you've sent it an unsolicited message (`poll`, or live SSE)
29
+ - **not buzz your phone every 90 seconds** while you're sitting at the keyboard
30
+
31
+ `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.
32
+
33
+ ## Quick start
6
34
 
7
35
  ```bash
8
36
  npx omni-notify-mcp
9
37
  ```
10
38
 
11
- Add to your MCP config (`~/.claude.json`, `.vscode/mcp.json`, or `claude_desktop_config.json`):
39
+ Add to your MCP config (`~/.claude.json`, `.vscode/mcp.json`, `claude_desktop_config.json`, etc.):
12
40
 
13
41
  ```json
14
42
  {
@@ -21,89 +49,134 @@ Add to your MCP config (`~/.claude.json`, `.vscode/mcp.json`, or `claude_desktop
21
49
  }
22
50
  ```
23
51
 
24
- Then create `~/.notify-mcp/config.json` (see [Configuration](#configuration)).
52
+ Then run the config UI to wire up your channels:
53
+
54
+ ```bash
55
+ npx omni-notify-mcp ui
56
+ ```
57
+
58
+ Open <http://localhost:3737>, toggle the channels you want, and hit Save. The MCP server picks up changes immediately — no restart.
59
+
60
+ ## What the agent gets
25
61
 
26
- ## Tools
62
+ Six tools, all server-configured (the agent never names a channel):
27
63
 
28
- ### `notify`
29
- Send a notification. Priority controls which channels fire:
64
+ | Tool | What it does |
65
+ |---|---|
66
+ | **`notify`** | Send a message to the user. Priority controls fan-out (see below). |
67
+ | **`ask`** | Send a question and **wait** for the user's reply (Telegram, or web link via email). |
68
+ | **`poll`** | Drain any unsolicited messages the user sent. |
69
+ | **`get_idle_seconds`** | Seconds since last keyboard/mouse input. -1 if unsupported. |
70
+ | **`get_idle_config`** | The server's idle-gating policy `{ enabled, thresholdSeconds }`. |
71
+ | **`get_dnd_status`** | Current DND state `{ active, reason }`. |
72
+
73
+ Priority routing for `notify`:
30
74
 
31
75
  | Priority | Channels |
32
- |----------|----------|
33
- | `low` | email only |
76
+ |---|---|
77
+ | `low` | email only |
34
78
  | `normal` | desktop + Telegram + email |
35
- | `high` | desktop + Telegram + WhatsApp + SMS + email |
79
+ | `high` | desktop + Telegram + SMS + email — **bypasses DND and idle gating** |
80
+
81
+ ## Features
82
+
83
+ ### Channels
84
+ - **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle**.
85
+ - **Telegram** — bidirectional. The bot **replies in-thread** to user messages and acknowledges every inbound message so the user knows it landed.
86
+ - **SMS** — Twilio.
87
+ - **Email** — Gmail App Password (one click) or any SMTP. `ask` over email sends a reply link the user clicks to answer.
88
+
89
+ ### Two-way (`ask`)
90
+ 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.
91
+
92
+ ### Real-time inbox push (SSE)
93
+ 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.
94
+
95
+ ### Multi-session tagging
96
+ 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.
97
+
98
+ ### Do Not Disturb
99
+ - **Manual toggle** — flip it on, all `priority < high` notifs drop on the floor.
100
+ - **Scheduled quiet hours** — e.g. 22:00 → 08:00, configurable per-day.
101
+ - `priority='high'` always punches through.
102
+ - Agents can pre-flight with `get_dnd_status` to skip the round-trip when DND is on.
36
103
 
37
- ### `ask`
38
- Send a question and wait for a reply via Telegram.
104
+ ### Idle gating (anti-buzz)
105
+ 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
106
 
40
- ### `poll`
41
- Check the inbox for pending messages from the user.
107
+ Cross-platform idle detection: Windows (PowerShell + `GetLastInputInfo`), macOS (`ioreg`), Linux (`xprintidle`).
108
+
109
+ ### Web config UI
110
+ One page, dark theme, live activity log streaming over SSE, one-click test buttons per channel, secrets masked at rest.
111
+
112
+ ### Activity log
113
+ 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.
114
+
115
+ ### Behavioral rules baked in
116
+ The MCP server ships with `instructions` that tell every connecting client:
117
+ 1. Pre-flight with `get_idle_seconds` and skip if user is active.
118
+ 2. Echo the message in chat too — don't trust the user is checking their phone.
119
+ 3. Use channel-agnostic wording ("notif", not "Telegram").
120
+ 4. Reply to inbox messages **through `notify`**, not just in chat.
121
+ 5. `priority='high'` is for blockers, not noise.
122
+
123
+ This means well-behaved agents get the right behavior automatically — no per-prompt nagging required.
42
124
 
43
125
  ## Configuration
44
126
 
45
- Create `~/.notify-mcp/config.json`:
127
+ Default location: `~/.notify-mcp/config.json`. The web UI manages this file for you, but the schema is straightforward:
46
128
 
47
129
  ```json
48
130
  {
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
- },
131
+ "desktop": { "enabled": true, "sound": true },
132
+ "telegram": { "enabled": true, "token": "BOT_TOKEN", "chatId": "CHAT_ID" },
63
133
  "sms": {
64
134
  "enabled": false,
65
- "accountSid": "YOUR_TWILIO_ACCOUNT_SID",
66
- "authToken": "YOUR_TWILIO_AUTH_TOKEN",
67
- "from": "+1YOUR_TWILIO_NUMBER",
68
- "to": "+1YOUR_PERSONAL_NUMBER"
135
+ "accountSid": "ACxxxx",
136
+ "authToken": "...",
137
+ "from": "+15550000000",
138
+ "to": "+15550000001"
69
139
  },
70
140
  "email": {
71
141
  "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
- }
142
+ "host": "smtp.gmail.com", "port": 587, "secure": false,
143
+ "user": "you@gmail.com", "pass": "GMAIL_APP_PASSWORD",
144
+ "to": "you@gmail.com"
145
+ },
146
+ "dnd": {
147
+ "enabled": false,
148
+ "schedule": {
149
+ "enabled": false,
150
+ "quietStart": "22:00", "quietEnd": "08:00",
151
+ "days": [0, 1, 2, 3, 4, 5, 6]
152
+ }
153
+ },
154
+ "idle": { "enabled": true, "thresholdSeconds": 120 }
79
155
  }
80
156
  ```
81
157
 
82
- Only enable the channels you need disabled channels are silently skipped.
83
-
84
- ### Channel Setup
158
+ Disabled channels are silently skipped. Secrets are masked when read back via the API.
85
159
 
86
- **Desktop** works out of the box on macOS, Windows, and Linux.
160
+ ### Channel setup
87
161
 
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
- ```
162
+ - **Desktop** — works out of the box. Toggle `sound` to mute the system chime.
163
+ - **Telegram** — create a bot via [@BotFather](https://t.me/botfather), then click **Detect** in the UI to auto-fill the chat ID.
164
+ - **SMS** Twilio account SID + auth token + a Twilio number.
165
+ - **Email** — Gmail [App Password](https://myaccount.google.com/apppasswords) (the UI walks you through it) or any SMTP host/user/pass.
94
166
 
95
- **WhatsApp** uses [Green API](https://green-api.com). Create a free instance and paste the `instanceId` and `apiToken`.
167
+ ## Endpoints (for power users)
96
168
 
97
- **SMS** uses [Twilio](https://twilio.com). Requires an account SID, auth token, and a Twilio phone number.
169
+ The UI server (default `:3737`) also exposes:
98
170
 
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.
171
+ | Path | Purpose |
172
+ |---|---|
173
+ | `POST /mcp[?tag=<name>]` | StreamableHTTP MCP transport. Optional session tag. |
174
+ | `GET /api/inbox/stream[?tag=<name>]` | SSE push of unsolicited user messages. |
175
+ | `GET /api/logs` | SSE stream of the activity log. |
176
+ | `GET/POST /api/config` | Read/write the config (secrets masked on read). |
177
+ | `POST /api/test/<channel>` | One-shot test send for desktop/telegram/sms/email. |
178
+ | `GET /reply/:token` | Web reply page for `ask` over email. |
106
179
 
107
180
  ## License
108
181
 
109
- MIT
182
+ 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>
@@ -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