@mcpcn/mcp-notification 1.0.3 → 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.
- package/README.md +167 -67
- package/dist/index.js +111 -473
- package/package.json +41 -39
package/README.md
CHANGED
|
@@ -1,67 +1,167 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## 功能特性
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
###
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
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,519 +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
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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());
|
|
16
19
|
}
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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());
|
|
24
29
|
}
|
|
25
|
-
|
|
26
|
-
* Validates notification parameters
|
|
27
|
-
*/
|
|
28
|
-
function validateParams(params) {
|
|
29
|
-
if (!params.title || typeof params.title !== 'string') {
|
|
30
|
-
throw new Error('Title is required and must be a string');
|
|
31
|
-
}
|
|
32
|
-
if (!params.message || typeof params.message !== 'string') {
|
|
33
|
-
throw new Error('Message is required and must be a string');
|
|
34
|
-
}
|
|
35
|
-
if (params.subtitle && typeof params.subtitle !== 'string') {
|
|
36
|
-
throw new Error('Subtitle must be a string');
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Builds the AppleScript command for sending a notification
|
|
41
|
-
*/
|
|
42
|
-
function buildNotificationCommand(params) {
|
|
43
|
-
const { title, message, subtitle, sound = true } = params;
|
|
44
|
-
let script = `display notification "${escapeString(message)}" with title "${escapeString(title)}"`;
|
|
45
|
-
if (subtitle) {
|
|
46
|
-
script += ` subtitle "${escapeString(subtitle)}"`;
|
|
47
|
-
}
|
|
48
|
-
if (sound) {
|
|
49
|
-
script += ` sound name "default"`;
|
|
50
|
-
}
|
|
51
|
-
return `osascript -e '${script}'`;
|
|
52
|
-
}
|
|
53
|
-
// 检测操作系统
|
|
54
|
-
function getOS() {
|
|
55
|
-
const currentPlatform = process.platform;
|
|
56
|
-
if (currentPlatform === 'win32')
|
|
57
|
-
return 'windows';
|
|
58
|
-
if (currentPlatform === 'darwin')
|
|
59
|
-
return 'macos';
|
|
60
|
-
return 'linux';
|
|
61
|
-
}
|
|
62
|
-
// Windows 通知命令构建
|
|
63
|
-
function buildWindowsNotificationCommand(params) {
|
|
64
|
-
const { title, message, sound = true } = params;
|
|
65
|
-
// 使用 PowerShell 的 BalloonTip
|
|
66
|
-
let script = `
|
|
67
|
-
Add-Type -AssemblyName System.Windows.Forms;
|
|
68
|
-
$notification = New-Object System.Windows.Forms.NotifyIcon;
|
|
69
|
-
$notification.Icon = [System.Drawing.SystemIcons]::Information;
|
|
70
|
-
$notification.BalloonTipTitle = "${escapeString(title)}";
|
|
71
|
-
$notification.BalloonTipText = "${escapeString(message)}";
|
|
72
|
-
$notification.Visible = $true;
|
|
73
|
-
$notification.ShowBalloonTip(5000);
|
|
74
|
-
`;
|
|
75
|
-
if (sound) {
|
|
76
|
-
script += `
|
|
77
|
-
[System.Media.SystemSounds]::Asterisk.Play();
|
|
78
|
-
`;
|
|
79
|
-
}
|
|
80
|
-
script += `
|
|
81
|
-
Start-Sleep -Seconds 1;
|
|
82
|
-
$notification.Dispose();
|
|
83
|
-
`;
|
|
84
|
-
return `powershell -Command "${script}"`;
|
|
85
|
-
}
|
|
86
|
-
// Linux 通知命令构建
|
|
87
|
-
function buildLinuxNotificationCommand(params) {
|
|
88
|
-
const { title, message, subtitle, sound = true } = params;
|
|
89
|
-
let command = `notify-send "${escapeString(title)}" "${escapeString(message)}"`;
|
|
90
|
-
if (subtitle) {
|
|
91
|
-
// 将 subtitle 添加到消息中,因为 notify-send 不直接支持副标题
|
|
92
|
-
command = `notify-send "${escapeString(title)}" "${escapeString(subtitle)}\n${escapeString(message)}"`;
|
|
93
|
-
}
|
|
94
|
-
// 添加声音支持
|
|
95
|
-
if (sound) {
|
|
96
|
-
command += ` --hint=string:sound-name:message-new-instant`;
|
|
97
|
-
}
|
|
98
|
-
return command;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* 解析时间字符串为毫秒数
|
|
102
|
-
*/
|
|
103
|
-
function parseTimeDelay(delay) {
|
|
104
|
-
if (typeof delay === 'number') {
|
|
105
|
-
return delay;
|
|
106
|
-
}
|
|
107
|
-
const timeString = delay.toLowerCase().trim();
|
|
108
|
-
const match = timeString.match(/^(\d+(?:\.\d+)?)\s*([smh]?)$/);
|
|
109
|
-
if (!match) {
|
|
110
|
-
throw new Error('Invalid time format. Use numbers (milliseconds) or strings like "10s", "1m", "1h"');
|
|
111
|
-
}
|
|
112
|
-
const value = parseFloat(match[1]);
|
|
113
|
-
const unit = match[2] || 'ms'; // 默认单位为毫秒
|
|
114
|
-
switch (unit) {
|
|
115
|
-
case 's': return value * 1000; // 秒
|
|
116
|
-
case 'm': return value * 60 * 1000; // 分钟
|
|
117
|
-
case 'h': return value * 60 * 60 * 1000; // 小时
|
|
118
|
-
default: return value; // 毫秒
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Sends a notification using the appropriate platform command
|
|
123
|
-
*/
|
|
124
|
-
async function sendNotification(params) {
|
|
125
|
-
// 如果有 repeat 参数,设置重复提醒
|
|
126
|
-
if (params.repeat !== undefined) {
|
|
127
|
-
const repeatMs = parseTimeDelay(params.repeat);
|
|
128
|
-
if (repeatMs <= 0) {
|
|
129
|
-
throw new Error('Repeat interval must be a positive number');
|
|
130
|
-
}
|
|
131
|
-
const notificationId = generateNotificationId();
|
|
132
|
-
const { repeat, repeatCount, ...notificationParams } = params;
|
|
133
|
-
const maxCount = repeatCount || Infinity;
|
|
134
|
-
// 创建重复发送的函数
|
|
135
|
-
const scheduleNextNotification = (currentCount) => {
|
|
136
|
-
if (currentCount >= maxCount) {
|
|
137
|
-
// 任务完成,从任务池中移除
|
|
138
|
-
repeatNotificationPool.delete(notificationId);
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const timeoutId = setTimeout(async () => {
|
|
142
|
-
try {
|
|
143
|
-
// 检查任务是否还在任务池中(可能已被取消)
|
|
144
|
-
const notification = repeatNotificationPool.get(notificationId);
|
|
145
|
-
if (!notification)
|
|
146
|
-
return;
|
|
147
|
-
await sendNotification(notificationParams);
|
|
148
|
-
// 更新任务信息
|
|
149
|
-
notification.currentCount++;
|
|
150
|
-
// 调度下一次通知
|
|
151
|
-
scheduleNextNotification(currentCount + 1);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
console.error('Repeated notification failed:', error);
|
|
155
|
-
// 即使失败也继续下一次
|
|
156
|
-
scheduleNextNotification(currentCount + 1);
|
|
157
|
-
}
|
|
158
|
-
}, repeatMs);
|
|
159
|
-
// 更新任务池中的任务信息
|
|
160
|
-
const notification = repeatNotificationPool.get(notificationId);
|
|
161
|
-
if (notification) {
|
|
162
|
-
// 清除旧的timeout
|
|
163
|
-
if (notification.timeoutId) {
|
|
164
|
-
clearTimeout(notification.timeoutId);
|
|
165
|
-
}
|
|
166
|
-
notification.timeoutId = timeoutId;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
// 创建任务并加入任务池
|
|
170
|
-
const notification = {
|
|
171
|
-
id: notificationId,
|
|
172
|
-
params,
|
|
173
|
-
timeoutId: null, // 稍后设置
|
|
174
|
-
currentCount: 0,
|
|
175
|
-
maxCount,
|
|
176
|
-
startTime: Date.now()
|
|
177
|
-
};
|
|
178
|
-
repeatNotificationPool.set(notificationId, notification);
|
|
179
|
-
// 如果有初始延迟,先等待延迟再开始重复
|
|
180
|
-
if (params.delay !== undefined) {
|
|
181
|
-
const delayMs = parseTimeDelay(params.delay);
|
|
182
|
-
notification.timeoutId = setTimeout(() => {
|
|
183
|
-
// 发送第一次通知并开始重复
|
|
184
|
-
sendNotification(notificationParams).then(() => {
|
|
185
|
-
notification.currentCount = 1;
|
|
186
|
-
scheduleNextNotification(1);
|
|
187
|
-
}).catch(error => {
|
|
188
|
-
console.error('Initial repeated notification failed:', error);
|
|
189
|
-
scheduleNextNotification(1);
|
|
190
|
-
});
|
|
191
|
-
}, delayMs);
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
// 没有初始延迟,立即开始第一次通知
|
|
195
|
-
try {
|
|
196
|
-
await sendNotification(notificationParams);
|
|
197
|
-
notification.currentCount = 1;
|
|
198
|
-
scheduleNextNotification(1);
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
console.error('Initial repeated notification failed:', error);
|
|
202
|
-
scheduleNextNotification(1);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return {
|
|
206
|
-
notificationId,
|
|
207
|
-
message: `Repeat notification notification created with ID: ${notificationId}`
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
// 如果有 delay 参数但没有 repeat,使用 setTimeout 延迟发送
|
|
211
|
-
if (params.delay !== undefined) {
|
|
212
|
-
const delayMs = parseTimeDelay(params.delay);
|
|
213
|
-
if (delayMs <= 0) {
|
|
214
|
-
throw new Error('Delay must be a positive number');
|
|
215
|
-
}
|
|
216
|
-
try {
|
|
217
|
-
// 设置延迟任务
|
|
218
|
-
setTimeout(async () => {
|
|
219
|
-
try {
|
|
220
|
-
// 创建不包含 delay 的参数对象,避免无限递归
|
|
221
|
-
const { delay, ...notificationParams } = params;
|
|
222
|
-
await sendNotification(notificationParams);
|
|
223
|
-
}
|
|
224
|
-
catch (error) {
|
|
225
|
-
console.error('Delayed notification failed:', error);
|
|
226
|
-
}
|
|
227
|
-
}, delayMs);
|
|
228
|
-
// 立即返回设置成功
|
|
229
|
-
return { message: 'Delayed notification scheduled successfully' };
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
throw new Error('Failed to schedule delayed notification');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
// 立即发送通知的逻辑
|
|
236
|
-
try {
|
|
237
|
-
validateParams(params);
|
|
238
|
-
const os = getOS();
|
|
239
|
-
let command;
|
|
240
|
-
switch (os) {
|
|
241
|
-
case 'macos':
|
|
242
|
-
command = buildNotificationCommand(params);
|
|
243
|
-
break;
|
|
244
|
-
case 'windows':
|
|
245
|
-
command = buildWindowsNotificationCommand(params);
|
|
246
|
-
break;
|
|
247
|
-
case 'linux':
|
|
248
|
-
command = buildLinuxNotificationCommand(params);
|
|
249
|
-
break;
|
|
250
|
-
default:
|
|
251
|
-
throw new Error(`Unsupported platform: ${os}`);
|
|
252
|
-
}
|
|
253
|
-
await execAsync(command);
|
|
254
|
-
return { message: 'Notification sent successfully' };
|
|
255
|
-
}
|
|
256
|
-
catch (error) {
|
|
257
|
-
if (error instanceof Error) {
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
260
|
-
// Handle different types of system errors
|
|
261
|
-
const err = error;
|
|
262
|
-
if (err.message.includes('execution error')) {
|
|
263
|
-
throw new Error('Failed to execute notification command');
|
|
264
|
-
}
|
|
265
|
-
else if (err.message.includes('permission')) {
|
|
266
|
-
throw new Error('Permission denied when trying to send notification');
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
throw new Error(`Unexpected error: ${err.message}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
/**
|
|
274
|
-
* 停止指定的重复提醒任务
|
|
275
|
-
*/
|
|
276
|
-
function stopRepeatNotification(notificationId) {
|
|
277
|
-
const notification = repeatNotificationPool.get(notificationId);
|
|
278
|
-
if (!notification) {
|
|
279
|
-
return false;
|
|
280
|
-
}
|
|
281
|
-
// 清除定时器
|
|
282
|
-
if (notification.timeoutId) {
|
|
283
|
-
clearTimeout(notification.timeoutId);
|
|
284
|
-
}
|
|
285
|
-
// 从任务池中移除
|
|
286
|
-
repeatNotificationPool.delete(notificationId);
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* 停止所有重复提醒任务
|
|
291
|
-
*/
|
|
292
|
-
function stopAllRepeatNotifications() {
|
|
293
|
-
const count = repeatNotificationPool.size;
|
|
294
|
-
// 清除所有定时器
|
|
295
|
-
for (const notification of repeatNotificationPool.values()) {
|
|
296
|
-
if (notification.timeoutId) {
|
|
297
|
-
clearTimeout(notification.timeoutId);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
// 清空任务池
|
|
301
|
-
repeatNotificationPool.clear();
|
|
302
|
-
return count;
|
|
303
|
-
}
|
|
304
|
-
/**
|
|
305
|
-
* 获取所有活跃的重复提醒任务信息
|
|
306
|
-
*/
|
|
307
|
-
function getActiveRepeatNotifications() {
|
|
308
|
-
return Array.from(repeatNotificationPool.values()).map(notification => ({
|
|
309
|
-
...notification,
|
|
310
|
-
// 不返回timeoutId,避免序列化问题
|
|
311
|
-
timeoutId: null
|
|
312
|
-
}));
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* 获取指定任务的信息
|
|
316
|
-
*/
|
|
317
|
-
function getRepeatNotificationInfo(notificationId) {
|
|
318
|
-
const notification = repeatNotificationPool.get(notificationId);
|
|
319
|
-
if (!notification) {
|
|
320
|
-
return null;
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
...notification,
|
|
324
|
-
// 不返回timeoutId,避免序列化问题
|
|
325
|
-
timeoutId: null
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
class NotificationServer {
|
|
30
|
+
class ReminderServer {
|
|
329
31
|
constructor() {
|
|
330
|
-
this.server = new Server({
|
|
331
|
-
|
|
332
|
-
version: '1.0.0',
|
|
333
|
-
}, {
|
|
334
|
-
capabilities: {
|
|
335
|
-
tools: {},
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
this.setupToolHandlers();
|
|
339
|
-
// Error handling
|
|
32
|
+
this.server = new Server({ name: 'notification-mcp', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
33
|
+
this.setupHandlers();
|
|
340
34
|
this.server.onerror = (error) => console.error('[MCP Error]', error);
|
|
341
35
|
process.on('SIGINT', async () => {
|
|
342
36
|
await this.server.close();
|
|
343
37
|
process.exit(0);
|
|
344
38
|
});
|
|
345
39
|
}
|
|
346
|
-
|
|
347
|
-
// List available tools
|
|
40
|
+
setupHandlers() {
|
|
348
41
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
349
42
|
tools: [
|
|
350
43
|
{
|
|
351
|
-
name: '
|
|
352
|
-
description: '
|
|
44
|
+
name: 'set_reminder',
|
|
45
|
+
description: '设置通知提醒。支持一次性、按间隔循环、每日循环。',
|
|
353
46
|
inputSchema: {
|
|
354
47
|
type: 'object',
|
|
355
48
|
properties: {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
},
|
|
364
|
-
subtitle: {
|
|
365
|
-
type: 'string',
|
|
366
|
-
description: '可选的副标题',
|
|
367
|
-
},
|
|
368
|
-
sound: {
|
|
369
|
-
type: 'boolean',
|
|
370
|
-
description: '是否播放默认提示音',
|
|
371
|
-
default: true,
|
|
372
|
-
},
|
|
373
|
-
delay: {
|
|
374
|
-
oneOf: [
|
|
375
|
-
{ type: 'number' },
|
|
376
|
-
{ type: 'string' }
|
|
377
|
-
],
|
|
378
|
-
description: '延迟发送通知或提醒(毫秒或时间字符串如"10s", "1m", "1h")',
|
|
379
|
-
},
|
|
380
|
-
repeat: {
|
|
381
|
-
oneOf: [
|
|
382
|
-
{ type: 'number' },
|
|
383
|
-
{ type: 'string' }
|
|
384
|
-
],
|
|
385
|
-
description: '重复通知或提醒的间隔(毫秒或时间字符串如"10s", "1m", "1h")',
|
|
386
|
-
},
|
|
387
|
-
repeatCount: {
|
|
388
|
-
type: 'number',
|
|
389
|
-
description: '重复次数(可选,如果设置了repeat但未设置此项则无限重复)',
|
|
390
|
-
minimum: 1,
|
|
391
|
-
},
|
|
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' },
|
|
392
56
|
},
|
|
393
|
-
required: ['
|
|
57
|
+
required: ['content', 'repeat'],
|
|
394
58
|
additionalProperties: false,
|
|
395
59
|
},
|
|
396
60
|
},
|
|
397
61
|
{
|
|
398
|
-
name: '
|
|
399
|
-
description: '
|
|
62
|
+
name: 'list_reminders',
|
|
63
|
+
description: '获取设备的提醒列表(仅未触发的 scheduled)。',
|
|
400
64
|
inputSchema: {
|
|
401
65
|
type: 'object',
|
|
402
|
-
properties: {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
},
|
|
413
|
-
required: ['
|
|
414
|
-
additionalProperties: false
|
|
415
|
-
}
|
|
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
|
+
},
|
|
416
80
|
},
|
|
417
81
|
],
|
|
418
82
|
}));
|
|
419
|
-
// Handle tool execution
|
|
420
83
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
421
84
|
try {
|
|
422
85
|
if (!request.params.arguments || typeof request.params.arguments !== 'object') {
|
|
423
|
-
throw new McpError(ErrorCode.InvalidParams, '
|
|
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)');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
console.error(`接收到 chatSessionId: ${chatSessionId}`);
|
|
424
98
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
repeatCount: typeof repeatCount === 'number' ? repeatCount : undefined
|
|
439
|
-
};
|
|
440
|
-
const result = await sendNotification(params);
|
|
441
|
-
return {
|
|
442
|
-
content: [
|
|
443
|
-
{
|
|
444
|
-
type: 'text',
|
|
445
|
-
text: result.notificationId ?
|
|
446
|
-
`${result.message}. Task ID: ${result.notificationId}` :
|
|
447
|
-
result.message,
|
|
448
|
-
},
|
|
449
|
-
],
|
|
450
|
-
};
|
|
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 };
|
|
451
112
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
case 'get_active_repeat_tasks': {
|
|
478
|
-
const tasks = getActiveRepeatNotifications();
|
|
479
|
-
return {
|
|
480
|
-
content: [
|
|
481
|
-
{
|
|
482
|
-
type: 'text',
|
|
483
|
-
text: JSON.stringify(tasks, null, 2),
|
|
484
|
-
},
|
|
485
|
-
],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
case 'get_repeat_task_info': {
|
|
489
|
-
const info = getRepeatNotificationInfo(taskId);
|
|
490
|
-
return {
|
|
491
|
-
content: [
|
|
492
|
-
{
|
|
493
|
-
type: 'text',
|
|
494
|
-
text: info ? JSON.stringify(info, null, 2) : `任务 ${taskId} 未找到`,
|
|
495
|
-
},
|
|
496
|
-
],
|
|
497
|
-
};
|
|
498
|
-
}
|
|
499
|
-
default:
|
|
500
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown task management action: ${action}`);
|
|
501
|
-
}
|
|
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 };
|
|
502
138
|
}
|
|
503
|
-
|
|
504
|
-
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
139
|
+
return { content: [{ type: 'text', text: resp.msg || '取消成功' }], isError: false };
|
|
505
140
|
}
|
|
141
|
+
throw new McpError(ErrorCode.MethodNotFound, `未知工具: ${name}`);
|
|
506
142
|
}
|
|
507
143
|
catch (error) {
|
|
508
|
-
|
|
144
|
+
if (error instanceof McpError)
|
|
145
|
+
throw error;
|
|
146
|
+
throw new McpError(ErrorCode.InternalError, `执行失败: ${error.message}`);
|
|
509
147
|
}
|
|
510
148
|
});
|
|
511
149
|
}
|
|
512
150
|
async run() {
|
|
513
151
|
const transport = new StdioServerTransport();
|
|
514
152
|
await this.server.connect(transport);
|
|
515
|
-
console.error('
|
|
153
|
+
console.error('Reminder MCP server running on stdio');
|
|
516
154
|
}
|
|
517
155
|
}
|
|
518
|
-
const server = new
|
|
156
|
+
const server = new ReminderServer();
|
|
519
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
|
-
"description": "系统通知MCP服务器",
|
|
5
|
-
"packageManager": "
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
+
}
|