@mcpcn/mcp-notification 1.0.4 → 1.0.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.
Files changed (3) hide show
  1. package/README.md +167 -67
  2. package/dist/index.js +113 -641
  3. package/package.json +41 -39
package/README.md CHANGED
@@ -1,67 +1,167 @@
1
- # 通知MCP服务器
2
-
3
- 跨平台系统通知MCP服务器,支持macOS、Windows和Linux系统。
4
-
5
- ## 功能特性
6
-
7
- - 📢 发送系统通知
8
- - 延迟发送
9
- - 🔄 重复通知
10
- - 🎵 声音支持
11
- - 🖥️ 跨平台支持(macOS、Windows、Linux)
12
-
13
- ## 工具
14
-
15
- ### send_notification
16
- 发送系统通知
17
-
18
- **参数:**
19
- - `title` (必需): 通知标题
20
- - `message` (必需): 通知内容
21
- - `subtitle` (可选): 副标题
22
- - `sound` (可选): 是否播放声音(默认true)
23
- - `delay` (可选): 延迟发送(毫秒或"10s", "1m", "1h"格式)
24
- - `repeat` (可选): 重复间隔(毫秒或时间字符串)
25
- - `repeatCount` (可选): 重复次数
26
-
27
- ### notification_task_management
28
- 管理通知任务
29
-
30
- **参数:**
31
- - `action` (必需): 操作类型
32
- - `stop_repeat_task`: 停止指定任务
33
- - `stop_all_repeat_tasks`: 停止所有任务
34
- - `get_active_repeat_tasks`: 获取活跃任务
35
- - `get_repeat_task_info`: 获取任务信息
36
- - `taskId` (部分操作需要): 任务ID
37
-
38
- ## 安装和使用
39
-
40
- ```bash
41
- # 安装依赖
42
- pnpm install
43
-
44
- # 构建
45
- pnpm build
46
-
47
- # 运行
48
- pnpm start
49
- ```
50
-
51
- ## 平台支持
52
-
53
- - **macOS**: 使用AppleScript的`display notification`
54
- - **Windows**: 使用PowerShell的BalloonTip
55
- - **Linux**: 使用`notify-send`
56
-
57
- ## 示例
58
-
59
- ```json
60
- {
61
- "title": "提醒",
62
- "message": "这是一条测试通知",
63
- "subtitle": "测试",
64
- "sound": true,
65
- "delay": "5s"
66
- }
67
- ```
1
+ # MCP 通知提醒服务器
2
+
3
+ 一个基于 Model Context Protocol (MCP) 的通知提醒服务器,提供设置提醒、查询提醒列表、取消提醒三种工具,并通过后端接口对接统一的提醒调度与分发。
4
+
5
+ ## 功能特性
6
+
7
+ - 🔔 设置提醒:支持一次性、固定间隔循环、每日循环
8
+ - 📋 查询列表:获取设备的待触发提醒列表(仅 `scheduled`)
9
+ - 取消提醒:按 `id` 取消指定提醒
10
+ - MCP 协议集成:适配各类 MCP 客户端
11
+ - 🌐 可配置后端地址:通过环境变量 `REMINDER_API_BASE` 指定
12
+
13
+ ## 安装
14
+
15
+ ### 前置要求
16
+
17
+ - Node.js >= 18
18
+
19
+ ### 安装依赖
20
+
21
+ ```bash
22
+ npm install
23
+ ```
24
+
25
+ ### 构建项目
26
+
27
+ ```bash
28
+ npm run build
29
+ ```
30
+
31
+ ## 使用方法
32
+
33
+ ### 1. 直接运行
34
+
35
+ ```bash
36
+ npm run start
37
+ # 或
38
+ node dist/index.js
39
+ ```
40
+
41
+ ### 2. 作为 MCP 服务器
42
+
43
+ 在您的 MCP 客户端配置中添加:
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "通知提醒": {
49
+ "command": "node",
50
+ "args": ["/path/to/通知提醒新/dist/index.js"],
51
+ "env": {
52
+ "REMINDER_API_BASE": "https://www.rapido.chat/api"
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ 或使用已发布命令名(全局安装后):
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "通知提醒": {
65
+ "command": "notification-mcp",
66
+ "args": [],
67
+ "env": {
68
+ "REMINDER_API_BASE": "https://www.mcpcn.cc/api"
69
+ }
70
+ }
71
+ }
72
+ }
73
+ ```
74
+
75
+ ## 工具说明
76
+
77
+ ### set_reminder
78
+
79
+ 设置通知提醒,支持以下模式(请求需携带会话头 `chatSessionId`,详见下文“会话标识”):
80
+
81
+ - 一次性(相对延时)
82
+ ```json
83
+ { "content": "开会", "repeat": "none", "delaySec": 300 }
84
+ ```
85
+
86
+ - 一次性(绝对时间)
87
+ ```json
88
+ { "content": "开会", "repeat": "none", "triggerAt": "2025-11-15T20:00:00+08:00" }
89
+ ```
90
+
91
+ - 间隔循环(每5分钟)
92
+ ```json
93
+ { "content": "喝水", "repeat": "interval", "intervalSec": 300 }
94
+ ```
95
+
96
+ - 每日循环(每天18:00,北京时间)
97
+ ```json
98
+ { "content": "下班打卡", "repeat": "daily", "timeOfDay": "18:00", "tzOffsetMin": 480 }
99
+ ```
100
+
101
+ 参数:
102
+
103
+ - `content` (string, 必需)
104
+ - `repeat` (string, 必需):`none|interval|daily`
105
+ - `delaySec` (number, 可选):一次性延时触发
106
+ - `triggerAt` (string, 可选):一次性绝对时间(RFC3339)
107
+ - `intervalSec` (number, 可选):间隔循环秒数
108
+ - `timeOfDay` (string, 可选):每日循环的时间(如 `18:00` 或 `18:00:00`)
109
+ - `tzOffsetMin` (number, 可选):时区偏移分钟(北京为 `480`)
110
+
111
+ ### list_reminders
112
+
113
+ 查询待触发提醒列表(请求需携带会话头 `chatSessionId`):
114
+
115
+ 参数:无
116
+
117
+ 返回:提醒条目数组(仅 `scheduled` 状态)
118
+
119
+ ### cancel_reminder
120
+
121
+ 取消指定提醒(请求需携带会话头 `chatSessionId`):
122
+
123
+ 参数:
124
+
125
+ - `id` (string, 必需)
126
+
127
+ ## 会话标识
128
+
129
+ MCP 客户端需在调用工具时携带 `meta.chatSessionId`,服务端会自动解析并将其作为 HTTP 请求头 `chatSessionId` 传给后端接口:
130
+
131
+ - 解析来源:`request.meta.chatSessionId` 或 `request.params.meta.chatSessionId`
132
+ - 请求头:`chatSessionId: <meta.chatSessionId>`
133
+
134
+ ## 后端接口
135
+
136
+ 默认后端基地址为 `https://www.mcpcn.cc/api`(可通过 `REMINDER_API_BASE` 修改)。接口为:
137
+
138
+ - 设置提醒:`https://www.mcpcn.cc/api/reminder/set`
139
+ - 列表查询:`https://www.mcpcn.cc/api/reminder/list`
140
+ - 取消提醒:`https://www.mcpcn.cc/api/reminder/cancel`
141
+
142
+ ## 项目结构
143
+
144
+ ```
145
+ 通知提醒新/
146
+ ├── src/
147
+ │ └── index.ts # MCP 服务器实现(工具与接口调用)
148
+ ├── dist/
149
+ │ └── index.js # 构建输出文件
150
+ ├── package.json # 项目配置(main/bin 脚本)
151
+ ├── tsconfig.json # TypeScript 配置
152
+ └── README.md # 使用说明
153
+ ```
154
+
155
+ ## 技术栈
156
+
157
+ - **TypeScript**
158
+ - **Node.js**
159
+ - **@modelcontextprotocol/sdk**
160
+
161
+ ## 许可证
162
+
163
+ MIT License
164
+
165
+ ## 贡献
166
+
167
+ 欢迎提交 Issue 和 Pull Request!
package/dist/index.js CHANGED
@@ -1,685 +1,157 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
- import { exec } from 'child_process';
6
- import { promisify } from 'util';
7
- import fs from 'fs';
8
- import os from 'os';
9
- import path from 'path';
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
- }
61
- /**
62
- * Escapes special characters in strings for AppleScript
63
- */
64
- function escapeString(str) {
65
- // Escape for both AppleScript and shell
66
- return str
67
- .replace(/'/g, "'\\''")
68
- .replace(/"/g, '\\"');
69
- }
70
- // 任务池:存储所有活跃的重复提醒任务
71
- const repeatNotificationPool = new Map();
72
- /**
73
- * 生成唯一任务ID
74
- */
75
- function generateNotificationId() {
76
- return 'notification_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now().toString(36);
77
- }
78
- /**
79
- * Validates notification parameters
80
- */
81
- function validateParams(params) {
82
- if (!params.title || typeof params.title !== 'string') {
83
- throw new Error('Title is required and must be a string');
84
- }
85
- if (!params.message || typeof params.message !== 'string') {
86
- throw new Error('Message is required and must be a string');
87
- }
88
- if (params.subtitle && typeof params.subtitle !== 'string') {
89
- throw new Error('Subtitle must be a string');
90
- }
91
- }
92
- /**
93
- * Builds the AppleScript command for sending a notification
94
- */
95
- function buildNotificationCommand(params) {
96
- const { title, message, subtitle, sound = true } = params;
97
- let script = `display notification "${escapeString(message)}" with title "${escapeString(title)}"`;
98
- if (subtitle) {
99
- script += ` subtitle "${escapeString(subtitle)}"`;
100
- }
101
- if (sound) {
102
- script += ` sound name "default"`;
103
- }
104
- return `osascript -e '${script}'`;
105
- }
106
- // 检测操作系统
107
- function getOS() {
108
- const currentPlatform = process.platform;
109
- if (currentPlatform === 'win32')
110
- return 'windows';
111
- if (currentPlatform === 'darwin')
112
- return 'macos';
113
- return 'linux';
114
- }
115
- // Windows 通知命令构建
116
- function buildWindowsNotificationCommand(params) {
117
- const { title, message, sound = true } = params;
118
- // 使用 PowerShell 的 BalloonTip
119
- let script = `
120
- Add-Type -AssemblyName System.Windows.Forms;
121
- $notification = New-Object System.Windows.Forms.NotifyIcon;
122
- $notification.Icon = [System.Drawing.SystemIcons]::Information;
123
- $notification.BalloonTipTitle = "${escapeString(title)}";
124
- $notification.BalloonTipText = "${escapeString(message)}";
125
- $notification.Visible = $true;
126
- $notification.ShowBalloonTip(5000);
127
- `;
128
- if (sound) {
129
- script += `
130
- [System.Media.SystemSounds]::Asterisk.Play();
131
- `;
132
- }
133
- script += `
134
- Start-Sleep -Seconds 1;
135
- $notification.Dispose();
136
- `;
137
- return `powershell -Command "${script}"`;
138
- }
139
- // Linux 通知命令构建
140
- function buildLinuxNotificationCommand(params) {
141
- const { title, message, subtitle, sound = true } = params;
142
- let command = `notify-send "${escapeString(title)}" "${escapeString(message)}"`;
143
- if (subtitle) {
144
- // 将 subtitle 添加到消息中,因为 notify-send 不直接支持副标题
145
- command = `notify-send "${escapeString(title)}" "${escapeString(subtitle)}\n${escapeString(message)}"`;
146
- }
147
- // 添加声音支持
148
- if (sound) {
149
- command += ` --hint=string:sound-name:message-new-instant`;
150
- }
151
- return command;
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
- }
193
- /**
194
- * 解析时间字符串为毫秒数
195
- */
196
- function parseTimeDelay(delay) {
197
- if (typeof delay === 'number') {
198
- return delay;
199
- }
200
- const timeString = delay.toLowerCase().trim();
201
- const match = timeString.match(/^(\d+(?:\.\d+)?)\s*([smh]?)$/);
202
- if (!match) {
203
- throw new Error('Invalid time format. Use numbers (milliseconds) or strings like "10s", "1m", "1h"');
204
- }
205
- const value = parseFloat(match[1]);
206
- const unit = match[2] || 'ms'; // 默认单位为毫秒
207
- switch (unit) {
208
- case 's': return value * 1000; // 秒
209
- case 'm': return value * 60 * 1000; // 分钟
210
- case 'h': return value * 60 * 60 * 1000; // 小时
211
- default: return value; // 毫秒
212
- }
213
- }
214
- /**
215
- * Sends a notification using the appropriate platform command
216
- */
217
- async function sendNotification(params) {
218
- // 如果有 repeat 参数,设置重复提醒
219
- if (params.repeat !== undefined) {
220
- const repeatMs = parseTimeDelay(params.repeat);
221
- if (repeatMs <= 0) {
222
- throw new Error('Repeat interval must be a positive number');
223
- }
224
- const notificationId = generateNotificationId();
225
- const { repeat, repeatCount, ...notificationParams } = params;
226
- const maxCount = repeatCount || Infinity;
227
- // 创建重复发送的函数
228
- const scheduleNextNotification = (currentCount) => {
229
- if (currentCount >= maxCount) {
230
- // 任务完成,从任务池中移除
231
- repeatNotificationPool.delete(notificationId);
232
- return;
233
- }
234
- const timeoutId = setTimeout(async () => {
235
- try {
236
- // 全局取消检查(支持重启后仍能停止)
237
- if (isGloballyCanceled(notificationId)) {
238
- // 从任务池中移除并停止
239
- repeatNotificationPool.delete(notificationId);
240
- return;
241
- }
242
- // 检查任务是否还在任务池中(可能已被取消)
243
- const notification = repeatNotificationPool.get(notificationId);
244
- if (!notification)
245
- return;
246
- await sendNotification(notificationParams);
247
- // 更新任务信息
248
- notification.currentCount++;
249
- // 调度下一次通知(再次检查 stopAll)
250
- if (!isGlobalStopAll()) {
251
- scheduleNextNotification(currentCount + 1);
252
- }
253
- }
254
- catch (error) {
255
- console.error('Repeated notification failed:', error);
256
- // 即使失败也继续下一次
257
- if (!isGlobalStopAll()) {
258
- scheduleNextNotification(currentCount + 1);
259
- }
260
- }
261
- }, repeatMs);
262
- // 更新任务池中的任务信息
263
- const notification = repeatNotificationPool.get(notificationId);
264
- if (notification) {
265
- // 清除旧的timeout
266
- if (notification.timeoutId) {
267
- clearTimeout(notification.timeoutId);
268
- }
269
- notification.timeoutId = timeoutId;
270
- }
271
- };
272
- // 创建任务并加入任务池
273
- const notification = {
274
- id: notificationId,
275
- params,
276
- timeoutId: null, // 稍后设置
277
- currentCount: 0,
278
- maxCount,
279
- startTime: Date.now()
280
- };
281
- repeatNotificationPool.set(notificationId, notification);
282
- // 如果有初始延迟,先等待延迟再开始重复
283
- if (params.delay !== undefined) {
284
- const delayMs = parseTimeDelay(params.delay);
285
- notification.timeoutId = setTimeout(() => {
286
- // 发送第一次通知并开始重复
287
- if (isGloballyCanceled(notificationId)) {
288
- repeatNotificationPool.delete(notificationId);
289
- return;
290
- }
291
- sendNotification(notificationParams).then(() => {
292
- notification.currentCount = 1;
293
- scheduleNextNotification(1);
294
- }).catch(error => {
295
- console.error('Initial repeated notification failed:', error);
296
- scheduleNextNotification(1);
297
- });
298
- }, delayMs);
299
- }
300
- else {
301
- // 没有初始延迟,立即开始第一次通知
302
- try {
303
- if (isGloballyCanceled(notificationId)) {
304
- repeatNotificationPool.delete(notificationId);
305
- return { message: 'Notification canceled before start' };
306
- }
307
- await sendNotification(notificationParams);
308
- notification.currentCount = 1;
309
- scheduleNextNotification(1);
310
- }
311
- catch (error) {
312
- console.error('Initial repeated notification failed:', error);
313
- scheduleNextNotification(1);
314
- }
315
- }
316
- return {
317
- notificationId,
318
- message: `Repeat notification notification created with ID: ${notificationId}`
319
- };
320
- }
321
- // 如果有 delay 参数但没有 repeat,使用 setTimeout 延迟发送
322
- if (params.delay !== undefined) {
323
- const delayMs = parseTimeDelay(params.delay);
324
- if (delayMs <= 0) {
325
- throw new Error('Delay must be a positive number');
326
- }
327
- try {
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 () => {
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;
349
- // 创建不包含 delay 的参数对象,避免无限递归
350
- const { delay, ...notificationParams } = params;
351
- await sendNotification(notificationParams);
352
- }
353
- catch (error) {
354
- console.error('Delayed notification failed:', error);
355
- }
356
- finally {
357
- // 单次延迟任务执行完成后从任务池移除
358
- repeatNotificationPool.delete(notificationId);
359
- }
360
- }, delayMs);
361
- // 回填 timeoutId
362
- record.timeoutId = timeoutId;
363
- // 返回任务ID,便于调用方管理
364
- return { notificationId, message: 'Delayed notification scheduled successfully' };
365
- }
366
- catch (error) {
367
- throw new Error('Failed to schedule delayed notification');
368
- }
369
- }
370
- // 立即发送通知的逻辑
371
- try {
372
- validateParams(params);
373
- const os = getOS();
374
- switch (os) {
375
- case 'macos':
376
- await executeMacNotification(params);
377
- break;
378
- case 'windows':
379
- await executeWindowsNotification(params);
380
- break;
381
- case 'linux':
382
- await executeLinuxNotification(params);
383
- break;
384
- default:
385
- throw new Error(`Unsupported platform: ${os}`);
386
- }
387
- return { message: 'Notification sent successfully' };
388
- }
389
- catch (error) {
390
- if (error instanceof Error) {
391
- throw error;
392
- }
393
- // Handle different types of system errors
394
- const err = error;
395
- if (err.message.includes('execution error')) {
396
- throw new Error('Failed to execute notification command');
397
- }
398
- else if (err.message.includes('permission')) {
399
- throw new Error('Permission denied when trying to send notification');
400
- }
401
- else {
402
- throw new Error(`Unexpected error: ${err.message}`);
403
- }
404
- }
405
- }
406
- /**
407
- * 停止指定的重复提醒任务
408
- */
409
- function stopRepeatNotification(notificationId) {
410
- const notification = repeatNotificationPool.get(notificationId);
411
- if (!notification) {
412
- return false;
413
- }
414
- // 清除定时器
415
- if (notification.timeoutId) {
416
- clearTimeout(notification.timeoutId);
417
- }
418
- // 从任务池中移除
419
- repeatNotificationPool.delete(notificationId);
420
- return true;
421
- }
422
- /**
423
- * 停止所有重复提醒任务
424
- */
425
- function stopAllRepeatNotifications() {
426
- const count = repeatNotificationPool.size;
427
- // 清除所有定时器
428
- for (const notification of repeatNotificationPool.values()) {
429
- if (notification.timeoutId) {
430
- clearTimeout(notification.timeoutId);
431
- }
432
- }
433
- // 清空任务池
434
- repeatNotificationPool.clear();
435
- return count;
436
- }
437
- /**
438
- * 获取所有活跃的重复提醒任务信息
439
- */
440
- function getActiveRepeatNotifications() {
441
- return Array.from(repeatNotificationPool.values()).map(notification => ({
442
- ...notification,
443
- // 不返回timeoutId,避免序列化问题
444
- timeoutId: null
445
- }));
446
- }
447
- /**
448
- * 获取指定任务的信息
449
- */
450
- function getRepeatNotificationInfo(notificationId) {
451
- const notification = repeatNotificationPool.get(notificationId);
452
- if (!notification) {
453
- return null;
454
- }
455
- return {
456
- ...notification,
457
- // 不返回timeoutId,避免序列化问题
458
- timeoutId: null
459
- };
460
- }
461
- class NotificationServer {
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js';
5
+ const API_BASE = process.env.REMINDER_API_BASE || 'https://www.rapido.chat/api';
6
+ async function postJson(path, body, chatSessionId) {
7
+ const headers = { 'Content-Type': 'application/json' };
8
+ if (chatSessionId)
9
+ headers['chatSessionId'] = chatSessionId;
10
+ const resp = await fetch(`${API_BASE}${path}`, {
11
+ method: 'POST',
12
+ headers,
13
+ body: JSON.stringify(body),
14
+ });
15
+ if (!resp.ok) {
16
+ throw new Error(`HTTP 错误: ${resp.status} ${resp.statusText}`);
17
+ }
18
+ return (await resp.json());
19
+ }
20
+ async function getJson(path, chatSessionId) {
21
+ const headers = {};
22
+ if (chatSessionId)
23
+ headers['chatSessionId'] = chatSessionId;
24
+ const resp = await fetch(`${API_BASE}${path}`, { headers });
25
+ if (!resp.ok) {
26
+ throw new Error(`HTTP 错误: ${resp.status} ${resp.statusText}`);
27
+ }
28
+ return (await resp.json());
29
+ }
30
+ class ReminderServer {
462
31
  constructor() {
463
- this.server = new Server({
464
- name: 'notification-mcp',
465
- version: '1.0.0',
466
- }, {
467
- capabilities: {
468
- tools: {},
469
- },
470
- });
471
- this.setupToolHandlers();
472
- // Error handling
32
+ this.server = new Server({ name: 'notification-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
33
+ this.setupHandlers();
473
34
  this.server.onerror = (error) => console.error('[MCP Error]', error);
474
35
  process.on('SIGINT', async () => {
475
36
  await this.server.close();
476
37
  process.exit(0);
477
38
  });
478
39
  }
479
- setupToolHandlers() {
480
- // List available tools
40
+ setupHandlers() {
481
41
  this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
482
42
  tools: [
483
43
  {
484
- name: 'send_notification',
485
- description: '发送系统通知或提醒',
44
+ name: 'set_reminder',
45
+ description: '设置通知提醒。支持一次性、按间隔循环、每日循环。',
486
46
  inputSchema: {
487
47
  type: 'object',
488
48
  properties: {
489
- title: {
490
- type: 'string',
491
- description: '通知或提醒的标题',
492
- },
493
- message: {
494
- type: 'string',
495
- description: '通知或提醒的内容',
496
- },
497
- subtitle: {
498
- type: 'string',
499
- description: '可选的副标题',
500
- },
501
- sound: {
502
- type: 'boolean',
503
- description: '是否播放默认提示音',
504
- default: true,
505
- },
506
- delay: {
507
- oneOf: [
508
- { type: 'number' },
509
- { type: 'string' }
510
- ],
511
- description: '延迟发送通知或提醒(毫秒或时间字符串如"10s", "1m", "1h")',
512
- },
513
- repeat: {
514
- oneOf: [
515
- { type: 'number' },
516
- { type: 'string' }
517
- ],
518
- description: '重复通知或提醒的间隔(毫秒或时间字符串如"10s", "1m", "1h")',
519
- },
520
- repeatCount: {
521
- type: 'number',
522
- description: '重复次数(可选,如果设置了repeat但未设置此项则无限重复)',
523
- minimum: 1,
524
- },
49
+ content: { type: 'string' },
50
+ repeat: { type: 'string', enum: ['none', 'interval', 'daily'] },
51
+ delaySec: { type: 'number' },
52
+ triggerAt: { type: 'string', description: 'RFC3339 时间,例如 2025-11-15T20:00:00+08:00' },
53
+ intervalSec: { type: 'number' },
54
+ timeOfDay: { type: 'string', description: '例如 18:00 或 18:00:00' },
55
+ tzOffsetMin: { type: 'number', description: '时区偏移分钟,例如北京为 480' },
525
56
  },
526
- required: ['title', 'message'],
57
+ required: ['content', 'repeat'],
527
58
  additionalProperties: false,
528
59
  },
529
60
  },
530
61
  {
531
- name: 'notification_task_management',
532
- description: '管理计划的通知或提醒任务',
62
+ name: 'list_reminders',
63
+ description: '获取设备的提醒列表(仅未触发的 scheduled)。',
533
64
  inputSchema: {
534
65
  type: 'object',
535
- properties: {
536
- action: {
537
- type: 'string',
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'],
539
- description: 'stop_repeat_task: 停止指定的重复通知或提醒任务. stop_all_repeat_tasks: 停止所有重复通知或提醒任务. get_active_repeat_tasks: 获取所有活跃的重复通知或提醒任务. get_repeat_task_info: 获取指定重复通知或提醒任务的信息.'
540
- },
541
- taskId: {
542
- type: 'string',
543
- description: '要管理的任务ID'
544
- }
545
- },
546
- required: ['action'],
547
- additionalProperties: false
548
- }
66
+ properties: {},
67
+ required: [],
68
+ additionalProperties: false,
69
+ },
70
+ },
71
+ {
72
+ name: 'cancel_reminder',
73
+ description: '取消指定提醒。',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: { id: { type: 'string' } },
77
+ required: ['id'],
78
+ additionalProperties: false,
79
+ },
549
80
  },
550
81
  ],
551
82
  }));
552
- // Handle tool execution
553
83
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
554
84
  try {
555
85
  if (!request.params.arguments || typeof request.params.arguments !== 'object') {
556
- throw new McpError(ErrorCode.InvalidParams, 'Invalid parameters');
86
+ throw new McpError(ErrorCode.InvalidParams, '无效的参数');
87
+ }
88
+ const name = request.params.name;
89
+ const args = request.params.arguments;
90
+ const chatSessionId = request?.meta?.chatSessionId ??
91
+ request?.params?.meta?.chatSessionId ??
92
+ request?.params?.arguments?.meta?.chatSessionId;
93
+ if (!chatSessionId) {
94
+ console.error('未在请求中检测到 chatSessionId(meta.chatSessionId)');
557
95
  }
558
- switch (request.params.name) {
559
- case 'send_notification': {
560
- const { title, message, subtitle, sound, delay, repeat, repeatCount } = request.params.arguments;
561
- if (typeof title !== 'string' || typeof message !== 'string') {
562
- throw new McpError(ErrorCode.InvalidParams, 'Title and message must be strings');
563
- }
564
- const params = {
565
- title,
566
- message,
567
- subtitle: typeof subtitle === 'string' ? subtitle : undefined,
568
- sound: typeof sound === 'boolean' ? sound : undefined,
569
- delay: (typeof delay === 'number' || typeof delay === 'string') ? delay : undefined,
570
- repeat: (typeof repeat === 'number' || typeof repeat === 'string') ? repeat : undefined,
571
- repeatCount: typeof repeatCount === 'number' ? repeatCount : undefined
572
- };
573
- const result = await sendNotification(params);
574
- return {
575
- content: [
576
- {
577
- type: 'text',
578
- text: result.notificationId ?
579
- `${result.message}. Task ID: ${result.notificationId}` :
580
- result.message,
581
- },
582
- ],
583
- isError: false,
584
- };
96
+ else {
97
+ console.error(`接收到 chatSessionId: ${chatSessionId}`);
98
+ }
99
+ if (name === 'set_reminder') {
100
+ const params = {
101
+ content: String(args.content || ''),
102
+ repeat: String(args.repeat || ''),
103
+ delaySec: args.delaySec,
104
+ triggerAt: args.triggerAt,
105
+ intervalSec: args.intervalSec,
106
+ timeOfDay: args.timeOfDay,
107
+ tzOffsetMin: args.tzOffsetMin,
108
+ };
109
+ const resp = await postJson('/reminder/set', params, chatSessionId);
110
+ if (resp.code !== 0) {
111
+ return { content: [{ type: 'text', text: `设置失败:${resp.msg}` }], isError: true };
585
112
  }
586
- case 'notification_task_management': {
587
- const { action, taskId } = request.params.arguments;
588
- switch (action) {
589
- case 'stop_repeat_task': {
590
- const success = stopRepeatNotification(taskId);
591
- return {
592
- content: [
593
- {
594
- type: 'text',
595
- text: success ? `任务 ${taskId} 已成功停止` : `任务 ${taskId} 未找到`,
596
- },
597
- ],
598
- isError: false,
599
- };
600
- }
601
- case 'stop_all_repeat_tasks': {
602
- const count = stopAllRepeatNotifications();
603
- return {
604
- content: [
605
- {
606
- type: 'text',
607
- text: `已停止 ${count} 个重复任务`,
608
- },
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,
623
- };
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
- }
637
- case 'get_active_repeat_tasks': {
638
- const tasks = getActiveRepeatNotifications();
639
- return {
640
- content: [
641
- {
642
- type: 'text',
643
- text: JSON.stringify(tasks, null, 2),
644
- },
645
- ],
646
- isError: false,
647
- };
648
- }
649
- case 'get_repeat_task_info': {
650
- const info = getRepeatNotificationInfo(taskId);
651
- return {
652
- content: [
653
- {
654
- type: 'text',
655
- text: info ? JSON.stringify(info, null, 2) : `任务 ${taskId} 未找到`,
656
- },
657
- ],
658
- isError: false,
659
- };
660
- }
661
- default:
662
- throw new McpError(ErrorCode.MethodNotFound, `Unknown task management action: ${action}`);
663
- }
113
+ return {
114
+ content: [
115
+ {
116
+ type: 'text',
117
+ text: JSON.stringify({ id: resp.data?.id, triggerAt: resp.data?.triggerAt, msg: resp.msg || '设置成功' }, null, 2),
118
+ },
119
+ ],
120
+ isError: false,
121
+ };
122
+ }
123
+ if (name === 'list_reminders') {
124
+ const resp = await getJson(`/reminder/list`, chatSessionId);
125
+ if (resp.code !== 0) {
126
+ return { content: [{ type: 'text', text: `获取失败:${resp.msg}` }], isError: true };
127
+ }
128
+ return {
129
+ content: [{ type: 'text', text: JSON.stringify(resp.data?.list ?? [], null, 2) }],
130
+ isError: false,
131
+ };
132
+ }
133
+ if (name === 'cancel_reminder') {
134
+ const params = { id: String(args.id || '') };
135
+ const resp = await postJson(`/reminder/cancel`, params, chatSessionId);
136
+ if (resp.code !== 0) {
137
+ return { content: [{ type: 'text', text: `取消失败:${resp.msg}` }], isError: true };
664
138
  }
665
- default:
666
- throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
139
+ return { content: [{ type: 'text', text: resp.msg || '取消成功' }], isError: false };
667
140
  }
141
+ throw new McpError(ErrorCode.MethodNotFound, `未知工具: ${name}`);
668
142
  }
669
143
  catch (error) {
670
- const message = error instanceof Error ? error.message : String(error);
671
- return {
672
- content: [{ type: 'text', text: `Error: ${message}` }],
673
- isError: true,
674
- };
144
+ if (error instanceof McpError)
145
+ throw error;
146
+ throw new McpError(ErrorCode.InternalError, `执行失败: ${error.message}`);
675
147
  }
676
148
  });
677
149
  }
678
150
  async run() {
679
151
  const transport = new StdioServerTransport();
680
152
  await this.server.connect(transport);
681
- console.error('Notification MCP server running on stdio');
153
+ console.error('Reminder MCP server running on stdio');
682
154
  }
683
155
  }
684
- const server = new NotificationServer();
156
+ const server = new ReminderServer();
685
157
  server.run().catch(console.error);
package/package.json CHANGED
@@ -1,39 +1,41 @@
1
- {
2
- "name": "@mcpcn/mcp-notification",
3
- "version": "1.0.4",
4
- "description": "系统通知MCP服务器",
5
- "packageManager": "yarn@1.22.22",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "bin": {
9
- "notification-mcp": "./dist/index.js"
10
- },
11
- "files": [
12
- "dist/**/*"
13
- ],
14
- "engines": {
15
- "node": ">=18"
16
- },
17
- "keywords": [
18
- "mcp",
19
- "notification",
20
- "系统通知",
21
- "跨平台"
22
- ],
23
- "scripts": {
24
- "build": "tsc && chmod +x dist/index.js",
25
- "start": "node dist/index.js",
26
- "dev": "tsc -w",
27
- "clean": "rm -rf dist",
28
- "prepare": "npm run clean && npm run build"
29
- },
30
- "type": "module",
31
- "license": "MIT",
32
- "devDependencies": {
33
- "@types/node": "^22.10.2",
34
- "typescript": "^5.7.2"
35
- },
36
- "dependencies": {
37
- "@modelcontextprotocol/sdk": "^1.0.4"
38
- }
39
- }
1
+ {
2
+ "name": "@mcpcn/mcp-notification",
3
+ "version": "1.0.5",
4
+ "description": "系统通知MCP服务器",
5
+ "packageManager": "yarn@1.22.22",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "notification-mcp": "./dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist/**/*"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "mcp",
19
+ "notification",
20
+ "通知提醒",
21
+ "schedule",
22
+ "interval",
23
+ "daily"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc && node -e \"try{require('fs').chmodSync('dist/index.js',0o755)}catch(e){}\"",
27
+ "start": "node dist/index.js",
28
+ "dev": "tsc -w",
29
+ "clean": "node -e \"try{require('fs').rmSync('dist',{recursive:true,force:true})}catch(e){}\"",
30
+ "prepare": "npm run clean && npm run build"
31
+ },
32
+ "type": "module",
33
+ "license": "MIT",
34
+ "devDependencies": {
35
+ "@types/node": "^22.10.2",
36
+ "typescript": "^5.7.2"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.0.4"
40
+ }
41
+ }