omni-notify-mcp 1.1.4 → 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
@@ -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.
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;
@@ -12,7 +15,34 @@ export async function sendDesktop(config, message) {
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)));
@@ -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();
@@ -568,6 +601,10 @@ async function sendNotification(message, priority, client) {
568
601
  ], { windowsHide: true, stdio: "ignore" });
569
602
  }
570
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
+ }
571
608
  await send("desktop", () => new Promise((res, rej) => notifier.notify({ title: "Claude Notify", message, sound: soundOpt }, (err) => err ? rej(err) : res())));
572
609
  }
573
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.4",
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",
@@ -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>