omni-notify-mcp 1.1.3 → 1.1.5
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/README.md +4 -4
- package/assets/screenshots/help-page.png +0 -0
- package/assets/screenshots/main-ui.png +0 -0
- package/dist/channels/desktop.js +31 -1
- package/dist/ui/server.js +48 -7
- package/package.json +3 -2
- package/ui/public/app.js +13 -0
- package/ui/public/index.html +8 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/logo.svg" width="128" height="128" alt="omni-notify-mcp">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/menih/notify-mcp/main/assets/logo.svg" width="128" height="128" alt="omni-notify-mcp">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">omni-notify-mcp</h1>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
</p>
|
|
20
20
|
|
|
21
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">
|
|
22
|
+
<img src="https://raw.githubusercontent.com/menih/notify-mcp/main/assets/screenshots/main-ui.png" width="900" alt="omni-notify-mcp config UI — channels and policies side by side, with live activity log">
|
|
23
23
|
</p>
|
|
24
24
|
|
|
25
25
|
---
|
|
@@ -86,7 +86,7 @@ Priority routing for `notify`:
|
|
|
86
86
|
## Features
|
|
87
87
|
|
|
88
88
|
### Channels
|
|
89
|
-
- **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle
|
|
89
|
+
- **Desktop** — native `node-notifier` (macOS/Windows/Linux). Per-channel **system-sound toggle** and optional **text-to-speech** (natural neural voice via `msedge-tts`, no API key).
|
|
90
90
|
- **Telegram** — bidirectional. The bot **replies in-thread** to user messages and acknowledges every inbound message so the user knows it landed.
|
|
91
91
|
- **SMS** — Twilio.
|
|
92
92
|
- **Email** — Gmail App Password (one click) or any SMTP. `ask` over email sends a reply link the user clicks to answer.
|
|
@@ -115,7 +115,7 @@ Cross-platform idle detection: Windows (PowerShell + `GetLastInputInfo`), macOS
|
|
|
115
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
116
|
|
|
117
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">
|
|
118
|
+
<img src="https://raw.githubusercontent.com/menih/notify-mcp/main/assets/screenshots/help-page.png" width="800" alt="Help page — copy-paste snippets for Claude Code, Cursor, VS Code, Claude Desktop, Windsurf, Zed">
|
|
119
119
|
</p>
|
|
120
120
|
|
|
121
121
|
### Activity log
|
|
Binary file
|
|
Binary file
|
package/dist/channels/desktop.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import notifier from "node-notifier";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
const DEFAULT_TTS_VOICE = "en-US-AndrewMultilingualNeural";
|
|
3
6
|
export async function sendDesktop(config, message) {
|
|
4
7
|
if (!config.enabled)
|
|
5
8
|
return;
|
|
@@ -9,10 +12,37 @@ export async function sendDesktop(config, message) {
|
|
|
9
12
|
if (wantSound && process.platform === "win32") {
|
|
10
13
|
spawn("powershell", [
|
|
11
14
|
"-NoProfile", "-Command",
|
|
12
|
-
"[
|
|
15
|
+
"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SystemSounds]::Asterisk.Play(); Start-Sleep -Milliseconds 600",
|
|
13
16
|
], { windowsHide: true, stdio: "ignore" });
|
|
14
17
|
}
|
|
18
|
+
if (config.tts) {
|
|
19
|
+
// Fire-and-forget: synthesize and play in the background so we don't
|
|
20
|
+
// block the MCP request on network + audio playback.
|
|
21
|
+
speak(message, config.ttsVoice ?? DEFAULT_TTS_VOICE).catch(() => { });
|
|
22
|
+
}
|
|
15
23
|
await new Promise((resolve, reject) => {
|
|
16
24
|
notifier.notify({ title: "Claude", message, sound: wantSound && process.platform !== "win32" }, (err) => err ? reject(err) : resolve());
|
|
17
25
|
});
|
|
18
26
|
}
|
|
27
|
+
export async function speak(text, voice = DEFAULT_TTS_VOICE) {
|
|
28
|
+
// Lazy import so boot doesn't pay for msedge-tts when TTS is off.
|
|
29
|
+
const mod = await import("msedge-tts");
|
|
30
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = mod;
|
|
31
|
+
const tts = new MsEdgeTTS();
|
|
32
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
|
33
|
+
const { mkdtempSync } = await import("fs");
|
|
34
|
+
const outDir = mkdtempSync(join(tmpdir(), "notify-tts-"));
|
|
35
|
+
const { audioFilePath } = await tts.toFile(outDir, text);
|
|
36
|
+
if (process.platform === "win32") {
|
|
37
|
+
spawn("powershell", [
|
|
38
|
+
"-NoProfile", "-Command",
|
|
39
|
+
`Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([uri]'${audioFilePath.replace(/\\/g, "\\\\")}'); $p.Play(); Start-Sleep -Seconds 10`,
|
|
40
|
+
], { windowsHide: true, stdio: "ignore" });
|
|
41
|
+
}
|
|
42
|
+
else if (process.platform === "darwin") {
|
|
43
|
+
spawn("afplay", [audioFilePath], { stdio: "ignore" });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
spawn("aplay", [audioFilePath], { stdio: "ignore" });
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/ui/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import twilio from "twilio";
|
|
|
13
13
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
15
15
|
import { z } from "zod";
|
|
16
|
+
import { tmpdir } from "os";
|
|
16
17
|
const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3737;
|
|
17
18
|
const REDIRECT_URI = `http://localhost:${PORT}/auth/google/callback`;
|
|
18
19
|
const PUBLIC_DIR = join(fileURLToPath(new URL("../../ui/public", import.meta.url)));
|
|
@@ -150,15 +151,15 @@ app.post("/api/config", (req, res) => {
|
|
|
150
151
|
// node-notifier's `sound: true` works reliably, no fallback needed.
|
|
151
152
|
app.post("/api/test/sound", (_req, res) => {
|
|
152
153
|
if (process.platform === "win32") {
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
154
|
+
// Use System.Media.SystemSounds.Asterisk — plays through the sound card
|
|
155
|
+
// (Windows notification sound), works on every machine. console::beep
|
|
156
|
+
// uses the PC speaker which modern hardware lacks.
|
|
156
157
|
spawn("powershell", [
|
|
157
158
|
"-NoProfile",
|
|
158
159
|
"-Command",
|
|
159
|
-
"[
|
|
160
|
+
"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SystemSounds]::Asterisk.Play(); Start-Sleep -Milliseconds 600",
|
|
160
161
|
], { windowsHide: true, stdio: "ignore" });
|
|
161
|
-
res.json({ ok: true, message: "
|
|
162
|
+
res.json({ ok: true, message: "Sound played (System.Media)" });
|
|
162
163
|
return;
|
|
163
164
|
}
|
|
164
165
|
notifier.notify({ title: "Claude Notify", message: "Sound test", sound: true, wait: false }, (err) => {
|
|
@@ -168,6 +169,38 @@ app.post("/api/test/sound", (_req, res) => {
|
|
|
168
169
|
res.json({ ok: true, message: "System sound triggered" });
|
|
169
170
|
});
|
|
170
171
|
});
|
|
172
|
+
async function speakText(text, voice) {
|
|
173
|
+
const mod = await import("msedge-tts");
|
|
174
|
+
const { MsEdgeTTS, OUTPUT_FORMAT } = mod;
|
|
175
|
+
const tts = new MsEdgeTTS();
|
|
176
|
+
await tts.setMetadata(voice, OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3);
|
|
177
|
+
const { mkdtempSync } = await import("fs");
|
|
178
|
+
const outDir = mkdtempSync(join(tmpdir(), "notify-tts-"));
|
|
179
|
+
const { audioFilePath } = await tts.toFile(outDir, text);
|
|
180
|
+
if (process.platform === "win32") {
|
|
181
|
+
spawn("powershell", [
|
|
182
|
+
"-NoProfile", "-Command",
|
|
183
|
+
`Add-Type -AssemblyName presentationCore; $p = New-Object System.Windows.Media.MediaPlayer; $p.Open([uri]'${audioFilePath.replace(/\\/g, "\\\\")}'); $p.Play(); Start-Sleep -Seconds 10`,
|
|
184
|
+
], { windowsHide: true, stdio: "ignore" });
|
|
185
|
+
}
|
|
186
|
+
else if (process.platform === "darwin") {
|
|
187
|
+
spawn("afplay", [audioFilePath], { stdio: "ignore" });
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
spawn("aplay", [audioFilePath], { stdio: "ignore" });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
app.post("/api/test/tts", async (_req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const cfg = loadConfig();
|
|
196
|
+
const voice = cfg.desktop?.ttsVoice ?? "en-US-AndrewMultilingualNeural";
|
|
197
|
+
await speakText("Notification from Claude. This is a voice test.", voice);
|
|
198
|
+
res.json({ ok: true, message: `TTS played (${voice})` });
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
171
204
|
app.post("/api/test/desktop", (_req, res) => {
|
|
172
205
|
const time = new Date().toLocaleTimeString();
|
|
173
206
|
const cfg = loadConfig();
|
|
@@ -175,7 +208,7 @@ app.post("/api/test/desktop", (_req, res) => {
|
|
|
175
208
|
if (wantSound && process.platform === "win32") {
|
|
176
209
|
spawn("powershell", [
|
|
177
210
|
"-NoProfile", "-Command",
|
|
178
|
-
"[
|
|
211
|
+
"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SystemSounds]::Asterisk.Play(); Start-Sleep -Milliseconds 600",
|
|
179
212
|
], { windowsHide: true, stdio: "ignore" });
|
|
180
213
|
}
|
|
181
214
|
notifier.notify({
|
|
@@ -558,12 +591,20 @@ async function sendNotification(message, priority, client) {
|
|
|
558
591
|
// Windows notification settings — fire a PowerShell beep alongside the
|
|
559
592
|
// toast so the audible cue is reliable. macOS/Linux: trust the OS.
|
|
560
593
|
if (wantSound && process.platform === "win32") {
|
|
594
|
+
// [console]::beep uses the PC speaker (motherboard buzzer), which
|
|
595
|
+
// modern laptops/desktops don't have — silent on most machines.
|
|
596
|
+
// SystemSounds.Asterisk plays through the actual sound card via
|
|
597
|
+
// the Windows notification sound, audible on every machine.
|
|
561
598
|
spawn("powershell", [
|
|
562
599
|
"-NoProfile", "-Command",
|
|
563
|
-
"[
|
|
600
|
+
"Add-Type -AssemblyName System.Windows.Forms; [System.Media.SystemSounds]::Asterisk.Play(); Start-Sleep -Milliseconds 600",
|
|
564
601
|
], { windowsHide: true, stdio: "ignore" });
|
|
565
602
|
}
|
|
566
603
|
const soundOpt = wantSound && process.platform !== "win32";
|
|
604
|
+
if (cfg.desktop?.tts) {
|
|
605
|
+
const voice = cfg.desktop?.ttsVoice ?? "en-US-AndrewMultilingualNeural";
|
|
606
|
+
speakText(message, voice).catch((err) => log("→", "tts", `ERROR: ${err instanceof Error ? err.message : String(err)}`, client));
|
|
607
|
+
}
|
|
567
608
|
await send("desktop", () => new Promise((res, rej) => notifier.notify({ title: "Claude Notify", message, sound: soundOpt }, (err) => err ? rej(err) : res())));
|
|
568
609
|
}
|
|
569
610
|
if (!desktopOnlyMode && cfg.telegram?.enabled && cfg.telegram.token && cfg.telegram.chatId) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omni-notify-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "An MCP server that lets AI agents (Claude, Cursor, etc.) reach you on any channel — desktop, Telegram, SMS, email — with two-way ask/reply, real-time inbox push, Do Not Disturb, idle gating, multi-session routing, and a one-page web UI for setup. Zero config code; configure once, agents call notify/ask.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"license": "MIT",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/menih/
|
|
39
|
+
"url": "https://github.com/menih/notify-mcp"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=18"
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
54
54
|
"express": "^4.19.2",
|
|
55
55
|
"googleapis": "^144.0.0",
|
|
56
|
+
"msedge-tts": "^2.0.5",
|
|
56
57
|
"node-notifier": "^10.0.1",
|
|
57
58
|
"nodemailer": "^6.9.9",
|
|
58
59
|
"open": "^10.1.0",
|
package/ui/public/app.js
CHANGED
|
@@ -44,6 +44,7 @@ function populateForm() {
|
|
|
44
44
|
// Desktop
|
|
45
45
|
$("desktop-enabled").checked = !!config.desktop?.enabled;
|
|
46
46
|
$("desktop-sound").checked = config.desktop?.sound !== false; // default on
|
|
47
|
+
$("desktop-tts").checked = !!config.desktop?.tts; // default off
|
|
47
48
|
|
|
48
49
|
// Email / Gmail
|
|
49
50
|
const email = config.email ?? {};
|
|
@@ -154,6 +155,7 @@ function saveDesktop() {
|
|
|
154
155
|
desktop: {
|
|
155
156
|
enabled: $("desktop-enabled").checked,
|
|
156
157
|
sound: $("desktop-sound").checked,
|
|
158
|
+
tts: $("desktop-tts").checked,
|
|
157
159
|
},
|
|
158
160
|
});
|
|
159
161
|
}
|
|
@@ -398,6 +400,17 @@ async function testSound() {
|
|
|
398
400
|
}
|
|
399
401
|
}
|
|
400
402
|
|
|
403
|
+
async function testTts() {
|
|
404
|
+
try {
|
|
405
|
+
const res = await fetch("/api/test/tts", { method: "POST" });
|
|
406
|
+
const json = await res.json();
|
|
407
|
+
if (!res.ok) throw new Error(json.error);
|
|
408
|
+
toast(json.message, "ok");
|
|
409
|
+
} catch (e) {
|
|
410
|
+
toast("TTS test failed: " + e, "error");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
401
414
|
// ── Test channels ─────────────────────────────────────────────────────────
|
|
402
415
|
|
|
403
416
|
async function testChannel(channel) {
|
package/ui/public/index.html
CHANGED
|
@@ -55,6 +55,14 @@
|
|
|
55
55
|
<span class="toggle-lbl">Play system sound</span>
|
|
56
56
|
<button class="btn btn-sm btn-ghost" onclick="testSound()">Test sound</button>
|
|
57
57
|
</div>
|
|
58
|
+
<div class="actions" style="margin-top:6px">
|
|
59
|
+
<label class="toggle-wrap">
|
|
60
|
+
<input type="checkbox" id="desktop-tts" onchange="saveDesktop()">
|
|
61
|
+
<span class="toggle"></span>
|
|
62
|
+
</label>
|
|
63
|
+
<span class="toggle-lbl">Speak notification</span>
|
|
64
|
+
<button class="btn btn-sm btn-ghost" onclick="testTts()">Test voice</button>
|
|
65
|
+
</div>
|
|
58
66
|
<span id="os-hint" class="os-tag hidden"></span>
|
|
59
67
|
</div>
|
|
60
68
|
</div>
|