omni-notify-mcp 1.3.23 → 1.3.30
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 +70 -5
- package/package.json +3 -2
package/dist/ui/server.js
CHANGED
|
@@ -147,6 +147,16 @@ const SECRETS_PATH = process.env.NOTIFY_MCP_SECRETS_PATH
|
|
|
147
147
|
|| join(CONFIG_DIR, "notify-secrets.json");
|
|
148
148
|
// Backward compatibility for an existing per-user install: older builds,
|
|
149
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.
|
|
150
160
|
const LEGACY_SECRETS_PATH = join(homedir(), ".notify-mcp-secrets");
|
|
151
161
|
// AWS SendTextMessage requires E.164 (e.g. +14089812202) — strip spaces, dashes,
|
|
152
162
|
// parens, dots so a user-entered "+1 408 981 2202" is accepted.
|
|
@@ -206,7 +216,6 @@ function mergePreservingSecrets(existing, update) {
|
|
|
206
216
|
return merged;
|
|
207
217
|
}
|
|
208
218
|
const app = express();
|
|
209
|
-
app.use((req, _res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url} UA:${req.headers['user-agent']?.slice(0, 60)}`); next(); });
|
|
210
219
|
app.use(express.json({
|
|
211
220
|
verify: (req, _res, buf) => {
|
|
212
221
|
req.rawBody = Buffer.from(buf);
|
|
@@ -2306,23 +2315,47 @@ async function sendNotificationSummary(message, priority, client) {
|
|
|
2306
2315
|
const out = await sendNotification(message, priority, client);
|
|
2307
2316
|
return out.text;
|
|
2308
2317
|
}
|
|
2318
|
+
// A shell `KEY=VALUE` file — the format release.sh/setup-secrets.sh (this repo
|
|
2319
|
+
// and BullseyeShared's shared release scripts) write — never starts with `{`
|
|
2320
|
+
// and always has at least one bare `NAME=` line. Used only to make the #444
|
|
2321
|
+
// mismatch diagnosable: this is a heuristic, not a parser, and only runs after
|
|
2322
|
+
// JSON.parse already failed.
|
|
2323
|
+
function looksLikeShellSecrets(raw) {
|
|
2324
|
+
const trimmed = raw.trimStart();
|
|
2325
|
+
return !trimmed.startsWith("{") && /^[A-Z_][A-Z0-9_]*=/m.test(raw);
|
|
2326
|
+
}
|
|
2309
2327
|
function loadSecrets() {
|
|
2310
2328
|
const path = secretsFilePath();
|
|
2311
2329
|
if (!path)
|
|
2312
2330
|
return {};
|
|
2331
|
+
let raw;
|
|
2313
2332
|
try {
|
|
2314
|
-
|
|
2333
|
+
raw = readFileSync(path, "utf8");
|
|
2315
2334
|
}
|
|
2316
2335
|
catch (err) {
|
|
2317
2336
|
throw new SecretsUnreadable(path, err);
|
|
2318
2337
|
}
|
|
2338
|
+
try {
|
|
2339
|
+
return decodeB64Fields(JSON.parse(raw));
|
|
2340
|
+
}
|
|
2341
|
+
catch (err) {
|
|
2342
|
+
if (path === LEGACY_SECRETS_PATH && looksLikeShellSecrets(raw)) {
|
|
2343
|
+
throw new SecretsUnreadable(path, err, "this looks like a shell KEY=VALUE file (NPM_TOKEN=…/VSCE_PAT=…), the " +
|
|
2344
|
+
"format release.sh/setup-secrets.sh write — those moved to " +
|
|
2345
|
+
"~/.bullseye-release-secrets on 2026-09-13 (#444) specifically because " +
|
|
2346
|
+
"this path is reserved for JSON notify-channel secrets; move or delete " +
|
|
2347
|
+
"the file at this path rather than editing it");
|
|
2348
|
+
}
|
|
2349
|
+
throw new SecretsUnreadable(path, err);
|
|
2350
|
+
}
|
|
2319
2351
|
}
|
|
2320
2352
|
// A secrets file that EXISTS but cannot be read is a hard defect, not an empty
|
|
2321
2353
|
// result: the caller would otherwise import nothing and believe the user has no
|
|
2322
2354
|
// credentials, which is the silent-escalation failure this file exists to stop.
|
|
2323
2355
|
class SecretsUnreadable extends Error {
|
|
2324
|
-
constructor(path, cause) {
|
|
2325
|
-
|
|
2356
|
+
constructor(path, cause, hint) {
|
|
2357
|
+
const base = `secrets file at ${path} exists but could not be read: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
2358
|
+
super(hint ? `${base} — ${hint}` : base);
|
|
2326
2359
|
}
|
|
2327
2360
|
}
|
|
2328
2361
|
function secretsFilePath() {
|
|
@@ -2436,6 +2469,36 @@ function importCredsOnStart() {
|
|
|
2436
2469
|
log("·", "import", "secrets file present but every config field it maps to is already set — nothing imported");
|
|
2437
2470
|
}
|
|
2438
2471
|
}
|
|
2472
|
+
// Story #445 (#438's real lesson): a server with no channel configured
|
|
2473
|
+
// answers `/v1/health` and every MCP tool call exactly like a healthy one —
|
|
2474
|
+
// nothing else here would ever say otherwise. That is how a machine ran for
|
|
2475
|
+
// five days delivering nothing while looking fine. Called once at startup,
|
|
2476
|
+
// right after importCredsOnStart() has had its one chance to fix the
|
|
2477
|
+
// problem — loud, impossible to miss in the console, and naming every path
|
|
2478
|
+
// this process actually checked, so the next time this happens it is
|
|
2479
|
+
// diagnosed in the first minute, not five days later by someone measuring
|
|
2480
|
+
// real sends.
|
|
2481
|
+
const CHANNEL_LABELS = [
|
|
2482
|
+
["desktop", "Desktop"], ["telegram", "Telegram"], ["email", "Email"],
|
|
2483
|
+
["ntfy", "ntfy"], ["discord", "Discord"], ["slack", "Slack"],
|
|
2484
|
+
["teams", "Teams"], ["sms", "SMS"],
|
|
2485
|
+
];
|
|
2486
|
+
function warnIfNoChannelConfigured(cfg) {
|
|
2487
|
+
if (CHANNEL_LABELS.some(([key]) => cfg[key]?.enabled === true))
|
|
2488
|
+
return;
|
|
2489
|
+
const bar = "!".repeat(78);
|
|
2490
|
+
const exists = (p) => (existsSync(p) ? "exists" : "MISSING");
|
|
2491
|
+
console.log(`\n${bar}`);
|
|
2492
|
+
console.log(" NO NOTIFICATION CHANNEL IS CONFIGURED — every notify call will reach nobody.");
|
|
2493
|
+
console.log(bar);
|
|
2494
|
+
console.log(` Config file (${exists(CONFIG_PATH)}) : ${CONFIG_PATH}`);
|
|
2495
|
+
console.log(` Secrets file (${exists(SECRETS_PATH)}) : ${SECRETS_PATH}`);
|
|
2496
|
+
console.log(` Legacy path (${exists(LEGACY_SECRETS_PATH)}) : ${LEGACY_SECRETS_PATH}`);
|
|
2497
|
+
console.log(` Secrets actually in use: ${secretsFilePath() ?? "none of the above"}`);
|
|
2498
|
+
console.log(` Fix: open http://localhost:${PORT} and turn a channel on, or set`);
|
|
2499
|
+
console.log(` NOTIFY_MCP_SECRETS_PATH to a notify-secrets.json with real credentials.`);
|
|
2500
|
+
console.log(`${bar}\n`);
|
|
2501
|
+
}
|
|
2439
2502
|
async function slackPost(text) {
|
|
2440
2503
|
const { webhook } = slackCreds();
|
|
2441
2504
|
if (!webhook)
|
|
@@ -3142,7 +3205,6 @@ app.all("/mcp", async (req, res) => {
|
|
|
3142
3205
|
});
|
|
3143
3206
|
return;
|
|
3144
3207
|
}
|
|
3145
|
-
console.log("[debug-url]", req.method, req.url, "query:", JSON.stringify(req.query), "ua:", req.headers["user-agent"]);
|
|
3146
3208
|
const existingSessionId = req.headers["mcp-session-id"];
|
|
3147
3209
|
if (existingSessionId && httpTransports[existingSessionId]) {
|
|
3148
3210
|
const transport = httpTransports[existingSessionId];
|
|
@@ -3301,6 +3363,9 @@ const httpServer = app.listen(PORT, () => {
|
|
|
3301
3363
|
// config.json cannot serve a process whose secrets file IS populated. It
|
|
3302
3364
|
// reports the channel names it filled — never a secret value.
|
|
3303
3365
|
importCredsOnStart();
|
|
3366
|
+
// Import has now had its one chance to fix a channel-less config — if
|
|
3367
|
+
// nothing is enabled even after that, say so loudly (story #445).
|
|
3368
|
+
warnIfNoChannelConfigured(loadConfig());
|
|
3304
3369
|
// Live Telegram/Slack pollers hit real external channels — never under test.
|
|
3305
3370
|
if (!TEST_ENDPOINTS) {
|
|
3306
3371
|
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.30",
|
|
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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"build:ui": "tsc -p ui/tsconfig.json",
|
|
26
26
|
"start": "node dist/index.js",
|
|
27
27
|
"ui": "npm run build:ui && node dist/ui/server.js",
|
|
28
|
-
"test": "npm run build && node
|
|
28
|
+
"test": "npm run build && node tests/run-tests.js",
|
|
29
|
+
"test:files": "node tests/run-tests.js",
|
|
29
30
|
"prepublishOnly": "npm run build:mcp"
|
|
30
31
|
},
|
|
31
32
|
"dependencies": {
|