omni-notify-mcp 1.3.22 → 1.3.24
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/dist/ui/server.js
CHANGED
|
@@ -13,6 +13,7 @@ import nodemailer from "nodemailer";
|
|
|
13
13
|
import { PinpointSMSVoiceV2Client, SendTextMessageCommand, DescribePhoneNumbersCommand, DescribeVerifiedDestinationNumbersCommand } from "@aws-sdk/client-pinpoint-sms-voice-v2";
|
|
14
14
|
import { tmpdir } from "os";
|
|
15
15
|
import { sendWithRouting, idleReading, formatIdleReading, describePriorityRouting } from "./messaging/notificationEngine.js";
|
|
16
|
+
import { telegramSendErrorHint } from "./messaging/telegramError.js";
|
|
16
17
|
import { z } from "zod";
|
|
17
18
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
19
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
@@ -146,6 +147,16 @@ const SECRETS_PATH = process.env.NOTIFY_MCP_SECRETS_PATH
|
|
|
146
147
|
|| join(CONFIG_DIR, "notify-secrets.json");
|
|
147
148
|
// Backward compatibility for an existing per-user install: older builds,
|
|
148
149
|
// `setup-secrets.sh` and the README all placed the file at `~/.notify-mcp-secrets`.
|
|
150
|
+
//
|
|
151
|
+
// #444: that exact filename is ALSO where release.sh/setup-secrets.sh (this
|
|
152
|
+
// repo, and BullseyeShared's shared scripts/vscode-extension/release.sh and
|
|
153
|
+
// publish-ext.sh) now write PUBLISH credentials — shell `KEY=VALUE` lines
|
|
154
|
+
// (NPM_TOKEN=…, VSCE_PAT=…), a different, incompatible format from the JSON
|
|
155
|
+
// this constant reads. Those scripts were moved to `~/.bullseye-release-secrets`
|
|
156
|
+
// on 2026-09-13 specifically to stop colliding with this legacy path. Do NOT
|
|
157
|
+
// widen this constant's name, and do not let release tooling point back at it
|
|
158
|
+
// — see loadSecrets() below, which now names the mismatch instead of throwing
|
|
159
|
+
// a bare parse error when it happens anyway.
|
|
149
160
|
const LEGACY_SECRETS_PATH = join(homedir(), ".notify-mcp-secrets");
|
|
150
161
|
// AWS SendTextMessage requires E.164 (e.g. +14089812202) — strip spaces, dashes,
|
|
151
162
|
// parens, dots so a user-entered "+1 408 981 2202" is accepted.
|
|
@@ -554,7 +565,7 @@ app.post("/api/test/telegram", async (_req, res) => {
|
|
|
554
565
|
if (r.ok)
|
|
555
566
|
sent++;
|
|
556
567
|
else
|
|
557
|
-
errors.push(`${chatId}: ${
|
|
568
|
+
errors.push(`${chatId}: ${await telegramSendErrorHint(r)}`);
|
|
558
569
|
}
|
|
559
570
|
catch (err) {
|
|
560
571
|
errors.push(`${chatId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1510,10 +1521,11 @@ async function sendNotification(message, priority, client) {
|
|
|
1510
1521
|
headers: { "Content-Type": "application/json" },
|
|
1511
1522
|
body: JSON.stringify(body),
|
|
1512
1523
|
});
|
|
1513
|
-
if (r.ok)
|
|
1524
|
+
if (r.ok) {
|
|
1514
1525
|
sent++;
|
|
1515
|
-
|
|
1516
|
-
|
|
1526
|
+
continue;
|
|
1527
|
+
}
|
|
1528
|
+
errors.push(`${chatId}: ${await telegramSendErrorHint(r)}`);
|
|
1517
1529
|
}
|
|
1518
1530
|
if (sent === 0 && errors.length)
|
|
1519
1531
|
throw new Error(errors.join("; "));
|
|
@@ -2304,14 +2316,37 @@ async function sendNotificationSummary(message, priority, client) {
|
|
|
2304
2316
|
const out = await sendNotification(message, priority, client);
|
|
2305
2317
|
return out.text;
|
|
2306
2318
|
}
|
|
2319
|
+
// A shell `KEY=VALUE` file — the format release.sh/setup-secrets.sh (this repo
|
|
2320
|
+
// and BullseyeShared's shared release scripts) write — never starts with `{`
|
|
2321
|
+
// and always has at least one bare `NAME=` line. Used only to make the #444
|
|
2322
|
+
// mismatch diagnosable: this is a heuristic, not a parser, and only runs after
|
|
2323
|
+
// JSON.parse already failed.
|
|
2324
|
+
function looksLikeShellSecrets(raw) {
|
|
2325
|
+
const trimmed = raw.trimStart();
|
|
2326
|
+
return !trimmed.startsWith("{") && /^[A-Z_][A-Z0-9_]*=/m.test(raw);
|
|
2327
|
+
}
|
|
2307
2328
|
function loadSecrets() {
|
|
2308
2329
|
const path = secretsFilePath();
|
|
2309
2330
|
if (!path)
|
|
2310
2331
|
return {};
|
|
2332
|
+
let raw;
|
|
2333
|
+
try {
|
|
2334
|
+
raw = readFileSync(path, "utf8");
|
|
2335
|
+
}
|
|
2336
|
+
catch (err) {
|
|
2337
|
+
throw new SecretsUnreadable(path, err);
|
|
2338
|
+
}
|
|
2311
2339
|
try {
|
|
2312
|
-
return decodeB64Fields(JSON.parse(
|
|
2340
|
+
return decodeB64Fields(JSON.parse(raw));
|
|
2313
2341
|
}
|
|
2314
2342
|
catch (err) {
|
|
2343
|
+
if (path === LEGACY_SECRETS_PATH && looksLikeShellSecrets(raw)) {
|
|
2344
|
+
throw new SecretsUnreadable(path, err, "this looks like a shell KEY=VALUE file (NPM_TOKEN=…/VSCE_PAT=…), the " +
|
|
2345
|
+
"format release.sh/setup-secrets.sh write — those moved to " +
|
|
2346
|
+
"~/.bullseye-release-secrets on 2026-09-13 (#444) specifically because " +
|
|
2347
|
+
"this path is reserved for JSON notify-channel secrets; move or delete " +
|
|
2348
|
+
"the file at this path rather than editing it");
|
|
2349
|
+
}
|
|
2315
2350
|
throw new SecretsUnreadable(path, err);
|
|
2316
2351
|
}
|
|
2317
2352
|
}
|
|
@@ -2319,8 +2354,9 @@ function loadSecrets() {
|
|
|
2319
2354
|
// result: the caller would otherwise import nothing and believe the user has no
|
|
2320
2355
|
// credentials, which is the silent-escalation failure this file exists to stop.
|
|
2321
2356
|
class SecretsUnreadable extends Error {
|
|
2322
|
-
constructor(path, cause) {
|
|
2323
|
-
|
|
2357
|
+
constructor(path, cause, hint) {
|
|
2358
|
+
const base = `secrets file at ${path} exists but could not be read: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
2359
|
+
super(hint ? `${base} — ${hint}` : base);
|
|
2324
2360
|
}
|
|
2325
2361
|
}
|
|
2326
2362
|
function secretsFilePath() {
|
|
@@ -2434,6 +2470,36 @@ function importCredsOnStart() {
|
|
|
2434
2470
|
log("·", "import", "secrets file present but every config field it maps to is already set — nothing imported");
|
|
2435
2471
|
}
|
|
2436
2472
|
}
|
|
2473
|
+
// Story #445 (#438's real lesson): a server with no channel configured
|
|
2474
|
+
// answers `/v1/health` and every MCP tool call exactly like a healthy one —
|
|
2475
|
+
// nothing else here would ever say otherwise. That is how a machine ran for
|
|
2476
|
+
// five days delivering nothing while looking fine. Called once at startup,
|
|
2477
|
+
// right after importCredsOnStart() has had its one chance to fix the
|
|
2478
|
+
// problem — loud, impossible to miss in the console, and naming every path
|
|
2479
|
+
// this process actually checked, so the next time this happens it is
|
|
2480
|
+
// diagnosed in the first minute, not five days later by someone measuring
|
|
2481
|
+
// real sends.
|
|
2482
|
+
const CHANNEL_LABELS = [
|
|
2483
|
+
["desktop", "Desktop"], ["telegram", "Telegram"], ["email", "Email"],
|
|
2484
|
+
["ntfy", "ntfy"], ["discord", "Discord"], ["slack", "Slack"],
|
|
2485
|
+
["teams", "Teams"], ["sms", "SMS"],
|
|
2486
|
+
];
|
|
2487
|
+
function warnIfNoChannelConfigured(cfg) {
|
|
2488
|
+
if (CHANNEL_LABELS.some(([key]) => cfg[key]?.enabled === true))
|
|
2489
|
+
return;
|
|
2490
|
+
const bar = "!".repeat(78);
|
|
2491
|
+
const exists = (p) => (existsSync(p) ? "exists" : "MISSING");
|
|
2492
|
+
console.log(`\n${bar}`);
|
|
2493
|
+
console.log(" NO NOTIFICATION CHANNEL IS CONFIGURED — every notify call will reach nobody.");
|
|
2494
|
+
console.log(bar);
|
|
2495
|
+
console.log(` Config file (${exists(CONFIG_PATH)}) : ${CONFIG_PATH}`);
|
|
2496
|
+
console.log(` Secrets file (${exists(SECRETS_PATH)}) : ${SECRETS_PATH}`);
|
|
2497
|
+
console.log(` Legacy path (${exists(LEGACY_SECRETS_PATH)}) : ${LEGACY_SECRETS_PATH}`);
|
|
2498
|
+
console.log(` Secrets actually in use: ${secretsFilePath() ?? "none of the above"}`);
|
|
2499
|
+
console.log(` Fix: open http://localhost:${PORT} and turn a channel on, or set`);
|
|
2500
|
+
console.log(` NOTIFY_MCP_SECRETS_PATH to a notify-secrets.json with real credentials.`);
|
|
2501
|
+
console.log(`${bar}\n`);
|
|
2502
|
+
}
|
|
2437
2503
|
async function slackPost(text) {
|
|
2438
2504
|
const { webhook } = slackCreds();
|
|
2439
2505
|
if (!webhook)
|
|
@@ -2835,6 +2901,10 @@ function createMcpServer(clientId, sessionTag) {
|
|
|
2835
2901
|
? `\n\nReply with: @${sessionTag} <your answer>`
|
|
2836
2902
|
: `\n\nReply to this message with your answer.`;
|
|
2837
2903
|
for (const chatId of cfg.telegram.chatIds) {
|
|
2904
|
+
// A non-2xx Telegram response does NOT reject this fetch — only a
|
|
2905
|
+
// network-level failure does — so this used to silently do nothing
|
|
2906
|
+
// on e.g. a revoked token: no error, no log line, just a question
|
|
2907
|
+
// the user never saw and no clue why. Story #443: check r.ok too.
|
|
2838
2908
|
await fetch(`https://api.telegram.org/bot${cfg.telegram.token}/sendMessage`, {
|
|
2839
2909
|
method: "POST",
|
|
2840
2910
|
headers: { "Content-Type": "application/json" },
|
|
@@ -2842,7 +2912,10 @@ function createMcpServer(clientId, sessionTag) {
|
|
|
2842
2912
|
chat_id: chatId,
|
|
2843
2913
|
text: `${askPrefix} ${question}${replyHint}`,
|
|
2844
2914
|
}),
|
|
2845
|
-
})
|
|
2915
|
+
})
|
|
2916
|
+
.then(async (r) => { if (!r.ok)
|
|
2917
|
+
log("→", "ask:telegram", `ERROR: ${chatId}: ${await telegramSendErrorHint(r)}`, clientId); })
|
|
2918
|
+
.catch((err) => log("→", "ask:telegram", `ERROR: ${chatId}: ${err}`, clientId));
|
|
2846
2919
|
}
|
|
2847
2920
|
}
|
|
2848
2921
|
const email = cfg.email ?? {};
|
|
@@ -3292,6 +3365,9 @@ const httpServer = app.listen(PORT, () => {
|
|
|
3292
3365
|
// config.json cannot serve a process whose secrets file IS populated. It
|
|
3293
3366
|
// reports the channel names it filled — never a secret value.
|
|
3294
3367
|
importCredsOnStart();
|
|
3368
|
+
// Import has now had its one chance to fix a channel-less config — if
|
|
3369
|
+
// nothing is enabled even after that, say so loudly (story #445).
|
|
3370
|
+
warnIfNoChannelConfigured(loadConfig());
|
|
3295
3371
|
// Live Telegram/Slack pollers hit real external channels — never under test.
|
|
3296
3372
|
if (!TEST_ENDPOINTS) {
|
|
3297
3373
|
startTelegramListener();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omni-notify-mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.24",
|
|
4
4
|
"description": "Reach your AI agents from anywhere — and let them reach you. Every notification channel is set up in one config UI with one-click server launch: desktop, Telegram, Slack, SMS, email, ntfy. Agents then notify you when long work finishes or a decision is needed, ask you a question and wait for your answer, and receive and reply to the messages you send them — with Do Not Disturb and idle detection deciding what actually gets through.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|