@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.
Files changed (2) hide show
  1. package/dist/index.js +180 -14
  2. 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
- scheduleNextNotification(currentCount + 1);
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
- scheduleNextNotification(currentCount + 1);
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
- setTimeout(async () => {
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
- return { message: 'Delayed notification scheduled successfully' };
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
- command = buildNotificationCommand(params);
376
+ await executeMacNotification(params);
243
377
  break;
244
378
  case 'windows':
245
- command = buildWindowsNotificationCommand(params);
379
+ await executeWindowsNotification(params);
246
380
  break;
247
381
  case 'linux':
248
- command = buildLinuxNotificationCommand(params);
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
- throw error;
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",
3
+ "version": "1.0.4",
4
4
  "description": "系统通知MCP服务器",
5
- "packageManager": "pnpm@8.12.1",
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 build",
28
- "prepare": "pnpm clean && pnpm build"
27
+ "clean": "rm -rf dist",
28
+ "prepare": "npm run clean && npm run build"
29
29
  },
30
30
  "type": "module",
31
31
  "license": "MIT",