@ppdocs/mcp 3.1.8 → 3.1.10

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.
@@ -1,637 +1,16 @@
1
- import { z } from 'zod';
2
- import * as storage from '../storage/httpClient.js';
3
- import { decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
4
- import { wrap, formatDocMarkdown, formatTreeText, countTreeDocs, formatFileSize } from './helpers.js';
5
- export function registerTools(server, projectId, _user) {
6
- // ===================== 跨项目访问 =====================
7
- // 0. 列出所有可访问的项目
8
- server.tool('kg_list_projects', '列出所有可访问的项目(返回name和id)。跨项目操作的第一步:先调用此工具获取项目ID,再作为其他工具的targetProject参数', {}, async () => {
9
- try {
10
- const projects = await storage.crossListProjects();
11
- if (projects.length === 0) {
12
- return wrap('暂无可访问的项目');
13
- }
14
- const lines = projects.map(p => {
15
- const isCurrent = p.id === projectId ? ' ★当前' : '';
16
- return `- ${p.name} (${p.id})${isCurrent}`;
17
- });
18
- return wrap(`可访问的项目 (${projects.length} 个):\n\n${lines.join('\n')}`);
19
- }
20
- catch (e) {
21
- return wrap(`获取项目列表失败: ${String(e)}`);
22
- }
23
- });
24
- // 0.1 创建项目
25
- server.tool('kg_create_project', '创建新项目。返回项目ID和密码(密码用于MCP连接)', {
26
- id: z.string().describe('项目ID(英文/数字/中文,如"my-app")'),
27
- name: z.string().describe('项目名称'),
28
- description: z.string().optional().describe('项目简介'),
29
- projectPath: z.string().optional().describe('项目源码路径(本地绝对路径)'),
30
- }, async (args) => {
31
- try {
32
- const result = await storage.createProject(args.id, args.name, args.description, args.projectPath || process.cwd());
33
- return wrap(`✅ 项目创建成功\n\n- 项目ID: ${result.project.id}\n- 项目名: ${result.project.name}\n- 密码: ${result.password}\n\nMCP 连接地址: http://<服务器IP>:20001/api/${result.project.id}/${result.password}`);
34
- }
35
- catch (e) {
36
- return wrap(`❌ ${String(e)}`);
37
- }
38
- });
39
- // ===================== 文档管理 =====================
40
- // 1. 创建文档
41
- server.tool('kg_create_node', '创建知识文档。使用目录路径分类,文件名作为文档名', {
42
- path: z.string().min(1).describe('完整文档路径(如"/前端/组件/Modal")'),
43
- summary: z.string().optional().describe('一句话简介'),
44
- content: z.string().describe('Markdown内容'),
45
- status: z.string().optional().describe('文档状态(默认"已完成")')
46
- }, async (args) => {
47
- const decoded = decodeObjectStrings(args);
48
- const doc = await storage.createDoc(projectId, decoded.path, {
49
- summary: decoded.summary || '',
50
- content: decoded.content,
51
- versions: [{
52
- version: 0.1,
53
- date: new Date().toISOString(),
54
- changes: '初始创建'
55
- }],
56
- bugfixes: [],
57
- status: decoded.status || '已完成'
58
- });
59
- return wrap(`✅ 文档已创建: ${decoded.path}\n\n${JSON.stringify(doc, null, 2)}`);
60
- });
61
- // 2. 删除文档或目录 (支持批量)
62
- server.tool('kg_delete_node', '删除文档或目录(支持批量,根不可删除,目录会递归删除)', { nodeId: z.union([z.string(), z.array(z.string())]).describe('文档路径或路径数组') }, async (args) => {
63
- const paths = Array.isArray(args.nodeId) ? args.nodeId : [args.nodeId];
64
- const results = [];
65
- for (const path of paths) {
66
- const success = await storage.deleteDoc(projectId, path);
67
- results.push({ path, success });
68
- }
69
- const successCount = results.filter(r => r.success).length;
70
- const failedPaths = results.filter(r => !r.success).map(r => r.path);
71
- if (paths.length === 1) {
72
- return wrap(results[0].success ? '删除成功' : '删除失败(文档不存在或是根文档)');
73
- }
74
- let msg = `✅ 批量删除完成: ${successCount}/${paths.length} 成功`;
75
- if (failedPaths.length > 0) {
76
- msg += `\n❌ 失败: ${failedPaths.join(', ')}`;
77
- }
78
- return wrap(msg);
79
- });
80
- // 3. 更新文档 (支持单个/批量两种模式)
81
- server.tool('kg_update_node', '更新文档内容。【单个模式】nodeId+字段;【批量模式】仅传updates数组。两种模式二选一', {
82
- nodeId: z.string().optional().describe('【单个模式】文档路径,如"/前端/组件/Button"'),
83
- summary: z.string().optional().describe('【单个模式】一句话简介'),
84
- content: z.string().optional().describe('【单个模式】Markdown内容'),
85
- status: z.string().optional().describe('【单个模式】文档状态'),
86
- versions: z.array(z.object({
87
- version: z.number(),
88
- date: z.string(),
89
- changes: z.string()
90
- })).optional().describe('【单个模式】版本记录数组'),
91
- bugfixes: z.array(z.object({
92
- date: z.string(),
93
- issue: z.string(),
94
- solution: z.string()
95
- })).optional().describe('【单个模式】修复记录数组'),
96
- updates: z.array(z.object({
97
- nodeId: z.string().describe('文档路径'),
98
- summary: z.string().optional(),
99
- content: z.string().optional(),
100
- status: z.string().optional(),
101
- versions: z.array(z.object({
102
- version: z.number(),
103
- date: z.string(),
104
- changes: z.string()
105
- })).optional(),
106
- bugfixes: z.array(z.object({
107
- date: z.string(),
108
- issue: z.string(),
109
- solution: z.string()
110
- })).optional()
111
- })).optional().describe('【批量模式】更新数组,每项包含nodeId和要更新的字段')
112
- }, async (args) => {
113
- const decoded = decodeObjectStrings(args);
114
- // 批量更新模式: 使用 updates 数组
115
- if (decoded.updates && Array.isArray(decoded.updates)) {
116
- const results = [];
117
- for (const item of decoded.updates) {
118
- if (item.nodeId === '/' || item.nodeId === '_root') {
119
- results.push({ path: item.nodeId, success: false });
120
- continue;
121
- }
122
- const existing = await storage.getDoc(projectId, item.nodeId);
123
- if (!existing) {
124
- results.push({ path: item.nodeId, success: false });
125
- continue;
126
- }
127
- const updates = {};
128
- if (item.summary !== undefined)
129
- updates.summary = item.summary;
130
- if (item.content !== undefined)
131
- updates.content = item.content;
132
- if (item.status !== undefined)
133
- updates.status = item.status;
134
- if (item.versions !== undefined)
135
- updates.versions = item.versions;
136
- if (item.bugfixes !== undefined)
137
- updates.bugfixes = item.bugfixes;
138
- const doc = await storage.updateDoc(projectId, item.nodeId, updates);
139
- results.push({ path: item.nodeId, success: !!doc });
140
- }
141
- const successCount = results.filter(r => r.success).length;
142
- const failedPaths = results.filter(r => !r.success).map(r => r.path);
143
- let msg = `✅ 批量更新完成: ${successCount}/${decoded.updates.length} 成功`;
144
- if (failedPaths.length > 0) {
145
- msg += `\n❌ 失败: ${failedPaths.join(', ')}`;
146
- }
147
- return wrap(msg);
148
- }
149
- // 单个更新模式: 必须提供 nodeId
150
- const { nodeId, summary, content, status, versions, bugfixes } = decoded;
151
- if (!nodeId) {
152
- return wrap('❌ 请提供 nodeId(单个模式)或 updates 数组(批量模式)');
153
- }
154
- // 根文档必须使用 kg_update_root 更新
155
- if (nodeId === '/' || nodeId === '_root') {
156
- return wrap('❌ 根文档请使用 kg_update_root 方法更新');
157
- }
158
- // 获取现有文档
159
- const existing = await storage.getDoc(projectId, nodeId);
160
- if (!existing) {
161
- return wrap('更新失败(文档不存在)');
162
- }
163
- // 构建更新内容
164
- const updates = {};
165
- if (summary !== undefined)
166
- updates.summary = summary;
167
- if (content !== undefined)
168
- updates.content = content;
169
- if (status !== undefined)
170
- updates.status = status;
171
- if (versions !== undefined)
172
- updates.versions = versions;
173
- if (bugfixes !== undefined)
174
- updates.bugfixes = bugfixes;
175
- const doc = await storage.updateDoc(projectId, nodeId, updates);
176
- return wrap(doc ? JSON.stringify(doc, null, 2) : '更新失败');
177
- });
178
- // 3.5 更新根文档 (项目介绍)
179
- server.tool('kg_update_root', '更新项目介绍(根文档)', {
180
- title: z.string().optional().describe('项目标题'),
181
- description: z.string().optional().describe('项目介绍(Markdown)')
182
- }, async (args) => {
183
- const decoded = decodeObjectStrings(args);
184
- if (decoded.title === undefined && decoded.description === undefined) {
185
- return wrap('❌ 请至少提供 title 或 description');
186
- }
187
- const existing = await storage.getDoc(projectId, '/');
188
- if (!existing) {
189
- return wrap('更新失败(根文档不存在)');
190
- }
191
- const updates = {};
192
- if (decoded.title !== undefined)
193
- updates.summary = decoded.title;
194
- if (decoded.description !== undefined)
195
- updates.content = decoded.description;
196
- const doc = await storage.updateDoc(projectId, '/', updates);
197
- return wrap(doc ? '✅ 项目介绍已更新' : '更新失败');
198
- });
199
- // 3.6 获取项目规则
200
- server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
201
- ruleType: z.string().optional()
202
- .describe('规则类型(如 userStyles, codeStyle, reviewRules, testRules, unitTests, 或自定义类型)。不传则返回全部'),
203
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
204
- }, async (args) => {
205
- // 跨项目访问
206
- if (args.targetProject) {
207
- const crossMeta = await storage.crossGetRulesMeta(args.targetProject);
208
- if (args.ruleType) {
209
- const rules = await storage.crossGetRules(args.targetProject, args.ruleType);
210
- if (rules.length === 0) {
211
- const label = crossMeta[args.ruleType]?.label || args.ruleType;
212
- return { content: [{ type: 'text', text: `暂无${label}规则(项目: ${args.targetProject})` }] };
213
- }
214
- return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${rules.join('\n')}` }] };
215
- }
216
- // 获取全部规则 (从 meta 获取所有类型)
217
- const types = Object.keys(crossMeta).length > 0 ? Object.keys(crossMeta) : Object.keys(RULE_TYPE_LABELS);
218
- const allRules = [];
219
- for (const type of types) {
220
- const rules = await storage.crossGetRules(args.targetProject, type);
221
- if (rules.length > 0) {
222
- const label = crossMeta[type]?.label || RULE_TYPE_LABELS[type] || type;
223
- allRules.push(`## ${label}\n${rules.join('\n')}`);
224
- }
225
- }
226
- if (allRules.length === 0) {
227
- return { content: [{ type: 'text', text: `暂无项目规则(项目: ${args.targetProject})` }] };
228
- }
229
- return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${allRules.join('\n\n')}` }] };
230
- }
231
- // 当前项目
232
- const rules = await getRules(projectId, args.ruleType || undefined);
233
- if (!rules || rules.trim() === '') {
234
- const meta = await storage.getRulesMeta();
235
- const typeName = args.ruleType ? (meta[args.ruleType]?.label || args.ruleType) : '项目';
236
- return { content: [{ type: 'text', text: `暂无${typeName}规则` }] };
237
- }
238
- return { content: [{ type: 'text', text: rules }] };
239
- });
240
- // 3.7 保存项目规则 (自动合并,去重追加)
241
- server.tool('kg_save_rules', '保存单个类型的项目规则(自动合并:已有规则保留,新规则去重追加)', {
242
- ruleType: z.string()
243
- .describe('规则类型(如 userStyles, codeStyle, reviewRules, testRules, unitTests, 或自定义类型)'),
244
- rules: z.array(z.string()).describe('规则数组')
245
- }, async (args) => {
246
- const decoded = decodeObjectStrings(args);
247
- const result = await storage.appendRules(projectId, decoded.ruleType, decoded.rules);
248
- if (!result.success) {
249
- return wrap('❌ 保存失败');
250
- }
251
- const meta = await storage.getRulesMeta();
252
- const label = meta[decoded.ruleType]?.label || decoded.ruleType;
253
- return wrap(`✅ ${label}已保存 (新增${result.added}条, 共${result.total}条)`);
254
- });
255
- // 3.8 获取规则触发配置
256
- server.tool('kg_get_rules_meta', '获取规则触发配置(所有类型的标签、关键词、触发数)。用于查看/编辑 hooks 触发条件', {}, async () => {
257
- try {
258
- const meta = await storage.getRulesMeta();
259
- if (Object.keys(meta).length === 0) {
260
- return wrap('暂无规则配置');
261
- }
262
- const lines = Object.entries(meta).map(([type, m]) => {
263
- const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
264
- const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
265
- return `- **${m.label}** (${type}): ${trigger}`;
266
- });
267
- return wrap(`规则触发配置:\n\n${lines.join('\n')}`);
268
- }
269
- catch (e) {
270
- return wrap(`❌ ${String(e)}`);
271
- }
272
- });
273
- // 3.9 保存规则触发配置 (自动合并)
274
- server.tool('kg_save_rules_meta', '保存规则触发配置(自动合并:已有类型更新,新类型创建,其他类型不受影响)', {
275
- meta: z.record(z.string(), z.object({
276
- label: z.string().describe('规则显示名称'),
277
- keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
278
- min_hits: z.number().default(1).describe('最低触发关键词数'),
279
- always: z.boolean().default(false).describe('是否始终触发(如 userStyles)')
280
- })).describe('规则类型 → 触发配置的映射')
281
- }, async (args) => {
282
- try {
283
- const decoded = decodeObjectStrings(args.meta);
284
- const result = await storage.mergeRulesMeta(decoded);
285
- if (!result.success) {
286
- return wrap('❌ 保存失败');
287
- }
288
- return wrap(`✅ 触发配置已保存 (更新${Object.keys(decoded).length}个类型, 共${result.total}个类型)`);
289
- }
290
- catch (e) {
291
- return wrap(`❌ ${String(e)}`);
292
- }
293
- });
294
- // 3.10 获取全局默认规则触发配置
295
- server.tool('kg_get_global_rules_meta', '获取全局默认规则触发配置(新项目创建时继承此配置)', {}, async () => {
296
- try {
297
- const meta = await storage.getGlobalRulesMeta();
298
- if (Object.keys(meta).length === 0) {
299
- return wrap('暂无全局默认规则配置');
300
- }
301
- const lines = Object.entries(meta).map(([type, m]) => {
302
- const kw = m.keywords.length > 0 ? m.keywords.join(', ') : '(无)';
303
- const trigger = m.always ? '始终触发' : `关键词≥${m.min_hits}: ${kw}`;
304
- return `- **${m.label}** (${type}): ${trigger}`;
305
- });
306
- return wrap(`全局默认规则触发配置:\n\n${lines.join('\n')}`);
307
- }
308
- catch (e) {
309
- return wrap(`❌ ${String(e)}`);
310
- }
311
- });
312
- // 3.11 保存全局默认规则触发配置 (自动合并)
313
- server.tool('kg_save_global_rules_meta', '保存全局默认触发配置(自动合并:已有类型更新,新类型创建,其他类型不受影响)', {
314
- meta: z.record(z.string(), z.object({
315
- label: z.string().describe('规则显示名称'),
316
- keywords: z.array(z.string()).default([]).describe('触发关键词列表'),
317
- min_hits: z.number().default(1).describe('最低触发关键词数'),
318
- always: z.boolean().default(false).describe('是否始终触发(如 userStyles)')
319
- })).describe('规则类型 → 触发配置的映射')
320
- }, async (args) => {
321
- try {
322
- const decoded = decodeObjectStrings(args.meta);
323
- const result = await storage.mergeGlobalRulesMeta(decoded);
324
- if (!result.success) {
325
- return wrap('❌ 保存失败');
326
- }
327
- return wrap(`✅ 全局触发配置已保存 (更新${Object.keys(decoded).length}个类型, 共${result.total}个类型)`);
328
- }
329
- catch (e) {
330
- return wrap(`❌ ${String(e)}`);
331
- }
332
- });
333
- // 5. 读取单个文档详情
334
- server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
335
- nodeId: z.string().describe('文档路径'),
336
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
337
- }, async (args) => {
338
- // 跨项目访问
339
- if (args.targetProject) {
340
- const doc = await storage.crossGetDoc(args.targetProject, args.nodeId);
341
- if (!doc) {
342
- return wrap(`文档不存在(项目: ${args.targetProject})`);
343
- }
344
- const lines = formatDocMarkdown(doc, args.nodeId);
345
- return wrap(`[跨项目: ${args.targetProject}]\n\n${lines.join('\n')}`);
346
- }
347
- // 当前项目
348
- const doc = await storage.getDoc(projectId, args.nodeId);
349
- if (!doc) {
350
- return wrap('文档不存在');
351
- }
352
- // 格式化文档
353
- const lines = formatDocMarkdown(doc, args.nodeId);
354
- return wrap(lines.join('\n'));
355
- });
356
- // 10. 获取知识库树状图
357
- server.tool('kg_get_tree', '获取知识库目录树。格式: 📄 文档名 # 简介。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
358
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
359
- }, async (args) => {
360
- try {
361
- // 跨项目访问
362
- if (args.targetProject) {
363
- const tree = await storage.crossGetTree(args.targetProject);
364
- if (tree && tree.length > 0) {
365
- const treeText = formatTreeText(tree);
366
- const docCount = countTreeDocs(tree);
367
- return wrap(`[跨项目: ${args.targetProject}]\n\n共 ${docCount} 个文档\n\n${treeText}`);
368
- }
369
- return wrap(`知识库为空(项目: ${args.targetProject})`);
370
- }
371
- // 当前项目
372
- const tree = await storage.getTree(projectId);
373
- if (tree && tree.length > 0) {
374
- const treeText = formatTreeText(tree);
375
- const docCount = countTreeDocs(tree);
376
- return wrap(`共 ${docCount} 个文档\n\n${treeText}`);
377
- }
378
- return wrap('知识库为空');
379
- }
380
- catch (e) {
381
- return wrap(JSON.stringify({
382
- error: '无法获取知识库结构',
383
- message: String(e),
384
- tip: '请确保软件端已启动'
385
- }, null, 2));
386
- }
387
- });
388
- // 11. 按状态筛选文档
389
- server.tool('kg_get_docs_by_status', '获取指定状态的文档列表。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
390
- statusList: z.array(z.string()).describe('状态列表(如["未完成","待修复"])'),
391
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
392
- }, async (args) => {
393
- // 跨项目访问
394
- if (args.targetProject) {
395
- const docs = await storage.crossGetDocsByStatus(args.targetProject, args.statusList);
396
- if (docs.length === 0) {
397
- return wrap(`未找到状态为 [${args.statusList.join(', ')}] 的文档(项目: ${args.targetProject})`);
398
- }
399
- const lines = docs.map(d => `- ${d.path} (${d.status || '已完成'}): ${d.summary || '无简介'}`);
400
- return wrap(`[跨项目: ${args.targetProject}]\n\n找到 ${docs.length} 个文档:\n\n${lines.join('\n')}`);
401
- }
402
- // 当前项目
403
- const docs = await storage.getDocsByStatus(projectId, args.statusList);
404
- if (docs.length === 0) {
405
- return wrap(`未找到状态为 [${args.statusList.join(', ')}] 的文档`);
406
- }
407
- const lines = docs.map(d => `- ${d.path} (${d.status || '已完成'}): ${d.summary || '无简介'}`);
408
- return wrap(`找到 ${docs.length} 个文档:\n\n${lines.join('\n')}`);
409
- });
410
- // 12. 跨项目复制文档到当前项目
411
- server.tool('kg_copy_node', '从其他项目复制文档到当前项目。支持单个或批量复制,可指定新路径。用于跨项目协作:拉取别的项目的知识文档到当前项目', {
412
- sourceProject: z.string().describe('源项目ID或名称(从kg_list_projects获取)'),
413
- sourcePath: z.union([z.string(), z.array(z.string())]).describe('源文档路径(单个或数组,如"/架构/数据流"或["/架构/数据流","/MCP/tools"])'),
414
- targetPath: z.string().optional().describe('目标路径前缀(如"/参考/项目B")。不填则保持原路径'),
415
- }, async (args) => {
416
- const decoded = decodeObjectStrings(args);
417
- const paths = Array.isArray(decoded.sourcePath) ? decoded.sourcePath : [decoded.sourcePath];
418
- const results = [];
419
- for (const srcPath of paths) {
420
- try {
421
- // 从源项目读取文档
422
- const doc = await storage.crossGetDoc(decoded.sourceProject, srcPath);
423
- if (!doc) {
424
- results.push({ path: srcPath, success: false, error: '源文档不存在' });
425
- continue;
426
- }
427
- // 计算目标路径
428
- const destPath = decoded.targetPath
429
- ? `${decoded.targetPath.replace(/\/$/, '')}${srcPath}`
430
- : srcPath;
431
- // 检查目标是否已存在
432
- const existing = await storage.getDoc(projectId, destPath);
433
- if (existing) {
434
- // 已存在则更新
435
- await storage.updateDoc(projectId, destPath, {
436
- summary: doc.summary,
437
- content: doc.content,
438
- });
439
- results.push({ path: destPath, success: true });
440
- }
441
- else {
442
- // 不存在则创建
443
- await storage.createDoc(projectId, destPath, {
444
- summary: doc.summary,
445
- content: doc.content,
446
- status: doc.status,
447
- });
448
- results.push({ path: destPath, success: true });
449
- }
450
- }
451
- catch (e) {
452
- results.push({ path: srcPath, success: false, error: String(e) });
453
- }
454
- }
455
- const successCount = results.filter(r => r.success).length;
456
- const failedItems = results.filter(r => !r.success);
457
- if (paths.length === 1) {
458
- if (results[0].success) {
459
- return wrap(`✅ 已复制文档: ${results[0].path}\n\n来源: [${decoded.sourceProject}] ${paths[0]}`);
460
- }
461
- return wrap(`❌ 复制失败: ${results[0].error}`);
462
- }
463
- let msg = `✅ 批量复制完成: ${successCount}/${paths.length} 成功`;
464
- if (failedItems.length > 0) {
465
- msg += `\n❌ 失败:\n${failedItems.map(f => ` - ${f.path}: ${f.error}`).join('\n')}`;
466
- }
467
- return wrap(msg);
468
- });
469
- // ===================== 任务管理 =====================
470
- // 7. 创建任务
471
- server.tool('task_create', '创建开发任务', {
472
- title: z.string().describe('任务标题'),
473
- description: z.string().describe('任务描述(Markdown)'),
474
- goals: z.array(z.string()).optional().describe('目标清单')
475
- }, async (args) => {
476
- const decoded = decodeObjectStrings(args);
477
- const task = await storage.createTask(projectId, {
478
- title: decoded.title,
479
- description: decoded.description,
480
- goals: decoded.goals || []
481
- }, _user);
482
- return wrap(JSON.stringify(task, null, 2));
483
- });
484
- // 8. 读取任务
485
- server.tool('task_get', '读取任务详情(按名字搜索,支持当前任务和历史任务)', {
486
- title: z.string().describe('任务名称(模糊匹配)'),
487
- status: z.enum(['active', 'archived', 'all']).optional().describe('状态筛选: active=进行中, archived=已归档, all=全部(默认)')
488
- }, async (args) => {
489
- const status = args.status || 'all';
490
- const searchTitle = args.title.toLowerCase();
491
- // 获取任务列表
492
- let tasks;
493
- if (status === 'all') {
494
- const [active, archived] = await Promise.all([
495
- storage.listTasks(projectId, 'active'),
496
- storage.listTasks(projectId, 'archived')
497
- ]);
498
- tasks = [...active, ...archived];
499
- }
500
- else {
501
- tasks = await storage.listTasks(projectId, status);
502
- }
503
- // 按名字模糊匹配
504
- const matched = tasks.filter(t => t.title.toLowerCase().includes(searchTitle));
505
- if (matched.length === 0) {
506
- return wrap(`未找到匹配 "${args.title}" 的任务`);
507
- }
508
- // 如果只有一个匹配,返回完整详情
509
- if (matched.length === 1) {
510
- const task = await storage.getTask(projectId, matched[0].id);
511
- return wrap(task ? JSON.stringify(task, null, 2) : '任务详情获取失败');
512
- }
513
- // 多个匹配,返回列表让用户选择
514
- const list = matched.map(t => ({
515
- id: t.id,
516
- title: t.title,
517
- status: t.status,
518
- creator: t.creator,
519
- created_at: t.created_at
520
- }));
521
- return wrap(`找到 ${matched.length} 个匹配任务:\n${JSON.stringify(list, null, 2)}\n\n请提供更精确的任务名称`);
522
- });
523
- // 9. 更新任务
524
- server.tool('task_update', '更新任务(添加进展日志)', {
525
- taskId: z.string().describe('任务ID'),
526
- log_type: z.enum(['progress', 'issue', 'solution', 'reference']).describe('日志类型: progress=进展, issue=问题, solution=方案, reference=参考'),
527
- content: z.string().describe('日志内容(Markdown)')
528
- }, async (args) => {
529
- const decoded = decodeObjectStrings(args);
530
- const task = await storage.addTaskLog(projectId, args.taskId, decoded.log_type, decoded.content);
531
- if (!task) {
532
- return wrap('更新失败(任务不存在或已归档)');
533
- }
534
- return wrap(`✅ 日志已添加,任务共有 ${task.logs.length} 条日志`);
535
- });
536
- // 10. 归档任务
537
- server.tool('task_archive', '归档任务(完成并填写经验总结)', {
538
- taskId: z.string().describe('任务ID'),
539
- summary: z.string().describe('经验总结(Markdown)'),
540
- difficulties: z.array(z.string()).optional().describe('遇到的困难'),
541
- solutions: z.array(z.string()).optional().describe('解决方案')
542
- }, async (args) => {
543
- const decoded = decodeObjectStrings(args);
544
- const task = await storage.completeTask(projectId, args.taskId, {
545
- summary: decoded.summary,
546
- difficulties: decoded.difficulties || [],
547
- solutions: decoded.solutions || [],
548
- references: []
549
- });
550
- if (!task) {
551
- return wrap('归档失败(任务不存在或已归档)');
552
- }
553
- return wrap(`✅ 任务已归档: ${task.title}`);
554
- });
555
- // ===================== 项目文件访问 =====================
556
- // 上传本地目录到中心服务器
557
- server.tool('project_upload', '上传本地项目目录到中心服务器(打包zip上传,自动排除node_modules/.git等)。上传后其他客户端可通过跨项目访问下载', {
558
- localDir: z.string().describe('本地目录的绝对路径(如"/home/user/project/src")'),
559
- remoteDir: z.string().optional().describe('上传到服务器的目标子目录(如"src"),不填则上传到根目录'),
560
- }, async (args) => {
561
- try {
562
- const result = await storage.uploadFiles(args.localDir, args.remoteDir);
563
- return wrap(`✅ 上传成功\n\n- 文件数量: ${result.fileCount}\n- 目标目录: ${args.remoteDir || '/'}\n\n其他客户端可通过 project_read_file / project_download_dir 访问`);
564
- }
565
- catch (e) {
566
- return wrap(`❌ ${String(e)}`);
567
- }
568
- });
569
- // 清空项目文件存储区
570
- server.tool('project_clear_files', '清空当前项目在中心服务器上的文件存储区', {}, async () => {
571
- try {
572
- await storage.clearFiles();
573
- return wrap('✅ 文件存储已清空');
574
- }
575
- catch (e) {
576
- return wrap(`❌ ${String(e)}`);
577
- }
578
- });
579
- // 列出项目文件
580
- server.tool('project_list_files', '浏览项目源码目录结构(仅列出文件名和大小,不读内容)。目标项目需已上传文件或关联源码目录。跨项目时先用kg_list_projects获取项目ID。查看文件内容请用project_read_file', {
581
- dir: z.string().optional().describe('子目录路径(如"src/components"),不填则列出根目录'),
582
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
583
- }, async (args) => {
584
- try {
585
- const files = args.targetProject
586
- ? await storage.crossListFiles(args.targetProject, args.dir)
587
- : await storage.listFiles(args.dir);
588
- if (files.length === 0) {
589
- return wrap('目录为空');
590
- }
591
- const lines = files.map(f => {
592
- const icon = f.isDir ? '📁' : '📄';
593
- const size = f.isDir ? '' : ` (${formatFileSize(f.size)})`;
594
- return `${icon} ${f.name}${size}`;
595
- });
596
- const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
597
- const dirLabel = args.dir || '/';
598
- return wrap(`${prefix}📂 ${dirLabel} (${files.length} 项)\n\n${lines.join('\n')}`);
599
- }
600
- catch (e) {
601
- return wrap(`❌ ${String(e)}`);
602
- }
603
- });
604
- // 读取项目文件
605
- server.tool('project_read_file', '读取单个源码文件的文本内容(限1MB)。适合查看具体代码实现。跨项目时先用kg_list_projects获取项目ID。浏览目录结构请用project_list_files', {
606
- path: z.string().describe('文件路径(如"src/main.ts")'),
607
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
608
- }, async (args) => {
609
- try {
610
- const content = args.targetProject
611
- ? await storage.crossReadFile(args.targetProject, args.path)
612
- : await storage.readFile(args.path);
613
- const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
614
- return wrap(`${prefix}📄 ${args.path}\n\n\`\`\`\n${content}\n\`\`\``);
615
- }
616
- catch (e) {
617
- return wrap(`❌ ${String(e)}`);
618
- }
619
- });
620
- // 下载项目文件或目录 (zip → 自动解压)
621
- server.tool('project_download', '下载项目源码到本地(自动识别文件/目录,打包zip解压)。目标项目需已上传文件或关联源码目录。跨项目时先用kg_list_projects获取项目ID', {
622
- remotePath: z.string().describe('远程路径,可以是目录(如"src")或单个文件(如"src/main.ts")'),
623
- localPath: z.string().optional().describe('本地保存路径(不填则保存到系统临时目录,目录不存在会自动创建)'),
624
- targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
625
- }, async (args) => {
626
- try {
627
- const result = args.targetProject
628
- ? await storage.crossDownload(args.targetProject, args.remotePath, args.localPath)
629
- : await storage.download(args.remotePath, args.localPath);
630
- const prefix = args.targetProject ? `[跨项目: ${args.targetProject}]\n\n` : '';
631
- return wrap(`${prefix}✅ 已下载并解压\n\n- 本地路径: ${result.localPath}\n- 文件数量: ${result.fileCount}`);
632
- }
633
- catch (e) {
634
- return wrap(`❌ ${String(e)}`);
635
- }
636
- });
1
+ /**
2
+ * MCP 工具注册入口
3
+ * 25 个工具拆分为 5 个子模块,统一在此注册
4
+ */
5
+ import { registerDocTools } from './docs.js';
6
+ import { registerRuleTools } from './rules.js';
7
+ import { registerTaskTools } from './tasks.js';
8
+ import { registerFileTools } from './files.js';
9
+ import { registerProjectTools } from './projects.js';
10
+ export function registerTools(server, projectId, user) {
11
+ registerProjectTools(server, projectId);
12
+ registerDocTools(server, projectId);
13
+ registerRuleTools(server, projectId);
14
+ registerTaskTools(server, projectId, user);
15
+ registerFileTools(server);
637
16
  }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 项目管理工具 (3个)
3
+ * kg_list_projects, kg_create_project, kg_update_root
4
+ */
5
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
+ export declare function registerProjectTools(server: McpServer, projectId: string): void;