@ppdocs/mcp 3.10.0 → 3.13.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.
@@ -2,33 +2,19 @@ import { z } from 'zod';
2
2
  import { getClient } from '../storage/httpClient.js';
3
3
  import { decodeObjectStrings } from '../utils.js';
4
4
  import { safeTool, wrap } from './shared.js';
5
- const linkSchema = z.object({
6
- url: z.string(),
7
- label: z.string(),
8
- });
9
- const fileSchema = z.object({
10
- name: z.string(),
11
- path: z.string(),
12
- });
13
- const adoptedNodeSchema = z.object({
14
- chartId: z.string(),
15
- nodeId: z.string(),
16
- nodeLabel: z.string(),
17
- });
5
+ function formatBytes(bytes) {
6
+ if (bytes < 1024)
7
+ return `${bytes} B`;
8
+ if (bytes < 1048576)
9
+ return `${(bytes / 1024).toFixed(1)} KB`;
10
+ return `${(bytes / 1048576).toFixed(1)} MB`;
11
+ }
18
12
  export function registerReferenceTools(server) {
19
13
  const client = () => getClient();
20
- server.tool('kg_ref', '📎 外部参考 管理项目引用的第三方资料、链接、脚本和样例文件。\n' +
21
- '涉及参考资料时主动调用:list 查看已有 → get 读详情 → read_file 读附件内容 → save 新增。\n' +
22
- 'actions: list|get|save|delete|read_file', {
23
- action: z.enum(['list', 'get', 'save', 'delete', 'read_file']).describe('操作类型'),
24
- id: z.string().optional().describe('参考 ID'),
25
- title: z.string().optional().describe('参考标题 (save)'),
26
- summary: z.string().optional().describe('Markdown 总结 (save)'),
27
- links: z.array(linkSchema).optional().describe('参考链接 (save)'),
28
- files: z.array(fileSchema).optional().describe('参考文件 (save)'),
29
- scripts: z.array(fileSchema).optional().describe('解析脚本 (save)'),
30
- adoptedBy: z.array(adoptedNodeSchema).optional().describe('已采用该参考的节点 (save)'),
31
- path: z.string().optional().describe('参考文件路径 (read_file)'),
14
+ server.tool('kg_ref', '外部参考管理。fetch: 输入URL拉取网页内容到本地持久化。list/get/delete: 管理已有参考。\nactions: list|get|fetch|delete', {
15
+ action: z.enum(['list', 'get', 'fetch', 'delete']).describe('操作类型'),
16
+ id: z.string().optional().describe('参考 ID (get/delete/fetch时指定ID则为refetch)'),
17
+ url: z.string().optional().describe('要拉取的文档 URL (fetch)'),
32
18
  }, async (args) => safeTool(async () => {
33
19
  const decoded = decodeObjectStrings(args);
34
20
  switch (decoded.action) {
@@ -37,19 +23,20 @@ export function registerReferenceTools(server) {
37
23
  if (refs.length === 0)
38
24
  return wrap('当前没有外部参考');
39
25
  const lines = [
40
- `📎 外部参考 (${refs.length})`,
26
+ `外部参考 (${refs.length})`,
41
27
  '',
42
- '| ID | 标题 | 链接 | 文件 | 已采用 |',
28
+ '| ID | 标题 | 链接 | 文件 | 拉取次数 |',
43
29
  '|:---|:---|---:|---:|---:|',
44
30
  ];
45
31
  for (const ref of refs) {
46
- lines.push(`| ${ref.id} | ${ref.title} | ${ref.links.length} | ${ref.files.length} | ${ref.adoptedBy.length} |`);
32
+ const fetchCount = ref.fetchHistory?.length || 0;
33
+ lines.push(`| ${ref.id} | ${ref.title} | ${ref.links.length} | ${ref.files.length} | ${fetchCount} |`);
47
34
  }
48
35
  return wrap(lines.join('\n'));
49
36
  }
50
37
  case 'get': {
51
38
  if (!decoded.id)
52
- return wrap('get 需要 id');
39
+ return wrap('get 需要 id');
53
40
  const ref = await client().getReference(decoded.id);
54
41
  if (!ref)
55
42
  return wrap(`未找到外部参考: ${decoded.id}`);
@@ -59,11 +46,17 @@ export function registerReferenceTools(server) {
59
46
  `- ID: ${ref.id}`,
60
47
  `- Links: ${ref.links.length}`,
61
48
  `- Files: ${ref.files.length}`,
62
- `- Scripts: ${ref.scripts.length}`,
63
- `- AdoptedBy: ${ref.adoptedBy.length}`,
64
- '',
65
- ref.summary || '(无摘要)',
66
49
  ];
50
+ if (ref.sourceUrl) {
51
+ lines.push(`- SourceURL: ${ref.sourceUrl}`);
52
+ }
53
+ if (ref.fetchHistory && ref.fetchHistory.length > 0) {
54
+ lines.push(`- FetchCount: ${ref.fetchHistory.length} 次`);
55
+ lines.push(`- LastFetch: ${ref.fetchHistory[ref.fetchHistory.length - 1].fetchedAt}`);
56
+ }
57
+ lines.push(`- Created: ${ref.createdAt}`);
58
+ lines.push(`- Updated: ${ref.updatedAt}`);
59
+ lines.push('', ref.summary || '(无摘要)');
67
60
  if (ref.links.length > 0) {
68
61
  lines.push('', '## Links');
69
62
  for (const link of ref.links) {
@@ -76,53 +69,66 @@ export function registerReferenceTools(server) {
76
69
  lines.push(`- ${file.name}: ${file.path}`);
77
70
  }
78
71
  }
79
- if (ref.scripts.length > 0) {
80
- lines.push('', '## Scripts');
81
- for (const script of ref.scripts) {
82
- lines.push(`- ${script.name}: ${script.path}`);
83
- }
72
+ if (ref.fetchHistory && ref.fetchHistory.length > 0) {
73
+ lines.push('', '## Fetch History');
74
+ lines.push('| # | 时间 | 标题 | 大小 | 文件 |');
75
+ lines.push('|:--|:-----|:-----|-----:|:-----|');
76
+ ref.fetchHistory.forEach((h, i) => {
77
+ lines.push(`| ${i + 1} | ${h.fetchedAt.substring(0, 19)} | ${h.title} | ${formatBytes(h.contentLength)} | ${h.contentFile} |`);
78
+ });
84
79
  }
85
80
  return wrap(lines.join('\n'));
86
81
  }
87
- case 'save': {
88
- if (!decoded.id)
89
- return wrap('❌ save 需要 id');
90
- if (!decoded.title)
91
- return wrap('❌ save 需要 title');
92
- const existing = await client().getReference(decoded.id);
93
- const now = new Date().toISOString();
94
- // 直接传递原始路径给后端, Rust save_reference 会自动:
95
- // 1. 检测绝对路径文件
96
- // 2. 复制到公共文件池/外部参考/{项目名}/
97
- // 3. 复制到 ref-files/ ( read_file API 读取)
98
- // 4. 替换为相对路径保存
99
- await client().saveReference({
100
- id: decoded.id,
101
- title: decoded.title,
102
- summary: decoded.summary || existing?.summary || '',
103
- links: decoded.links || existing?.links || [],
104
- files: decoded.files || existing?.files || [],
105
- scripts: decoded.scripts || existing?.scripts || [],
106
- adoptedBy: decoded.adoptedBy || existing?.adoptedBy || [],
107
- createdAt: existing?.createdAt || now,
108
- updatedAt: now,
109
- });
110
- return wrap(`✅ 外部参考已保存 (${decoded.id})`);
82
+ case 'fetch': {
83
+ // 如果提供了 id 但没有 url,视为 refetch(从已有参考的 sourceUrl 重新拉取)
84
+ if (!decoded.url && decoded.id) {
85
+ const ref = await client().getReference(decoded.id);
86
+ if (!ref)
87
+ return wrap(`未找到参考: ${decoded.id}`);
88
+ if (!ref.sourceUrl)
89
+ return wrap(`参考 ${decoded.id} 没有 sourceUrl,请提供 url 参数`);
90
+ const result = await client().fetchRefUrl(ref.sourceUrl, decoded.id);
91
+ return wrap([
92
+ `文档已重新拉取 ( ${result.fetchCount} )`,
93
+ '',
94
+ `| 字段 | 值 |`,
95
+ `|:---|:---|`,
96
+ `| 参考 ID | ${result.refId} |`,
97
+ `| 标题 | ${result.title} |`,
98
+ `| 内容大小 | ${formatBytes(result.contentLength)} |`,
99
+ '',
100
+ '### 内容预览',
101
+ '',
102
+ result.contentPreview.substring(0, 300),
103
+ ].join('\n'));
104
+ }
105
+ if (!decoded.url)
106
+ return wrap('fetch 需要 url (要拉取的文档地址)');
107
+ const result = await client().fetchRefUrl(decoded.url, decoded.id);
108
+ const lines = [
109
+ `文档已拉取并保存`,
110
+ '',
111
+ `| 字段 | 值 |`,
112
+ `|:---|:---|`,
113
+ `| 参考 ID | ${result.refId} |`,
114
+ `| 标题 | ${result.title} |`,
115
+ `| 内容大小 | ${formatBytes(result.contentLength)} |`,
116
+ `| 累计拉取 | ${result.fetchCount} 次 |`,
117
+ '',
118
+ '### 内容预览',
119
+ '',
120
+ result.contentPreview.substring(0, 500),
121
+ ];
122
+ return wrap(lines.join('\n'));
111
123
  }
112
124
  case 'delete': {
113
125
  if (!decoded.id)
114
- return wrap('delete 需要 id');
126
+ return wrap('delete 需要 id');
115
127
  const ok = await client().deleteReference(decoded.id);
116
- return wrap(ok ? `✅ 外部参考已删除 (${decoded.id})` : `ℹ️ 未找到外部参考 (${decoded.id})`);
117
- }
118
- case 'read_file': {
119
- if (!decoded.path)
120
- return wrap('❌ read_file 需要 path');
121
- const content = await client().readReferenceFile(decoded.path);
122
- return wrap(content);
128
+ return wrap(ok ? `外部参考已删除 (${decoded.id})` : `未找到外部参考 (${decoded.id})`);
123
129
  }
124
130
  default:
125
- return wrap(`❌ 未知 action: ${decoded.action}`);
131
+ return wrap(`未知 action: ${decoded.action}`);
126
132
  }
127
133
  }));
128
134
  }
@@ -1,6 +1,5 @@
1
1
  /**
2
- * 📝 kg_task (4→1) 极简任务管理
3
- * AI 只需说人话写内容,后端负责所有格式化、分类、时间戳
2
+ * kg_task — 任务管理
4
3
  */
5
4
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
6
5
  import { type McpContext } from './shared.js';
@@ -1,6 +1,5 @@
1
1
  /**
2
- * 📝 kg_task (4→1) 极简任务管理
3
- * AI 只需说人话写内容,后端负责所有格式化、分类、时间戳
2
+ * kg_task — 任务管理
4
3
  */
5
4
  import { z } from 'zod';
6
5
  import { getClient } from '../storage/httpClient.js';
@@ -8,7 +7,7 @@ import { decodeObjectStrings } from '../utils.js';
8
7
  import { wrap, safeTool } from './shared.js';
9
8
  export function registerTaskTools(server, ctx) {
10
9
  const client = () => getClient();
11
- server.tool('kg_task', '📝 任务记录 — action: create(创建,建议bindTo关联节点)|get(查询)|update(追加进度)|archive(归档)|delete(删除)。⚠️ 每完成一个步骤必须立即update!', {
10
+ server.tool('kg_task', '任务记录 — action: create(创建)|get(查询)|update(追加进度)|archive(归档)|delete(删除)', {
12
11
  action: z.enum(['create', 'get', 'update', 'archive', 'delete'])
13
12
  .describe('操作类型'),
14
13
  title: z.string().optional()
@@ -26,9 +25,9 @@ export function registerTaskTools(server, ctx) {
26
25
  summary: z.string().optional()
27
26
  .describe('经验总结Markdown (archive)'),
28
27
  difficulties: z.array(z.string()).optional()
29
- .describe('遇到的困难 (archive)'),
28
+ .describe('遇到的困难 (archive, 可选)'),
30
29
  solutions: z.array(z.string()).optional()
31
- .describe('解决方案 (archive)'),
30
+ .describe('解决方案 (archive, 可选)'),
32
31
  status: z.enum(['active', 'archived', 'all']).optional()
33
32
  .describe('状态筛选 (get, 默认active)'),
34
33
  bindTo: z.string().optional()
@@ -40,12 +39,10 @@ export function registerTaskTools(server, ctx) {
40
39
  switch (decoded.action) {
41
40
  case 'create': {
42
41
  if (!decoded.title)
43
- return wrap('create 需要 title');
44
- if (!decoded.description)
45
- return wrap('❌ create 需要 description');
42
+ return wrap('create 需要 title');
46
43
  const task = await client().createTask({
47
44
  title: decoded.title,
48
- description: decoded.description,
45
+ description: decoded.description || '',
49
46
  goals: decoded.goals || []
50
47
  }, ctx.user);
51
48
  // 自动绑定到流程图节点
@@ -63,22 +60,18 @@ export function registerTaskTools(server, ctx) {
63
60
  node.boundTasks.push(task.id);
64
61
  }
65
62
  await client().saveFlowchart(chartId, chart);
66
- bindMsg = `\n🔗 已绑定到节点: ${decoded.bindTo} [${chartId}]`;
63
+ bindMsg = `\n已绑定到节点: ${decoded.bindTo} [${chartId}]`;
67
64
  }
68
65
  else {
69
- bindMsg = `\n⚠️ 节点 "${decoded.bindTo}" 不存在于流程图 "${chartId}"`;
66
+ bindMsg = `\n节点 "${decoded.bindTo}" 不存在于流程图 "${chartId}"`;
70
67
  }
71
68
  }
72
69
  }
73
70
  catch {
74
- bindMsg = `\n⚠️ 绑定失败`;
71
+ bindMsg = `\n绑定失败`;
75
72
  }
76
73
  }
77
- // 约束: 未绑定流程图 强制警告
78
- if (!decoded.bindTo) {
79
- bindMsg = `\n\n⚠️ 【未关联知识锚点】此任务未绑定到任何流程图节点!\n💡 建议: 使用 kg_flowchart(get) 查看节点列表, 然后用 kg_flowchart(bind, nodeId, tasks:["${task.id}"]) 绑定\n📌 不关联的任务会成为孤岛数据, 无法被其他AI发现`;
80
- }
81
- return wrap(`✅ 任务已创建\nID: ${task.id}\n标题: ${task.title}\n验收标准: ${task.detail.goals.length}条${bindMsg}`);
74
+ return wrap(`任务已创建\nID: ${task.id}\n标题: ${task.title}\n验收标准: ${task.detail.goals.length}条${bindMsg}`);
82
75
  }
83
76
  case 'get': {
84
77
  // 无参数 = 列出活跃任务
@@ -96,33 +89,33 @@ export function registerTaskTools(server, ctx) {
96
89
  tasks = await client().listTasks(status);
97
90
  }
98
91
  if (tasks.length === 0)
99
- return wrap(`📋 ${status === 'archived' ? '归档' : '活跃'}任务为空`);
92
+ return wrap(`${status === 'archived' ? '归档' : '活跃'}任务为空`);
100
93
  const rows = tasks.map(t => {
101
- const icon = t.status === 'active' ? '🟢' : '';
94
+ const icon = t.status === 'active' ? '' : '';
102
95
  const date = t.updated_at?.split('T')[0]?.slice(5) || '';
103
96
  const log = (t.last_log || '').substring(0, 40);
104
97
  return `| ${icon} | ${t.title} | ${t.id} | ${date} | ${log} |`;
105
98
  }).join('\n');
106
- return wrap(`📋 ${tasks.length} 个任务\n\n| 状态 | 标题 | ID | 更新 | 最近日志 |\n|:-----|:-----|:---|:-----|:---------|\n${rows}`);
99
+ return wrap(`共 ${tasks.length} 个任务\n\n| 状态 | 标题 | ID | 更新 | 最近日志 |\n|:-----|:-----|:---|:-----|:---------|\n${rows}`);
107
100
  }
108
101
  // 有 taskId = 获取详情 (结构化摘要)
109
102
  if (decoded.taskId) {
110
103
  const task = await client().getTask(decoded.taskId, 'smart');
111
104
  if (!task)
112
- return wrap('任务未找到');
105
+ return wrap('任务未找到');
113
106
  const lines = [
114
- `📝 ${task.title}`,
107
+ `${task.title}`,
115
108
  `ID: ${task.id} | 状态: ${task.status}`,
116
109
  `创建: ${task.created_at?.split('T')[0] || ''}`,
117
110
  ];
118
111
  if (task.detail?.description)
119
- lines.push(`\n📋 ${task.detail.description}`);
112
+ lines.push(`\n${task.detail.description}`);
120
113
  if (task.detail?.goals?.length) {
121
- lines.push(`\n🎯 验收标准:`);
114
+ lines.push(`\n验收标准:`);
122
115
  task.detail.goals.forEach((g, i) => lines.push(` ${i + 1}. ☐ ${g}`));
123
116
  }
124
117
  if (task.logs?.length) {
125
- lines.push(`\n📊 进度日志 (${task.logs.length}条, 最近3条):`);
118
+ lines.push(`\n进度日志 (${task.logs.length}条, 最近3条):`);
126
119
  for (const log of task.logs.slice(-3)) {
127
120
  lines.push(` [${log.log_type}] ${log.time?.split('T')[0] || ''}: ${log.content?.substring(0, 120)}`);
128
121
  }
@@ -151,18 +144,18 @@ export function registerTaskTools(server, ctx) {
151
144
  if (!task)
152
145
  return wrap('任务详情获取失败');
153
146
  const lines = [
154
- `📝 ${task.title}`,
147
+ `${task.title}`,
155
148
  `ID: ${task.id} | 状态: ${task.status}`,
156
149
  `创建: ${task.created_at?.split('T')[0] || ''}`,
157
150
  ];
158
151
  if (task.detail?.description)
159
- lines.push(`\n📋 ${task.detail.description}`);
152
+ lines.push(`\n${task.detail.description}`);
160
153
  if (task.detail?.goals?.length) {
161
- lines.push(`\n🎯 验收标准:`);
154
+ lines.push(`\n验收标准:`);
162
155
  task.detail.goals.forEach((g, i) => lines.push(` ${i + 1}. ☐ ${g}`));
163
156
  }
164
157
  if (task.logs?.length) {
165
- lines.push(`\n📊 进度日志 (${task.logs.length}条, 最近3条):`);
158
+ lines.push(`\n进度日志 (${task.logs.length}条, 最近3条):`);
166
159
  for (const log of task.logs.slice(-3)) {
167
160
  lines.push(` [${log.log_type}] ${log.time?.split('T')[0] || ''}: ${log.content?.substring(0, 120)}`);
168
161
  }
@@ -174,45 +167,38 @@ export function registerTaskTools(server, ctx) {
174
167
  }
175
168
  case 'update': {
176
169
  if (!decoded.taskId)
177
- return wrap('update 需要 taskId');
170
+ return wrap('update 需要 taskId');
178
171
  if (!decoded.content)
179
- return wrap('update 需要 content');
172
+ return wrap('update 需要 content');
180
173
  // log_type 可选 — 不传则后端自动检测关键词
181
174
  const task = await client().addTaskLog(decoded.taskId, decoded.content, decoded.log_type);
182
175
  if (!task)
183
176
  return wrap('更新失败(任务不存在或已归档)');
184
177
  const lastLog = task.logs[task.logs.length - 1];
185
- // 返回任务摘要 + 验收清单,让 AI 始终知道剩余目标
186
- const goalsDisplay = task.detail.goals.length > 0
187
- ? task.detail.goals.map((g, i) => ` ${i + 1}. ☐ ${g}`).join('\n')
188
- : ' (无验收标准)';
189
- return wrap(`✅ 进度已记录 [${lastLog?.log_type || 'progress'}]\n` +
190
- `📋 ${task.title} | 日志: ${task.logs.length}条\n` +
191
- `\n🎯 验收标准:\n${goalsDisplay}`);
178
+ return wrap(`进度已记录 [${lastLog?.log_type || 'progress'}]\n` +
179
+ `${task.title} | 日志: ${task.logs.length}条`);
192
180
  }
193
181
  case 'archive': {
194
182
  if (!decoded.taskId)
195
- return wrap('archive 需要 taskId');
196
- if (!decoded.summary)
197
- return wrap('❌ archive 需要 summary');
183
+ return wrap('archive 需要 taskId');
198
184
  const task = await client().completeTask(decoded.taskId, {
199
- summary: decoded.summary,
185
+ summary: decoded.summary || '任务已完成',
200
186
  difficulties: decoded.difficulties || [],
201
187
  solutions: decoded.solutions || [],
202
188
  references: []
203
189
  });
204
190
  if (!task)
205
191
  return wrap('归档失败(任务不存在或已归档)');
206
- return wrap(`✅ 任务已归档: ${task.title}`);
192
+ return wrap(`任务已归档: ${task.title}`);
207
193
  }
208
194
  case 'delete': {
209
195
  if (!decoded.taskId)
210
- return wrap('delete 需要 taskId');
196
+ return wrap('delete 需要 taskId');
211
197
  const ok = await client().deleteTask(decoded.taskId);
212
- return wrap(ok ? `✅ 任务已删除 (ID: ${decoded.taskId})` : '删除失败(任务不存在)');
198
+ return wrap(ok ? `任务已删除 (ID: ${decoded.taskId})` : '删除失败(任务不存在)');
213
199
  }
214
200
  default:
215
- return wrap(`❌ 未知 action: ${decoded.action}`);
201
+ return wrap(`未知 action: ${decoded.action}`);
216
202
  }
217
203
  }));
218
204
  }
@@ -4,9 +4,8 @@ import { decodeObjectStrings } from '../utils.js';
4
4
  import { safeTool, wrap } from './shared.js';
5
5
  export function registerWorkflowTools(server, _ctx) {
6
6
  const client = () => getClient();
7
- server.tool('kg_workflow', '📘 文档工作流 — AI 的标准操作手册。每个工作流是一份 Markdown 文档,定义了特定场景下 AI 应该遵循的步骤和规范。\n' +
8
- '调用方式:空参数=列出所有工作流;传 id=获取工作流正文;save/delete 需显式 action。\n' +
9
- '典型用法:拿到任务后先 kg_workflow() 看有无相关工作流,有则 kg_workflow(id:"xxx") 读取并遵循执行。', {
7
+ server.tool('kg_workflow', '文档工作流。AI 的标准操作手册,每个工作流定义特定场景下应遵循的步骤。空参数=列出全部;传id=获取正文;save/delete需显式action。' +
8
+ 'actions: list|get|save|delete', {
10
9
  action: z.enum(['list', 'get', 'save', 'delete']).optional().describe('省略时自动推断:无参数=list,有id=get'),
11
10
  id: z.string().optional().describe('工作流 ID'),
12
11
  scope: z.enum(['all', 'global', 'project']).optional().describe('范围:all(默认)|global|project'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.10.0",
3
+ "version": "3.13.0",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,15 +0,0 @@
1
- /**
2
- * 💬 kg_discuss (8 actions)
3
- * 合并: list, read, create, reply, close, delete, complete, history
4
- * 统一走 HTTP → Rust 后端 (单一写入者)
5
- * sender 格式: "projectId:user" (如 "p-ca3sgejg:张三")
6
- *
7
- * 权限模型:
8
- * list/history — 任何人可列出(全局可见)
9
- * create — 任何人可发起(指定参与项目ID)
10
- * read/reply — 仅讨论组内成员(initiator + participants)
11
- * complete/close/delete — 仅发起人
12
- */
13
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
- import { type McpContext } from './shared.js';
15
- export declare function registerDiscussionTools(server: McpServer, ctx: McpContext): void;