@mcpcn/mcp-notification 1.0.3 → 1.0.4
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/index.js +180 -14
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -4,7 +4,60 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
4
4
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
5
|
import { exec } from 'child_process';
|
|
6
6
|
import { promisify } from 'util';
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import path from 'path';
|
|
7
10
|
const execAsync = promisify(exec);
|
|
11
|
+
// ---- Global cancellation state (cross-process) ----
|
|
12
|
+
const GLOBAL_STATE_DIR = path.join(os.homedir(), '.mcp', 'notification-mcp');
|
|
13
|
+
const GLOBAL_CANCEL_FILE = path.join(GLOBAL_STATE_DIR, 'cancel.json');
|
|
14
|
+
function ensureStateDir() {
|
|
15
|
+
try {
|
|
16
|
+
if (!fs.existsSync(GLOBAL_STATE_DIR)) {
|
|
17
|
+
fs.mkdirSync(GLOBAL_STATE_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { }
|
|
21
|
+
}
|
|
22
|
+
function readCancelState() {
|
|
23
|
+
try {
|
|
24
|
+
const raw = fs.readFileSync(GLOBAL_CANCEL_FILE, 'utf8');
|
|
25
|
+
const data = JSON.parse(raw);
|
|
26
|
+
return {
|
|
27
|
+
stopAll: Boolean(data.stopAll),
|
|
28
|
+
canceledIds: Array.isArray(data.canceledIds) ? data.canceledIds : [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { stopAll: false, canceledIds: [] };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function writeCancelState(state) {
|
|
36
|
+
ensureStateDir();
|
|
37
|
+
fs.writeFileSync(GLOBAL_CANCEL_FILE, JSON.stringify(state), 'utf8');
|
|
38
|
+
}
|
|
39
|
+
function isGlobalStopAll() {
|
|
40
|
+
return Boolean(readCancelState().stopAll);
|
|
41
|
+
}
|
|
42
|
+
function isGloballyCanceled(id) {
|
|
43
|
+
const state = readCancelState();
|
|
44
|
+
return Boolean(state.stopAll) || (state.canceledIds || []).includes(id);
|
|
45
|
+
}
|
|
46
|
+
function setGlobalStopAll(flag) {
|
|
47
|
+
const state = readCancelState();
|
|
48
|
+
state.stopAll = flag;
|
|
49
|
+
writeCancelState(state);
|
|
50
|
+
}
|
|
51
|
+
function cancelTaskGlobally(id) {
|
|
52
|
+
const state = readCancelState();
|
|
53
|
+
const set = new Set(state.canceledIds || []);
|
|
54
|
+
set.add(id);
|
|
55
|
+
state.canceledIds = Array.from(set);
|
|
56
|
+
writeCancelState(state);
|
|
57
|
+
}
|
|
58
|
+
function clearGlobalCancel() {
|
|
59
|
+
writeCancelState({ stopAll: false, canceledIds: [] });
|
|
60
|
+
}
|
|
8
61
|
/**
|
|
9
62
|
* Escapes special characters in strings for AppleScript
|
|
10
63
|
*/
|
|
@@ -97,6 +150,46 @@ function buildLinuxNotificationCommand(params) {
|
|
|
97
150
|
}
|
|
98
151
|
return command;
|
|
99
152
|
}
|
|
153
|
+
// 直接执行:macOS 通知
|
|
154
|
+
async function executeMacNotification(params) {
|
|
155
|
+
const command = buildNotificationCommand(params);
|
|
156
|
+
await execAsync(command);
|
|
157
|
+
}
|
|
158
|
+
// 直接执行:Windows 通知
|
|
159
|
+
async function executeWindowsNotification(params) {
|
|
160
|
+
const { title, message, sound = true } = params;
|
|
161
|
+
let script = `
|
|
162
|
+
Add-Type -AssemblyName System.Windows.Forms;
|
|
163
|
+
Add-Type -AssemblyName System.Drawing;
|
|
164
|
+
$notification = New-Object System.Windows.Forms.NotifyIcon;
|
|
165
|
+
$notification.Icon = [System.Drawing.SystemIcons]::Information;
|
|
166
|
+
$notification.BalloonTipTitle = "${escapeString(title)}";
|
|
167
|
+
$notification.BalloonTipText = "${escapeString(message)}";
|
|
168
|
+
$notification.Visible = $true;
|
|
169
|
+
$notification.ShowBalloonTip(5000);
|
|
170
|
+
`;
|
|
171
|
+
if (sound) {
|
|
172
|
+
script += `
|
|
173
|
+
[System.Media.SystemSounds]::Asterisk.Play();
|
|
174
|
+
`;
|
|
175
|
+
}
|
|
176
|
+
script += `
|
|
177
|
+
$sw = [Diagnostics.Stopwatch]::StartNew();
|
|
178
|
+
while ($sw.ElapsedMilliseconds -lt 6000) {
|
|
179
|
+
[System.Windows.Forms.Application]::DoEvents();
|
|
180
|
+
Start-Sleep -Milliseconds 100;
|
|
181
|
+
}
|
|
182
|
+
$notification.Dispose();
|
|
183
|
+
`;
|
|
184
|
+
const encoded = Buffer.from(script, 'utf16le').toString('base64');
|
|
185
|
+
const command = `powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand ${encoded}`;
|
|
186
|
+
await execAsync(command);
|
|
187
|
+
}
|
|
188
|
+
// 直接执行:Linux 通知
|
|
189
|
+
async function executeLinuxNotification(params) {
|
|
190
|
+
const command = buildLinuxNotificationCommand(params);
|
|
191
|
+
await execAsync(command);
|
|
192
|
+
}
|
|
100
193
|
/**
|
|
101
194
|
* 解析时间字符串为毫秒数
|
|
102
195
|
*/
|
|
@@ -140,6 +233,12 @@ async function sendNotification(params) {
|
|
|
140
233
|
}
|
|
141
234
|
const timeoutId = setTimeout(async () => {
|
|
142
235
|
try {
|
|
236
|
+
// 全局取消检查(支持重启后仍能停止)
|
|
237
|
+
if (isGloballyCanceled(notificationId)) {
|
|
238
|
+
// 从任务池中移除并停止
|
|
239
|
+
repeatNotificationPool.delete(notificationId);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
143
242
|
// 检查任务是否还在任务池中(可能已被取消)
|
|
144
243
|
const notification = repeatNotificationPool.get(notificationId);
|
|
145
244
|
if (!notification)
|
|
@@ -147,13 +246,17 @@ async function sendNotification(params) {
|
|
|
147
246
|
await sendNotification(notificationParams);
|
|
148
247
|
// 更新任务信息
|
|
149
248
|
notification.currentCount++;
|
|
150
|
-
//
|
|
151
|
-
|
|
249
|
+
// 调度下一次通知(再次检查 stopAll)
|
|
250
|
+
if (!isGlobalStopAll()) {
|
|
251
|
+
scheduleNextNotification(currentCount + 1);
|
|
252
|
+
}
|
|
152
253
|
}
|
|
153
254
|
catch (error) {
|
|
154
255
|
console.error('Repeated notification failed:', error);
|
|
155
256
|
// 即使失败也继续下一次
|
|
156
|
-
|
|
257
|
+
if (!isGlobalStopAll()) {
|
|
258
|
+
scheduleNextNotification(currentCount + 1);
|
|
259
|
+
}
|
|
157
260
|
}
|
|
158
261
|
}, repeatMs);
|
|
159
262
|
// 更新任务池中的任务信息
|
|
@@ -181,6 +284,10 @@ async function sendNotification(params) {
|
|
|
181
284
|
const delayMs = parseTimeDelay(params.delay);
|
|
182
285
|
notification.timeoutId = setTimeout(() => {
|
|
183
286
|
// 发送第一次通知并开始重复
|
|
287
|
+
if (isGloballyCanceled(notificationId)) {
|
|
288
|
+
repeatNotificationPool.delete(notificationId);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
184
291
|
sendNotification(notificationParams).then(() => {
|
|
185
292
|
notification.currentCount = 1;
|
|
186
293
|
scheduleNextNotification(1);
|
|
@@ -193,6 +300,10 @@ async function sendNotification(params) {
|
|
|
193
300
|
else {
|
|
194
301
|
// 没有初始延迟,立即开始第一次通知
|
|
195
302
|
try {
|
|
303
|
+
if (isGloballyCanceled(notificationId)) {
|
|
304
|
+
repeatNotificationPool.delete(notificationId);
|
|
305
|
+
return { message: 'Notification canceled before start' };
|
|
306
|
+
}
|
|
196
307
|
await sendNotification(notificationParams);
|
|
197
308
|
notification.currentCount = 1;
|
|
198
309
|
scheduleNextNotification(1);
|
|
@@ -214,9 +325,27 @@ async function sendNotification(params) {
|
|
|
214
325
|
throw new Error('Delay must be a positive number');
|
|
215
326
|
}
|
|
216
327
|
try {
|
|
217
|
-
//
|
|
218
|
-
|
|
328
|
+
// 生成任务ID,并加入任务池,便于后续管理/取消
|
|
329
|
+
const notificationId = generateNotificationId();
|
|
330
|
+
const record = {
|
|
331
|
+
id: notificationId,
|
|
332
|
+
params,
|
|
333
|
+
timeoutId: null,
|
|
334
|
+
currentCount: 0,
|
|
335
|
+
maxCount: 1,
|
|
336
|
+
startTime: Date.now(),
|
|
337
|
+
};
|
|
338
|
+
repeatNotificationPool.set(notificationId, record);
|
|
339
|
+
const timeoutId = setTimeout(async () => {
|
|
219
340
|
try {
|
|
341
|
+
if (isGloballyCanceled(notificationId)) {
|
|
342
|
+
repeatNotificationPool.delete(notificationId);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// 如果任务已被取消并从池中移除,则不再执行
|
|
346
|
+
const exists = repeatNotificationPool.get(notificationId);
|
|
347
|
+
if (!exists)
|
|
348
|
+
return;
|
|
220
349
|
// 创建不包含 delay 的参数对象,避免无限递归
|
|
221
350
|
const { delay, ...notificationParams } = params;
|
|
222
351
|
await sendNotification(notificationParams);
|
|
@@ -224,9 +353,15 @@ async function sendNotification(params) {
|
|
|
224
353
|
catch (error) {
|
|
225
354
|
console.error('Delayed notification failed:', error);
|
|
226
355
|
}
|
|
356
|
+
finally {
|
|
357
|
+
// 单次延迟任务执行完成后从任务池移除
|
|
358
|
+
repeatNotificationPool.delete(notificationId);
|
|
359
|
+
}
|
|
227
360
|
}, delayMs);
|
|
228
|
-
//
|
|
229
|
-
|
|
361
|
+
// 回填 timeoutId
|
|
362
|
+
record.timeoutId = timeoutId;
|
|
363
|
+
// 返回任务ID,便于调用方管理
|
|
364
|
+
return { notificationId, message: 'Delayed notification scheduled successfully' };
|
|
230
365
|
}
|
|
231
366
|
catch (error) {
|
|
232
367
|
throw new Error('Failed to schedule delayed notification');
|
|
@@ -236,21 +371,19 @@ async function sendNotification(params) {
|
|
|
236
371
|
try {
|
|
237
372
|
validateParams(params);
|
|
238
373
|
const os = getOS();
|
|
239
|
-
let command;
|
|
240
374
|
switch (os) {
|
|
241
375
|
case 'macos':
|
|
242
|
-
|
|
376
|
+
await executeMacNotification(params);
|
|
243
377
|
break;
|
|
244
378
|
case 'windows':
|
|
245
|
-
|
|
379
|
+
await executeWindowsNotification(params);
|
|
246
380
|
break;
|
|
247
381
|
case 'linux':
|
|
248
|
-
|
|
382
|
+
await executeLinuxNotification(params);
|
|
249
383
|
break;
|
|
250
384
|
default:
|
|
251
385
|
throw new Error(`Unsupported platform: ${os}`);
|
|
252
386
|
}
|
|
253
|
-
await execAsync(command);
|
|
254
387
|
return { message: 'Notification sent successfully' };
|
|
255
388
|
}
|
|
256
389
|
catch (error) {
|
|
@@ -402,7 +535,7 @@ class NotificationServer {
|
|
|
402
535
|
properties: {
|
|
403
536
|
action: {
|
|
404
537
|
type: 'string',
|
|
405
|
-
enum: ['stop_repeat_task', 'stop_all_repeat_tasks', 'get_active_repeat_tasks', 'get_repeat_task_info'],
|
|
538
|
+
enum: ['stop_repeat_task', 'stop_all_repeat_tasks', 'get_active_repeat_tasks', 'get_repeat_task_info', 'stop_all_repeat_tasks_globally', 'stop_repeat_task_globally', 'clear_global_state'],
|
|
406
539
|
description: 'stop_repeat_task: 停止指定的重复通知或提醒任务. stop_all_repeat_tasks: 停止所有重复通知或提醒任务. get_active_repeat_tasks: 获取所有活跃的重复通知或提醒任务. get_repeat_task_info: 获取指定重复通知或提醒任务的信息.'
|
|
407
540
|
},
|
|
408
541
|
taskId: {
|
|
@@ -447,6 +580,7 @@ class NotificationServer {
|
|
|
447
580
|
result.message,
|
|
448
581
|
},
|
|
449
582
|
],
|
|
583
|
+
isError: false,
|
|
450
584
|
};
|
|
451
585
|
}
|
|
452
586
|
case 'notification_task_management': {
|
|
@@ -461,6 +595,7 @@ class NotificationServer {
|
|
|
461
595
|
text: success ? `任务 ${taskId} 已成功停止` : `任务 ${taskId} 未找到`,
|
|
462
596
|
},
|
|
463
597
|
],
|
|
598
|
+
isError: false,
|
|
464
599
|
};
|
|
465
600
|
}
|
|
466
601
|
case 'stop_all_repeat_tasks': {
|
|
@@ -472,8 +607,33 @@ class NotificationServer {
|
|
|
472
607
|
text: `已停止 ${count} 个重复任务`,
|
|
473
608
|
},
|
|
474
609
|
],
|
|
610
|
+
isError: false,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
case 'stop_repeat_task_globally': {
|
|
614
|
+
if (typeof taskId !== 'string') {
|
|
615
|
+
throw new McpError(ErrorCode.InvalidParams, 'taskId is required for stop_repeat_task_globally');
|
|
616
|
+
}
|
|
617
|
+
cancelTaskGlobally(taskId);
|
|
618
|
+
// 也尝试本进程内停止
|
|
619
|
+
const success = stopRepeatNotification(taskId);
|
|
620
|
+
return {
|
|
621
|
+
content: [{ type: 'text', text: `已全局标记停止任务 ${taskId}` + (success ? '(并停止本进程任务)' : '') }],
|
|
622
|
+
isError: false,
|
|
475
623
|
};
|
|
476
624
|
}
|
|
625
|
+
case 'stop_all_repeat_tasks_globally': {
|
|
626
|
+
setGlobalStopAll(true);
|
|
627
|
+
const count = stopAllRepeatNotifications();
|
|
628
|
+
return {
|
|
629
|
+
content: [{ type: 'text', text: `已全局标记停止所有重复任务,并停止本进程内 ${count} 个任务` }],
|
|
630
|
+
isError: false,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
case 'clear_global_state': {
|
|
634
|
+
clearGlobalCancel();
|
|
635
|
+
return { content: [{ type: 'text', text: '已清除全局停止状态' }], isError: false };
|
|
636
|
+
}
|
|
477
637
|
case 'get_active_repeat_tasks': {
|
|
478
638
|
const tasks = getActiveRepeatNotifications();
|
|
479
639
|
return {
|
|
@@ -483,6 +643,7 @@ class NotificationServer {
|
|
|
483
643
|
text: JSON.stringify(tasks, null, 2),
|
|
484
644
|
},
|
|
485
645
|
],
|
|
646
|
+
isError: false,
|
|
486
647
|
};
|
|
487
648
|
}
|
|
488
649
|
case 'get_repeat_task_info': {
|
|
@@ -494,6 +655,7 @@ class NotificationServer {
|
|
|
494
655
|
text: info ? JSON.stringify(info, null, 2) : `任务 ${taskId} 未找到`,
|
|
495
656
|
},
|
|
496
657
|
],
|
|
658
|
+
isError: false,
|
|
497
659
|
};
|
|
498
660
|
}
|
|
499
661
|
default:
|
|
@@ -505,7 +667,11 @@ class NotificationServer {
|
|
|
505
667
|
}
|
|
506
668
|
}
|
|
507
669
|
catch (error) {
|
|
508
|
-
|
|
670
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
671
|
+
return {
|
|
672
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
673
|
+
isError: true,
|
|
674
|
+
};
|
|
509
675
|
}
|
|
510
676
|
});
|
|
511
677
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcpcn/mcp-notification",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "系统通知MCP服务器",
|
|
5
|
-
"packageManager": "
|
|
5
|
+
"packageManager": "yarn@1.22.22",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"build": "tsc && chmod +x dist/index.js",
|
|
25
25
|
"start": "node dist/index.js",
|
|
26
26
|
"dev": "tsc -w",
|
|
27
|
-
"clean": "rm -rf
|
|
28
|
-
"prepare": "
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"prepare": "npm run clean && npm run build"
|
|
29
29
|
},
|
|
30
30
|
"type": "module",
|
|
31
31
|
"license": "MIT",
|