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 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
@@ -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
- "[console]::beep(880,180); Start-Sleep -Milliseconds 60; [console]::beep(660,180)",
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
- // Direct PowerShell beep bypasses the notification subsystem entirely.
154
- // Plays an 800Hz tone for 200ms through the system speakers. Cannot be
155
- // muted by per-app notification settings.
154
+ // Use System.Media.SystemSounds.Asteriskplays 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
- "[console]::beep(880,180); Start-Sleep -Milliseconds 60; [console]::beep(660,180)",
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: "Beep sent (Windows console.beep)" });
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
- "[console]::beep(880,180); Start-Sleep -Milliseconds 60; [console]::beep(660,180)",
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
- "[console]::beep(880,180); Start-Sleep -Milliseconds 60; [console]::beep(660,180)",
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",
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/omni-notify-mcp"
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) {
@@ -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>