roost-mcp 0.0.0-stage → 0.1.0

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 (4) hide show
  1. package/LICENSE +213 -0
  2. package/README.md +105 -2
  3. package/index.js +268 -0
  4. package/package.json +34 -4
package/LICENSE ADDED
@@ -0,0 +1,213 @@
1
+ qq-bot-roost(栖 Roost)免费非商业许可协议 v2.0
2
+
3
+ Copyright (c) 2026 tdcq9594
4
+
5
+ 本协议的结构参考 PolyForm Noncommercial License 1.0.0
6
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>,
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
+ 1. 软件本身的售价,或下载费、激活费、注册费;
43
+ 2. 授权费、许可费、订阅费、会员费;
44
+ 3. 以「技术支持」「培训」「咨询」「定制开发」等名义收取的费用;
45
+ 4. 以「捐赠」「赞助」「打赏」等名义设置的付费门槛——
46
+ 自愿且无附加条件的捐赠不属于收费,但不得将任何付费行为作为
47
+ 获取或使用本软件的前提;
48
+ 5. 通过捆绑销售、搭售或其他方式变相就本软件收费。
49
+
50
+ 简言之:这个软件必须免费,任何人都不能靠它收钱。
51
+
52
+ ================================================================
53
+ 四、许可授予
54
+ ================================================================
55
+
56
+ **版权许可**:许可人授予你一项版权许可,允许你就本软件做一切
57
+ 在不获许可时会侵犯其版权的行为,但仅限于本协议允许的用途。
58
+ 你只能按第五条分发本软件,只能按第六条修改或创作新作品。
59
+
60
+ **分发许可**:许可人额外授予你分发本软件副本的版权许可。
61
+ 你的分发许可涵盖分发经第六条允许的修改版和新作品。
62
+
63
+ **专利许可**:许可人授予你一项专利许可,涵盖许可人能够许可、
64
+ 或将来能够许可的、你会因使用本软件而侵犯的专利权利要求。
65
+
66
+ ================================================================
67
+ 五、允许的用途
68
+ ================================================================
69
+
70
+ 以下用途均为本协议允许的用途:
71
+
72
+ **非商业目的**:任何非商业性的目的。
73
+
74
+ **个人使用**:为公共利益的知识增进、个人学习、私人娱乐、业余爱好、
75
+ 个人研究、试验与测试而使用,且**不带有任何预期的商业应用**。
76
+
77
+ **非营利组织**:慈善组织、教育机构、公共研究机构、公共安全或公共卫生机构、
78
+ 环境保护组织、政府机构的使用,无论其资金来源如何、或资金附带何种义务。
79
+
80
+ **合理使用**:你依法律可能享有的「合理使用」(fair use)权利不受本协议限制。
81
+
82
+ ================================================================
83
+ 六、修改与新作品
84
+ ================================================================
85
+
86
+ 许可人额外授予你一项版权许可,允许你为本协议允许的任何用途
87
+ 修改本软件、并基于本软件创作新作品。
88
+
89
+ 分发修改版或新作品时,必须:
90
+
91
+ 1. 保留声明:完整保留本版权声明与本协议,不得删除或修改;
92
+ 2. 沿用本协议:修改版和衍生作品必须继续适用本协议,不得改用更宽松的许可;
93
+ 3. 标明修改:明确说明你对原作品做了哪些修改;
94
+ 4. 不得背书:不得暗示许可人为你的产品或修改版提供担保或背书;
95
+ 5. 保持免费:不得就本软件收取任何费用(见第三条)。
96
+
97
+ ================================================================
98
+ 七、禁止商业使用
99
+ ================================================================
100
+
101
+ 不得将本软件(含原版、修改版及衍生作品)用于任何商业目的。
102
+
103
+ 「商业目的」指以商业优势或金钱报酬为目的的任何用途,包括但不限于:
104
+
105
+ 1. 出售、出租、许可本软件;
106
+ 2. 将本软件作为收费服务、收费产品或收费功能的一部分提供;
107
+ 3. 将本软件集成进以营利为目的的产品、系统或服务;
108
+ 4. 通过本软件直接或间接获取广告收入、佣金、订阅费或其他商业收益;
109
+ 5. 在你的组织内部用于其主营业务或经营活动。
110
+
111
+ 企业内部出于评估、测试目的的非生产性使用,不视为商业使用。
112
+
113
+ ================================================================
114
+ 八、不提供商业授权
115
+ ================================================================
116
+
117
+ 本软件**不提供任何形式的商业授权**。
118
+
119
+ 鉴于本协议禁止就本软件收取任何费用,也不允许任何商业使用,
120
+ 不存在通过付费取得商业使用权的途径。任何声称代表本项目出售授权、
121
+ 提供付费版本或收取商业许可费用的行为,均与许可人无关。
122
+
123
+ ================================================================
124
+ 九、通知义务
125
+ ================================================================
126
+
127
+ 你必须确保:任何从你处获得本软件任何部分副本的人,
128
+ 同时获得本协议的一份副本(或本协议的网址),
129
+ 以及许可人随本软件提供的、以「Required Notice:」开头的任何纯文本行。
130
+
131
+ 例如:
132
+
133
+ > Required Notice: Copyright (c) 2026 tdcq9594
134
+
135
+ ================================================================
136
+ 十、专利防御
137
+ ================================================================
138
+
139
+ 如果你以书面形式主张本软件侵犯或协助侵犯任何专利,
140
+ 你依本协议获得的专利许可立即终止。如果你的组织提出此类主张,
141
+ 你为你的组织所做工作的专利许可立即终止。
142
+
143
+ ================================================================
144
+ 十一、无其他权利
145
+ ================================================================
146
+
147
+ 本协议不允许你再许可或转让你的任何许可给他人,
148
+ 也不阻止许可人向他人授予许可。本协议不暗示任何其他许可。
149
+
150
+ ================================================================
151
+ 十二、违约与补救
152
+ ================================================================
153
+
154
+ 你首次收到书面通知,指出你违反了本协议任何条款、
155
+ 或对本软件做了你的许可未涵盖的行为时,
156
+ 只要你在收到通知后 **32 天内**完全遵守本协议并采取实际步骤纠正此前的违约行为,
157
+ 你的许可仍可继续有效。
158
+
159
+ 逾期未补救的,你的全部许可立即终止。
160
+
161
+ 许可终止后,你必须停止使用本软件,并销毁你所持有的全部副本(含修改版)。
162
+ 终止不影响许可人就此前发生的违约行为追究责任。
163
+
164
+ ================================================================
165
+ 十三、免责
166
+ ================================================================
167
+
168
+ 在法律允许的最大范围内,本软件按「现状」提供,不附带任何明示或默示的
169
+ 担保或条件,包括但不限于对适销性、特定用途适用性、所有权和不侵权的担保。
170
+
171
+ 许可人不对因本协议、或因使用本软件及本软件的性质而产生的任何损害向你负责,
172
+ 无论基于何种法律主张,也无论是否已被告知此类损害的可能性。
173
+
174
+ ================================================================
175
+ 十四、附则
176
+ ================================================================
177
+
178
+ 本协议构成双方就本软件达成的完整约定。本协议任何条款被认定无效或
179
+ 不可执行时,不影响其余条款的效力。
180
+
181
+ ----------------------------------------------------------------
182
+ English Summary (for reference only; the Chinese text above prevails)
183
+
184
+ Copyright (c) 2026 tdcq9594. All rights reserved.
185
+
186
+ This license follows the structure of the PolyForm Noncommercial License 1.0.0
187
+ and adds a stricter no-fee requirement.
188
+
189
+ The software is provided FREE OF CHARGE, PERMANENTLY. No one may charge any
190
+ fee for it, in any form — sale price, download fee, activation fee, license
191
+ fee, subscription, membership, paid support, training, customization, or any
192
+ paid prerequisite for obtaining or using the software. Voluntary donations
193
+ with no conditions attached are not a fee, but no payment may ever be required
194
+ as a precondition for access or use.
195
+
196
+ You may use, copy, modify and distribute the software for any permitted
197
+ purpose. Permitted purposes include any noncommercial purpose, personal use
198
+ for research, experiment, testing for the benefit of public knowledge,
199
+ personal study, private entertainment, hobby projects, amateur pursuits or
200
+ religious observance without any anticipated commercial application, and use
201
+ by charitable, educational, public research, public safety or health,
202
+ environmental protection, or government organizations.
203
+
204
+ COMMERCIAL USE OF ANY KIND IS PROHIBITED. No commercial license is offered.
205
+
206
+ You must pass these terms along with any copy, keep notices intact, disclose
207
+ changes, distribute derivative works under the same terms, and imply no
208
+ endorsement. If you first receive written notice of a violation, your licenses
209
+ continue if you come into full compliance within 32 days.
210
+
211
+ THE SOFTWARE COMES AS IS, WITHOUT ANY WARRANTY OR CONDITION, AND THE LICENSOR
212
+ WILL NOT BE LIABLE FOR ANY DAMAGES ARISING OUT OF THESE TERMS OR THE USE OR
213
+ NATURE OF THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,106 @@
1
- # Temporary Holding Version
1
+ # roost-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把 [Roost](https://github.com/tdcq9594/qq-bot-roost) 的 QQ 能力做成 **MCP 工具**,
4
+ 让 Claude Desktop / Cursor / Cline / Continue / Windsurf 这些工作台能直接发 QQ 消息。
5
+
6
+ 方向跟 `ai-chat` 插件相反:
7
+
8
+ | | 谁当大脑 | 干什么 |
9
+ |---|---|---|
10
+ | `ai-chat` 插件 | 工作台 | QQ 消息进来 → 工作台想 → 插件发出去 |
11
+ | `roost-mcp` | 工作台 | 在工作台里说「帮我在群里发一句」→ 它调 QQ |
12
+
13
+ 两个可以同时用:`ai-chat` 负责「AI 住在群里」,`roost-mcp` 负责「你顺手让它发一句」。
14
+
15
+ ---
16
+
17
+ ## 前置
18
+
19
+ Roost 要跑着,并且 ≥ 0.8.0(`POST /api/qq/call` 是这个版本加的)。
20
+
21
+ ---
22
+
23
+ ## 装
24
+
25
+ ### Claude Desktop
26
+
27
+ `claude_desktop_config.json`:
28
+
29
+ ```jsonc
30
+ {
31
+ "mcpServers": {
32
+ "qq": {
33
+ "command": "npx",
34
+ "args": ["-y", "roost-mcp"],
35
+ "env": {
36
+ "ROOST_URL": "http://127.0.0.1:8080",
37
+ "ROOST_KEY": "面板访问密钥"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Cursor / Cline / 其他支持 MCP 的
45
+
46
+ 同样三件事:命令 `npx -y roost-mcp`、环境变量 `ROOST_URL` / `ROOST_KEY`。
47
+ 具体写在哪个配置文件里各家不同,看各自文档。
48
+
49
+ ### 本地调试
50
+
51
+ ```bash
52
+ ROOST_URL=http://127.0.0.1:8080 ROOST_KEY=xxx node index.js
53
+ # 然后在 stdin 里敲一行 JSON-RPC:
54
+ {"jsonrpc":"2.0","id":1,"method":"tools/list"}
55
+ ```
56
+
57
+ ---
58
+
59
+ ## 环境变量
60
+
61
+ | 变量 | 默认 | 说明 |
62
+ |---|---|---|
63
+ | `ROOST_URL` | `http://127.0.0.1:8080` | Roost 面板地址 |
64
+ | `ROOST_KEY` | 空 | 面板访问密钥(`webui.accessKey`)。只监听本机时可以留空 |
65
+ | `ROOST_BOT` | 空 | 多机器人时只操作这一个;留空 = 默认机器人 |
66
+
67
+ ---
68
+
69
+ ## 工具
70
+
71
+ | 工具 | 干什么 |
72
+ |---|---|
73
+ | `qq_send` | 发文本 / Markdown / 图片到群或私聊 |
74
+ | `qq_recall` | 撤回机器人自己发的消息 |
75
+ | `qq_mute` | 群成员禁言 / 解除(需管理员) |
76
+ | `qq_group_info` | 查群信息 |
77
+ | `qq_capabilities` | 列出 Roost 封装了哪些官方接口(AI 不知道能调什么时先看这个) |
78
+ | `qq_api` | 调其余任意官方接口,兜底 |
79
+
80
+ `qq_api` 的参数约定:`__target` 是第一个位置参数,`__rest` 是后续位置参数,
81
+ 其余键合成一个对象追加在最后。危险操作(踢人、黑名单、审批策略)要 `confirm: true`。
82
+
83
+ ---
84
+
85
+ ## 设计说明
86
+
87
+ **为什么不给 47 个接口各做一个工具**:AI 选不过来,也没人维护。
88
+ 六个工具 + 一个能力清单,剩下的靠 `qq_api` 兜底。官方加接口时,
89
+ Roost 那边在 `endpoints.js` 加一行,这里零改动。
90
+
91
+ **为什么日志走 stderr**:MCP 的 stdio 传输里 stdout 是协议通道,
92
+ 混一行日志客户端就解析失败。这是这类 server 最常见的坑。
93
+
94
+ **为什么工具失败返回 `isError` 而不是抛异常**:让模型看到失败原因
95
+ (比如「被动回复超限」),它才有机会换个做法;进程崩了就只能等用户重启。
96
+
97
+ ---
98
+
99
+ ## 测试
100
+
101
+ ```bash
102
+ node test/mcp.test.mjs
103
+ ```
104
+
105
+ 起一个假 Roost,spawn 真的 MCP server,按协议发 JSON-RPC,验证:
106
+ 协议形状、stdout 里只有 JSON-RPC、工具失败走 `isError`、未知方法返回正确错误码。
package/index.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * roost-mcp —— 把 Roost 的 QQ 能力暴露成 MCP 工具。
4
+ *
5
+ * 方向跟 ai-chat 插件相反:
6
+ * ai-chat 「工作台当大脑」——QQ 消息进来,工作台想,插件发出去
7
+ * roost-mcp 「工作台当手脚」——在 Claude Desktop / Cursor 里说
8
+ * 「帮我在群里发一句」,它调这个 MCP server
9
+ *
10
+ * 走的是 Roost 内核的通用能力通道(`POST /api/qq/call`,v0.8.0 起):
11
+ * 一个端点覆盖官方 47 个接口,白名单和风险分级在 Roost 那边,这里只负责转发。
12
+ *
13
+ * 零依赖:只用 node 内置的 fetch 和 stdio。
14
+ *
15
+ * 用法(Claude Desktop 的 claude_desktop_config.json):
16
+ * { "mcpServers": { "qq": {
17
+ * "command": "npx", "args": ["-y", "roost-mcp"],
18
+ * "env": { "ROOST_URL": "http://127.0.0.1:8080", "ROOST_KEY": "面板密钥" }
19
+ * } } }
20
+ */
21
+
22
+ import { createInterface } from 'node:readline';
23
+
24
+ const BASE = String(process.env.ROOST_URL || 'http://127.0.0.1:8080').replace(/\/+$/, '');
25
+ const KEY = String(process.env.ROOST_KEY || '');
26
+ const BOT = String(process.env.ROOST_BOT || '');
27
+
28
+ /** 日志必须走 stderr——stdout 是协议通道,混一个字节都会让客户端解析失败 */
29
+ const log = (...a) => process.stderr.write(`[roost-mcp] ${a.join(' ')}\n`);
30
+
31
+ /* ---------------- 调 Roost ---------------- */
32
+
33
+ function qs() {
34
+ return KEY ? `?key=${encodeURIComponent(KEY)}` : '';
35
+ }
36
+
37
+ async function call(method, args = {}, confirm = false) {
38
+ const body = { method, args };
39
+ if (BOT) body.bot = BOT;
40
+ if (confirm) body.confirm = true;
41
+
42
+ const res = await fetch(`${BASE}/api/qq/call${qs()}`, {
43
+ method: 'POST',
44
+ headers: { 'content-type': 'application/json' },
45
+ body: JSON.stringify(body),
46
+ signal: AbortSignal.timeout(20000),
47
+ });
48
+
49
+ const text = await res.text();
50
+ let j = null;
51
+ try {
52
+ j = text ? JSON.parse(text) : null;
53
+ } catch {
54
+ throw new Error(`Roost 返回了非 JSON(HTTP ${res.status}):${text.slice(0, 120)}`);
55
+ }
56
+ if (!res.ok || j?.ok === false) {
57
+ throw new Error(j?.error || `调用 ${method} 失败(HTTP ${res.status})`);
58
+ }
59
+ return j?.result ?? j;
60
+ }
61
+
62
+ /* ---------------- 工具定义 ---------------- */
63
+
64
+ const TOOLS = [
65
+ {
66
+ name: 'qq_send',
67
+ description:
68
+ '在 QQ 里发一条消息。可以发纯文本、Markdown,或者一张图片(给 URL)。'
69
+ + '群聊填 scene=group + 群的 openid;私聊填 scene=c2c + 用户 openid。',
70
+ inputSchema: {
71
+ type: 'object',
72
+ properties: {
73
+ scene: { type: 'string', enum: ['group', 'c2c'], description: '群聊还是私聊' },
74
+ target: { type: 'string', description: '群 openid 或用户 openid' },
75
+ text: { type: 'string', description: '纯文本内容' },
76
+ markdown: { type: 'string', description: 'Markdown 内容,和 text 二选一' },
77
+ imageUrl: { type: 'string', description: '图片 URL,和 text/markdown 三选一' },
78
+ },
79
+ required: ['scene', 'target'],
80
+ },
81
+ async run(a) {
82
+ const method = a.scene === 'c2c' ? 'sendC2C' : 'sendGroup';
83
+
84
+ if (a.imageUrl) {
85
+ // uploadMedia(scene, target, opts) —— 三个位置参数
86
+ const up = await call('uploadMedia', {
87
+ __target: a.scene,
88
+ __rest: [a.target, { fileType: 1, url: a.imageUrl }],
89
+ });
90
+ if (!up?.file_info) throw new Error('图片上传没拿到 file_info');
91
+ return call(method, {
92
+ __target: a.target,
93
+ msgType: 7,
94
+ content: ' ',
95
+ media: { file_info: up.file_info },
96
+ });
97
+ }
98
+ if (a.markdown) {
99
+ return call(method, { __target: a.target, markdown: { content: String(a.markdown) } });
100
+ }
101
+ if (!a.text) throw new Error('text / markdown / imageUrl 至少给一个');
102
+ return call(method, { __target: a.target, content: String(a.text) });
103
+ },
104
+ },
105
+
106
+ {
107
+ name: 'qq_recall',
108
+ description: '撤回一条机器人自己发的消息。',
109
+ inputSchema: {
110
+ type: 'object',
111
+ properties: {
112
+ scene: { type: 'string', enum: ['group', 'c2c'] },
113
+ target: { type: 'string', description: '群 openid 或用户 openid' },
114
+ messageId: { type: 'string', description: '要撤回的消息 id' },
115
+ },
116
+ required: ['scene', 'target', 'messageId'],
117
+ },
118
+ run: (a) =>
119
+ call(a.scene === 'c2c' ? 'retractC2CMessage' : 'retractGroupMessage', {
120
+ __target: a.target,
121
+ __rest: [a.messageId],
122
+ }),
123
+ },
124
+
125
+ {
126
+ name: 'qq_mute',
127
+ description: '群成员禁言 / 解除禁言。需要机器人是群管理员。',
128
+ inputSchema: {
129
+ type: 'object',
130
+ properties: {
131
+ groupId: { type: 'string', description: '群 openid' },
132
+ memberOpenid: { type: 'string', description: '成员的 openid' },
133
+ durationSec: { type: 'number', description: '禁言秒数,0 表示解除' },
134
+ },
135
+ required: ['groupId', 'memberOpenid', 'durationSec'],
136
+ },
137
+ run: (a) =>
138
+ call('muteMembers', {
139
+ __target: a.groupId,
140
+ __rest: [[{ memberOpenid: a.memberOpenid, durationSec: a.durationSec }]],
141
+ }),
142
+ },
143
+
144
+ {
145
+ name: 'qq_group_info',
146
+ description: '查群基本信息(群名、人数等)。',
147
+ inputSchema: {
148
+ type: 'object',
149
+ properties: { groupId: { type: 'string', description: '群 openid' } },
150
+ required: ['groupId'],
151
+ },
152
+ run: (a) => call('getGroupInfo', { __target: a.groupId }),
153
+ },
154
+
155
+ {
156
+ name: 'qq_capabilities',
157
+ description:
158
+ '列出 Roost 封装了哪些官方接口(方法名、风险等级、限制)。'
159
+ + '不知道有什么能调的时候先看这个。',
160
+ inputSchema: { type: 'object', properties: {} },
161
+ async run() {
162
+ const res = await fetch(`${BASE}/api/qq/capabilities${qs()}`, { signal: AbortSignal.timeout(10000) });
163
+ if (!res.ok) throw new Error(`取能力清单失败(HTTP ${res.status})`);
164
+ const list = await res.json();
165
+ return { count: Array.isArray(list) ? list.length : 0, endpoints: list };
166
+ },
167
+ },
168
+
169
+ {
170
+ name: 'qq_api',
171
+ description:
172
+ '直接调 Roost 封装的其他官方接口。参数用 __target 传第一个位置参数,'
173
+ + '__rest 传后续位置参数,其余键会合成一个对象追加在最后。'
174
+ + '危险操作(踢人、黑名单、审批策略)需要 confirm=true。',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {
178
+ method: { type: 'string', description: '方法名,如 createPanel / getGroupMembers' },
179
+ args: { type: 'object', description: '参数对象' },
180
+ confirm: { type: 'boolean', description: '危险操作确认' },
181
+ },
182
+ required: ['method'],
183
+ },
184
+ run: (a) => call(a.method, a.args ?? {}, a.confirm === true),
185
+ },
186
+ ];
187
+
188
+ /* ---------------- MCP 协议 ---------------- */
189
+
190
+ const PROTOCOL = '2024-11-05';
191
+
192
+ function reply(id, result) {
193
+ send({ jsonrpc: '2.0', id, result });
194
+ }
195
+
196
+ function replyError(id, code, message) {
197
+ send({ jsonrpc: '2.0', id, error: { code, message } });
198
+ }
199
+
200
+ function send(obj) {
201
+ process.stdout.write(JSON.stringify(obj) + '\n');
202
+ }
203
+
204
+ async function handle(msg) {
205
+ const { id, method, params } = msg;
206
+
207
+ // 通知类没有 id,不用回
208
+ if (id === undefined || id === null) {
209
+ if (method === 'notifications/initialized') log('客户端已初始化');
210
+ return;
211
+ }
212
+
213
+ try {
214
+ switch (method) {
215
+ case 'initialize':
216
+ return reply(id, {
217
+ protocolVersion: PROTOCOL,
218
+ capabilities: { tools: {} },
219
+ serverInfo: { name: 'roost-mcp', version: '0.1.0' },
220
+ });
221
+
222
+ case 'ping':
223
+ return reply(id, {});
224
+
225
+ case 'tools/list':
226
+ return reply(id, {
227
+ tools: TOOLS.map((t) => ({ name: t.name, description: t.description, inputSchema: t.inputSchema })),
228
+ });
229
+
230
+ case 'tools/call': {
231
+ const name = params?.name;
232
+ const tool = TOOLS.find((t) => t.name === name);
233
+ if (!tool) return replyError(id, -32602, `没有这个工具:${name}`);
234
+ try {
235
+ const out = await tool.run(params?.arguments ?? {});
236
+ return reply(id, { content: [{ type: 'text', text: JSON.stringify(out ?? {}, null, 2) }] });
237
+ } catch (e) {
238
+ // 工具执行失败按协议用 isError 返回,让模型看到原因而不是崩掉
239
+ return reply(id, { content: [{ type: 'text', text: `出错了:${e.message}` }], isError: true });
240
+ }
241
+ }
242
+
243
+ default:
244
+ return replyError(id, -32601, `不认识的方法:${method}`);
245
+ }
246
+ } catch (e) {
247
+ log('处理失败:', e?.message ?? e);
248
+ return replyError(id, -32603, e?.message ?? '内部错误');
249
+ }
250
+ }
251
+
252
+ const rl = createInterface({ input: process.stdin });
253
+ rl.on('line', (line) => {
254
+ const t = line.trim();
255
+ if (!t) return;
256
+ let msg;
257
+ try {
258
+ msg = JSON.parse(t);
259
+ } catch {
260
+ log('收到非 JSON 的一行,忽略');
261
+ return;
262
+ }
263
+ Promise.resolve(handle(msg)).catch((e) => log('未捕获:', e?.message ?? e));
264
+ });
265
+
266
+ rl.on('close', () => process.exit(0));
267
+
268
+ log(`就绪:${BASE}${BOT ? `(只操作 ${BOT})` : ''}`);
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "roost-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "MCP server:把 Roost 的 QQ 能力暴露给 Claude Desktop / Cursor / 任何支持 MCP 的工作台",
5
+ "type": "module",
6
+ "bin": {
7
+ "roost-mcp": "index.js"
8
+ },
9
+ "main": "index.js",
10
+ "files": [
11
+ "index.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "test": "node test/mcp.test.mjs"
17
+ },
18
+ "keywords": [
19
+ "mcp",
20
+ "model-context-protocol",
21
+ "qq",
22
+ "roost"
23
+ ],
24
+ "license": "SEE LICENSE IN LICENSE",
25
+ "engines": {
26
+ "node": ">=22"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/tdcq9594/qq-bot-roost-plugins.git",
31
+ "directory": "packages/roost-mcp"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ }
36
+ }