@ppdocs/mcp 3.4.0 → 3.6.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.
package/dist/config.d.ts CHANGED
@@ -6,6 +6,7 @@ export interface PpdocsConfig {
6
6
  apiUrl: string;
7
7
  projectId: string;
8
8
  user: string;
9
+ agentId: string;
9
10
  source: 'env' | 'file';
10
11
  }
11
12
  export declare const PPDOCS_CONFIG_FILE = ".ppdocs";
package/dist/config.js CHANGED
@@ -16,7 +16,13 @@ function readEnvConfig() {
16
16
  if (!apiUrl)
17
17
  return null;
18
18
  const match = apiUrl.match(/\/api\/([^/]+)\/[^/]+\/?$/);
19
- return { apiUrl, projectId: match?.[1] || 'unknown', user: process.env.PPDOCS_USER || generateUser(), source: 'env' };
19
+ return {
20
+ apiUrl,
21
+ projectId: match?.[1] || 'unknown',
22
+ user: process.env.PPDOCS_USER || generateUser(),
23
+ agentId: process.env.PPDOCS_AGENT_ID || 'default',
24
+ source: 'env',
25
+ };
20
26
  }
21
27
  function readPpdocsFile() {
22
28
  const configPath = path.join(process.cwd(), '.ppdocs');
@@ -26,7 +32,7 @@ function readPpdocsFile() {
26
32
  const c = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
27
33
  if (!c.api || !c.projectId || !c.key)
28
34
  return null;
29
- return { apiUrl: `${c.api}/api/${c.projectId}/${c.key}`, projectId: c.projectId, user: c.user || generateUser(), source: 'file' };
35
+ return { apiUrl: `${c.api}/api/${c.projectId}/${c.key}`, projectId: c.projectId, user: c.user || generateUser(), agentId: c.agentId || process.env.PPDOCS_AGENT_ID || 'default', source: 'file' };
30
36
  }
31
37
  catch {
32
38
  return null;
package/dist/index.js CHANGED
@@ -39,8 +39,9 @@ async function main() {
39
39
  }
40
40
  const projectId = config?.projectId || 'pending';
41
41
  const user = config?.user || 'agent';
42
+ const agentId = config?.agentId || process.env.PPDOCS_AGENT_ID || 'default';
42
43
  const server = new McpServer({ name: `ppdocs [${projectId}]`, version: VERSION }, { capabilities: { tools: {} } });
43
- registerTools(server, projectId, user, (newProjectId, newApiUrl) => {
44
+ registerTools(server, projectId, user, agentId, (newProjectId, newApiUrl) => {
44
45
  console.error(`[kg_init] 项目已切换: ${newProjectId}`);
45
46
  });
46
47
  const transport = new StdioServerTransport();
@@ -15,7 +15,9 @@ import { getClient } from '../storage/httpClient.js';
15
15
  import { decodeObjectStrings } from '../utils.js';
16
16
  import { wrap, safeTool } from './shared.js';
17
17
  function sender(ctx) {
18
- return `${ctx.projectId}:${ctx.user}`;
18
+ return ctx.agentId && ctx.agentId !== 'default'
19
+ ? `${ctx.projectId}:${ctx.user}:${ctx.agentId}`
20
+ : `${ctx.projectId}:${ctx.user}`;
19
21
  }
20
22
  /** 提取 sender 中的 projectId */
21
23
  function senderProjectId(senderStr) {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * kg_doc — 文档查询视图
3
+ * 纯查询工具,数据来源为流程图节点的 docEntries + description
4
+ * 不创建独立文档,只是提供面向文档的查询入口
5
+ *
6
+ * read 默认返回当前文档,history=true 返回历史列表,index=N 返回指定历史条目
7
+ */
8
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ import { type McpContext } from './shared.js';
10
+ export declare function registerDocQueryTools(server: McpServer, _ctx: McpContext): void;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * kg_doc — 文档查询视图
3
+ * 纯查询工具,数据来源为流程图节点的 docEntries + description
4
+ * 不创建独立文档,只是提供面向文档的查询入口
5
+ *
6
+ * read 默认返回当前文档,history=true 返回历史列表,index=N 返回指定历史条目
7
+ */
8
+ import { z } from 'zod';
9
+ import { getClient } from '../storage/httpClient.js';
10
+ import { decodeObjectStrings } from '../utils.js';
11
+ import { wrap, safeTool } from './shared.js';
12
+ function normalizeText(value) {
13
+ return (value ?? '').trim().toLowerCase();
14
+ }
15
+ /** 文档专用评分 — 侧重 summary/content/description */
16
+ function scoreDocMatch(node, terms) {
17
+ const description = normalizeText(node.description);
18
+ const entries = node.docEntries ?? [];
19
+ const allSummaries = entries.map((e) => e.summary).join(' ').toLowerCase();
20
+ const allContents = entries.map((e) => e.content).join(' ').toLowerCase();
21
+ const label = normalizeText(node.label);
22
+ let score = 0;
23
+ let matched = 0;
24
+ for (const term of terms) {
25
+ let hit = false;
26
+ if (allSummaries.includes(term)) {
27
+ score += 40;
28
+ hit = true;
29
+ }
30
+ if (allContents.includes(term)) {
31
+ score += 30;
32
+ hit = true;
33
+ }
34
+ if (description.includes(term)) {
35
+ score += 20;
36
+ hit = true;
37
+ }
38
+ if (label.includes(term)) {
39
+ score += 10;
40
+ hit = true;
41
+ }
42
+ if (hit)
43
+ matched += 1;
44
+ }
45
+ return matched === 0 ? 0 : score + matched * 50;
46
+ }
47
+ /** 加载所有流程图的完整数据 */
48
+ async function loadAllCharts() {
49
+ const client = getClient();
50
+ const briefs = (await client.listFlowcharts());
51
+ const loaded = await Promise.all(briefs.map(async (b) => (await client.getFlowchart(b.id))));
52
+ return loaded.filter((c) => Boolean(c));
53
+ }
54
+ /** 格式化日期为简短形式 */
55
+ function shortDate(iso) {
56
+ try {
57
+ return iso.slice(0, 10);
58
+ }
59
+ catch {
60
+ return iso;
61
+ }
62
+ }
63
+ export function registerDocQueryTools(server, _ctx) {
64
+ server.tool('kg_doc', '📘 文档查询视图 — 跨流程图搜索和阅读节点内嵌文档(docEntries + description)。纯查询,不创建独立文档。\n' +
65
+ '调用方式:search(query:"关键词") 全文搜索 | list 列出有文档的节点 | read(nodeId) 读取当前文档。\n' +
66
+ 'read(nodeId, history:true) 查看历史列表(标题+日期) | read(nodeId, index:N) 读取第N条历史详情(0=最新)。\n' +
67
+ 'actions: search(全文搜索)|list(文档列表)|read(读取文档,支持history/index参数)', {
68
+ action: z.enum(['search', 'list', 'read']).describe('操作类型 (Allowed: search, list, read)'),
69
+ query: z.string().optional().describe('搜索关键词 (search)'),
70
+ nodeId: z.string().optional().describe('节点ID (read)'),
71
+ chartId: z.string().optional().describe('限定流程图ID'),
72
+ history: z.boolean().optional().describe('read时传true返回历史列表(标题+日期)'),
73
+ index: z.number().optional().describe('read时指定历史条目序号(0=最新, 1=次新...)'),
74
+ limit: z.number().optional().describe('搜索结果限制, default 10'),
75
+ }, async (args) => safeTool(async () => {
76
+ const decoded = decodeObjectStrings(args);
77
+ switch (decoded.action) {
78
+ case 'search': {
79
+ const rawQuery = decoded.query?.trim();
80
+ if (!rawQuery)
81
+ return wrap('search requires query.');
82
+ const terms = rawQuery.toLowerCase().split(/\s+/).filter(Boolean);
83
+ const charts = decoded.chartId
84
+ ? [(await getClient().getFlowchart(decoded.chartId))].filter((c) => Boolean(c))
85
+ : await loadAllCharts();
86
+ const matches = charts
87
+ .flatMap((chart) => chart.nodes
88
+ .filter((n) => (n.docEntries && n.docEntries.length > 0) || n.description)
89
+ .map((node) => ({ chart, node, score: scoreDocMatch(node, terms) })))
90
+ .filter((m) => m.score > 0)
91
+ .sort((a, b) => b.score - a.score)
92
+ .slice(0, decoded.limit ?? 10);
93
+ if (matches.length === 0)
94
+ return wrap(`No docs matched: ${rawQuery}`);
95
+ const lines = [`Doc search: "${rawQuery}" (${matches.length} results)`, ''];
96
+ for (const { chart, node } of matches) {
97
+ const docCount = node.docEntries?.length ?? 0;
98
+ const latest = docCount > 0 ? node.docEntries[docCount - 1] : null;
99
+ lines.push(`- **${node.label}** [${chart.id}/${node.id}] history=${docCount}`);
100
+ if (latest)
101
+ lines.push(` current: ${shortDate(latest.date)} — ${latest.summary}`);
102
+ if (node.description)
103
+ lines.push(` desc: ${node.description.slice(0, 80)}${node.description.length > 80 ? '...' : ''}`);
104
+ }
105
+ return wrap(lines.join('\n'));
106
+ }
107
+ case 'list': {
108
+ const charts = decoded.chartId
109
+ ? [(await getClient().getFlowchart(decoded.chartId))].filter((c) => Boolean(c))
110
+ : await loadAllCharts();
111
+ const nodesWithDocs = charts.flatMap((chart) => chart.nodes
112
+ .filter((n) => (n.docEntries && n.docEntries.length > 0) || n.description)
113
+ .map((n) => ({ chart, node: n })));
114
+ if (nodesWithDocs.length === 0)
115
+ return wrap('No nodes with documentation found.');
116
+ const lines = [`Documented nodes: ${nodesWithDocs.length}`, ''];
117
+ for (const { chart, node } of nodesWithDocs) {
118
+ const docCount = node.docEntries?.length ?? 0;
119
+ const latest = docCount > 0 ? node.docEntries[docCount - 1] : null;
120
+ const latestInfo = latest ? ` | ${shortDate(latest.date)} ${latest.summary}` : '';
121
+ lines.push(`- ${node.label} [${chart.id}/${node.id}] history=${docCount}${latestInfo}`);
122
+ }
123
+ return wrap(lines.join('\n'));
124
+ }
125
+ case 'read': {
126
+ if (!decoded.nodeId)
127
+ return wrap('read requires nodeId.');
128
+ const chartId = decoded.chartId || 'main';
129
+ const chart = (await getClient().getFlowchart(chartId));
130
+ if (!chart)
131
+ return wrap(`Flowchart not found: ${chartId}`);
132
+ const node = chart.nodes.find((n) => n.id === decoded.nodeId);
133
+ if (!node)
134
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
135
+ const entries = node.docEntries ?? [];
136
+ // history=true → 列出历史记录(标题+日期)
137
+ if (decoded.history) {
138
+ if (entries.length === 0)
139
+ return wrap(`No history for ${node.label} [${chartId}/${decoded.nodeId}]`);
140
+ const lines = [`## ${node.label} — history (${entries.length})`, ''];
141
+ for (let i = entries.length - 1; i >= 0; i--) {
142
+ const e = entries[i];
143
+ const tag = i === entries.length - 1 ? ' (current)' : '';
144
+ lines.push(`${entries.length - 1 - i}. ${shortDate(e.date)} — ${e.summary}${tag}`);
145
+ }
146
+ lines.push('', '_Use read(nodeId, index:N) to view specific entry._');
147
+ return wrap(lines.join('\n'));
148
+ }
149
+ // index=N → 读取指定历史条目 (0=最新)
150
+ if (decoded.index !== undefined) {
151
+ const idx = entries.length - 1 - decoded.index;
152
+ if (idx < 0 || idx >= entries.length) {
153
+ return wrap(`Index out of range: ${decoded.index} (total: ${entries.length})`);
154
+ }
155
+ const entry = entries[idx];
156
+ const lines = [
157
+ `## ${node.label} — #${decoded.index}`,
158
+ `${shortDate(entry.date)} — ${entry.summary}`,
159
+ '',
160
+ entry.content,
161
+ ];
162
+ return wrap(lines.join('\n'));
163
+ }
164
+ // 默认: 返回当前文档 (description + 最新 docEntry)
165
+ const lines = [`## ${node.label} [${chartId}/${node.id}]`, ''];
166
+ if (node.description) {
167
+ lines.push(node.description, '');
168
+ }
169
+ if (entries.length > 0) {
170
+ const latest = entries[entries.length - 1];
171
+ lines.push(`---`, `**${latest.summary}** (${shortDate(latest.date)})`, '', latest.content);
172
+ if (entries.length > 1) {
173
+ lines.push('', `_${entries.length - 1} older entries. Use read(nodeId, history:true) to browse._`);
174
+ }
175
+ }
176
+ else if (!node.description) {
177
+ lines.push('_No documentation._');
178
+ }
179
+ return wrap(lines.join('\n'));
180
+ }
181
+ default:
182
+ return wrap(`Unknown action: ${String(decoded.action)}`);
183
+ }
184
+ }));
185
+ }
@@ -32,7 +32,6 @@ function ensureNodeShape(node) {
32
32
  boundDirs: ensureArray(node.boundDirs),
33
33
  boundTasks: ensureArray(node.boundTasks),
34
34
  docEntries: Array.isArray(node.docEntries) ? [...node.docEntries] : [],
35
- versions: Array.isArray(node.versions) ? [...node.versions] : [],
36
35
  };
37
36
  }
38
37
  function buildNode(input) {
@@ -52,7 +51,6 @@ function buildNode(input) {
52
51
  boundTasks: [],
53
52
  subFlowchart: input.subFlowchart || null,
54
53
  docEntries: [],
55
- versions: [{ version: 'v1.0', date: now, changes: 'initial' }],
56
54
  lastUpdated: now,
57
55
  lastQueried: '',
58
56
  };
@@ -81,27 +79,98 @@ function formatNodeLine(node) {
81
79
  function formatEdgeLine(edge) {
82
80
  const label = edge.label ? ` [${edge.label}]` : '';
83
81
  const type = edge.edgeType ? ` (${edge.edgeType})` : '';
84
- return `- ${edge.from} -> ${edge.to}${label}${type}`;
82
+ return `- ${edge.from} ${edge.to}${label}${type}`;
85
83
  }
84
+ /** 紧凑节点格式 (用于 get 视图) — 含内联 doc 摘要 */
85
+ function formatNodeCompact(node) {
86
+ const badges = [];
87
+ const fileCount = (node.boundFiles?.length ?? 0) + (node.boundDirs?.length ?? 0);
88
+ if (fileCount > 0)
89
+ badges.push(`files=${fileCount}`);
90
+ if (node.boundTasks?.length)
91
+ badges.push(`tasks=${node.boundTasks.length}`);
92
+ const docCount = node.docEntries?.length ?? 0;
93
+ if (docCount > 0)
94
+ badges.push(`docs=${docCount}`);
95
+ if (node.subFlowchart)
96
+ badges.push(`▶${node.subFlowchart}`);
97
+ const meta = `${node.nodeType ?? 'process'}/${node.domain ?? 'system'}`;
98
+ const suffix = badges.length > 0 ? ` [${badges.join(' ')}]` : '';
99
+ const desc = node.description ? `\n > ${node.description.slice(0, 100)}${node.description.length > 100 ? '…' : ''}` : '';
100
+ const latestDoc = docCount > 0 ? `\n doc: ${node.docEntries[docCount - 1].summary}` : '';
101
+ return `- **${node.label}** \`${node.id}\` (${meta})${suffix}${desc}${latestDoc}`;
102
+ }
103
+ /** 拓扑树 — DFS 从入度为0的节点开始, 展示调用结构 */
104
+ function buildTopologyTree(chart) {
105
+ const nodeMap = new Map(chart.nodes.map((n) => [n.id, n]));
106
+ const inDegree = new Map();
107
+ for (const n of chart.nodes)
108
+ inDegree.set(n.id, 0);
109
+ for (const e of chart.edges) {
110
+ inDegree.set(e.to, (inDegree.get(e.to) ?? 0) + 1);
111
+ }
112
+ // 按 from 分组出边
113
+ const outEdges = new Map();
114
+ for (const e of chart.edges) {
115
+ if (!outEdges.has(e.from))
116
+ outEdges.set(e.from, []);
117
+ outEdges.get(e.from).push(e);
118
+ }
119
+ const roots = chart.nodes.filter((n) => (inDegree.get(n.id) ?? 0) === 0);
120
+ if (roots.length === 0)
121
+ return ['(no root nodes — graph may contain cycles)'];
122
+ const lines = [];
123
+ const visited = new Set();
124
+ function dfs(nodeId, indent) {
125
+ if (visited.has(nodeId)) {
126
+ lines.push(`${' '.repeat(indent)}↺ ${nodeId} (cycle)`);
127
+ return;
128
+ }
129
+ visited.add(nodeId);
130
+ const node = nodeMap.get(nodeId);
131
+ const label = node ? node.label : nodeId;
132
+ lines.push(`${' '.repeat(indent)}${indent === 0 ? '◆' : '├'} ${label} [${nodeId}]`);
133
+ const children = outEdges.get(nodeId) ?? [];
134
+ for (const edge of children) {
135
+ const edgeLabel = edge.label ? ` —${edge.label}→ ` : ' → ';
136
+ if (visited.has(edge.to)) {
137
+ const target = nodeMap.get(edge.to);
138
+ lines.push(`${' '.repeat(indent + 1)}├${edgeLabel}↺ ${target?.label ?? edge.to}`);
139
+ }
140
+ else {
141
+ if (children.length > 1 || edge.label) {
142
+ lines.push(`${' '.repeat(indent + 1)}${edgeLabel.trim()}`);
143
+ }
144
+ dfs(edge.to, indent + 1);
145
+ }
146
+ }
147
+ }
148
+ for (const root of roots) {
149
+ dfs(root.id, 0);
150
+ }
151
+ // 未访问的孤立节点
152
+ const orphans = chart.nodes.filter((n) => !visited.has(n.id));
153
+ if (orphans.length > 0) {
154
+ lines.push('', '(orphans):');
155
+ for (const n of orphans) {
156
+ lines.push(` ○ ${n.label} [${n.id}]`);
157
+ }
158
+ }
159
+ return lines;
160
+ }
161
+ /** 封装文档更新:自动将上一条归档,追加新条目 */
86
162
  function appendDocEntry(node, summary, content) {
87
163
  const nextNode = ensureNodeShape(node);
88
164
  const entries = nextNode.docEntries ?? [];
89
- let nextVer = 0.1;
90
- if (entries.length > 0) {
91
- const last = entries[entries.length - 1]?.version ?? '';
92
- const parsed = Number.parseFloat(last.replace(/^v/, ''));
93
- if (!Number.isNaN(parsed)) {
94
- nextVer = Math.round((parsed + 0.1) * 10) / 10;
95
- }
96
- }
165
+ const version = `v${entries.length + 1}.0`;
97
166
  entries.push({
98
- version: `v${nextVer.toFixed(1)}`,
99
167
  date: new Date().toISOString(),
100
168
  summary: summary.replace(/\\n/g, '\n'),
101
169
  content: content.replace(/\\n/g, '\n'),
102
170
  });
103
171
  nextNode.docEntries = entries;
104
- return { node: nextNode, version: `v${nextVer.toFixed(1)}` };
172
+ nextNode.lastUpdated = new Date().toISOString();
173
+ return { node: nextNode, version };
105
174
  }
106
175
  function collectNeighborLayers(chart, startNodeId, maxDepth) {
107
176
  const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
@@ -295,8 +364,10 @@ export function registerFlowchartTools(server, _ctx) {
295
364
  return wrap('No flowcharts found.');
296
365
  }
297
366
  const lines = ['Flowcharts:', '', ...charts.map((chart) => {
298
- const parent = chart.parentChart ? ` parent=${chart.parentChart}` : '';
299
- return `- ${chart.title} [${chart.id}] ${chart.nodeCount} nodes / ${chart.edgeCount} edges${parent}`;
367
+ const parent = chart.parentChart
368
+ ? ` ${chart.parentChart}${chart.parentNode ? `/${chart.parentNode}` : ''}`
369
+ : '';
370
+ return `- ${chart.title} [${chart.id}] ${chart.nodeCount}n/${chart.edgeCount}e${parent}`;
300
371
  })];
301
372
  return wrap(lines.join('\n'));
302
373
  }
@@ -307,16 +378,46 @@ export function registerFlowchartTools(server, _ctx) {
307
378
  return wrap(`Flowchart not found: ${chartId}`);
308
379
  }
309
380
  const lines = [
310
- `Flowchart: ${chart.title} [${chart.id}]`,
311
- chart.parentChart ? `Parent chart: ${chart.parentChart}` : '',
312
- chart.parentNode ? `Parent node: ${chart.parentNode}` : '',
313
- '',
314
- `Nodes (${chart.nodes.length}):`,
315
- ...chart.nodes.map(formatNodeLine),
316
- '',
317
- `Edges (${chart.edges.length}):`,
318
- ...chart.edges.map(formatEdgeLine),
319
- ].filter((line) => line.length > 0);
381
+ `# ${chart.title} [${chart.id}]`,
382
+ ];
383
+ if (chart.parentChart) {
384
+ lines.push(`parent: ${chart.parentChart}${chart.parentNode ? `/${chart.parentNode}` : ''}`);
385
+ }
386
+ // Mermaid 流程图
387
+ lines.push('', '```mermaid', 'graph TD');
388
+ // 节点声明
389
+ for (const n of chart.nodes) {
390
+ const safeLabel = n.label.replace(/"/g, '#quot;');
391
+ lines.push(` ${n.id}["${safeLabel}"]`);
392
+ }
393
+ // 边
394
+ for (const e of chart.edges) {
395
+ const arrow = e.label ? `-->|${e.label}|` : '-->';
396
+ lines.push(` ${e.from} ${arrow} ${e.to}`);
397
+ }
398
+ lines.push('```');
399
+ // 节点简介表 (MD table)
400
+ lines.push('', `## Nodes (${chart.nodes.length})`, '');
401
+ lines.push('| 节点 | 类型 | 简介 | 绑定文件 |');
402
+ lines.push('|:-----|:-----|:-----|:---------|');
403
+ for (const n of chart.nodes) {
404
+ const nameCol = n.subFlowchart
405
+ ? `**${n.label}** \`${n.id}\` ▶${n.subFlowchart}`
406
+ : `**${n.label}** \`${n.id}\``;
407
+ const typeCol = `${n.nodeType ?? 'process'}/${n.domain ?? 'system'}`;
408
+ const descCol = n.description
409
+ ? n.description.slice(0, 80).replace(/[\|\n]/g, ' ') + (n.description.length > 80 ? '…' : '')
410
+ : '';
411
+ const fileNames = [];
412
+ for (const f of (n.boundFiles ?? [])) {
413
+ fileNames.push(f.replace(/\\/g, '/').split('/').pop() ?? f);
414
+ }
415
+ for (const d of (n.boundDirs ?? [])) {
416
+ fileNames.push(d.replace(/\\/g, '/').split('/').pop() + '/');
417
+ }
418
+ const fileCol = fileNames.length > 0 ? fileNames.join(', ') : '-';
419
+ lines.push(`| ${nameCol} | ${typeCol} | ${descCol} | ${fileCol} |`);
420
+ }
320
421
  return wrap(lines.join('\n'));
321
422
  }
322
423
  case 'search': {
@@ -463,63 +564,81 @@ export function registerFlowchartTools(server, _ctx) {
463
564
  const showTasks = decoded.includeTasks !== false;
464
565
  const showFiles = decoded.includeFiles !== false;
465
566
  const showDoc = decoded.includeDoc !== false;
567
+ const relations = collectNodeRelations(chart, node.id);
466
568
  const lines = [
467
- `Node: ${node.label} [${node.id}]`,
468
- `Type: ${node.nodeType ?? 'process'} | Domain: ${node.domain ?? 'system'}`,
569
+ `## ${node.label} \`${node.id}\``,
570
+ `${node.nodeType ?? 'process'} / ${node.domain ?? 'system'}`,
469
571
  ];
470
572
  if (node.description) {
471
- lines.push(`Description: ${node.description}`);
573
+ lines.push('', '### Description');
574
+ lines.push(...node.description.split('\n').map((l) => `> ${l}`));
472
575
  }
473
576
  if (node.input && node.input.length > 0) {
474
- lines.push(`Input: ${node.input.join(', ')}`);
577
+ lines.push(`\nInput: ${node.input.join(', ')}`);
475
578
  }
476
579
  if (node.output && node.output.length > 0) {
477
580
  lines.push(`Output: ${node.output.join(', ')}`);
478
581
  }
479
582
  if (node.subFlowchart) {
480
- lines.push(`Sub flowchart: ${node.subFlowchart}`);
583
+ lines.push(`Sub-chart: ${node.subFlowchart}`);
481
584
  }
482
585
  if (node.lastUpdated) {
483
- lines.push(`Last updated: ${node.lastUpdated}`);
484
- }
485
- if (node.lastQueried) {
486
- lines.push(`Last queried: ${node.lastQueried}`);
487
- }
488
- lines.push('');
489
- if (showDoc && node.docEntries && node.docEntries.length > 0) {
490
- lines.push(`Node docs (${node.docEntries.length}):`);
491
- const showAll = decoded.fullDoc === true;
492
- node.docEntries.forEach((entry, index) => {
493
- const isLatest = index === node.docEntries.length - 1;
494
- if (isLatest || showAll) {
495
- lines.push(`- ${entry.version} ${entry.date}${isLatest ? ' latest' : ''}`);
496
- lines.push(` summary: ${entry.summary}`);
497
- lines.push(...entry.content.split('\n').map((line) => ` ${line}`));
498
- }
499
- else {
500
- lines.push(`- ${entry.version} ${entry.date}: ${entry.summary}`);
501
- }
502
- });
503
- lines.push('');
586
+ lines.push(`Updated: ${node.lastUpdated}`);
587
+ }
588
+ // 关系: →← 方向箭头
589
+ if (relations.incoming.length > 0 || relations.outgoing.length > 0) {
590
+ lines.push('', '### Relations');
591
+ for (const { edge, node: src } of relations.incoming) {
592
+ const lbl = edge.label ? ` [${edge.label}]` : '';
593
+ const typ = edge.edgeType ? ` (${edge.edgeType})` : '';
594
+ lines.push(`← ${src?.label ?? edge.from} \`${edge.from}\`${lbl}${typ}`);
595
+ }
596
+ for (const { edge, node: tgt } of relations.outgoing) {
597
+ const lbl = edge.label ? ` [${edge.label}]` : '';
598
+ const typ = edge.edgeType ? ` (${edge.edgeType})` : '';
599
+ lines.push(`→ ${tgt?.label ?? edge.to} \`${edge.to}\`${lbl}${typ}`);
600
+ }
504
601
  }
602
+ // 邻居展开
505
603
  if (expandDepth > 0) {
506
604
  const layers = collectNeighborLayers(chart, node.id, expandDepth);
507
605
  if (layers.length > 0) {
508
- lines.push(`Linked nodes (${layers.length} layers):`);
606
+ lines.push('', '### Neighbors');
509
607
  layers.forEach((layer, index) => {
510
608
  lines.push(`L${index + 1}:`);
511
609
  lines.push(...layer.map((item) => ` ${item}`));
512
610
  });
513
- lines.push('');
611
+ }
612
+ }
613
+ // 内嵌文档
614
+ if (showDoc && node.docEntries && node.docEntries.length > 0) {
615
+ lines.push('', `### Docs (${node.docEntries.length})`);
616
+ const showAll = decoded.fullDoc === true;
617
+ const latest = node.docEntries[node.docEntries.length - 1];
618
+ // 当前文档 (最新)
619
+ lines.push(`**${latest.summary}** (${latest.date.slice(0, 10)})`);
620
+ lines.push(latest.content);
621
+ // 历史记录
622
+ if (node.docEntries.length > 1) {
623
+ lines.push('', `History (${node.docEntries.length - 1}):`);
624
+ for (let i = node.docEntries.length - 2; i >= 0; i--) {
625
+ const entry = node.docEntries[i];
626
+ if (showAll) {
627
+ lines.push(`- ${entry.date.slice(0, 10)}: **${entry.summary}**`);
628
+ lines.push(entry.content);
629
+ }
630
+ else {
631
+ lines.push(`- ${entry.date.slice(0, 10)}: ${entry.summary}`);
632
+ }
633
+ }
514
634
  }
515
635
  }
516
636
  if (showDocs && node.boundDocs && node.boundDocs.length > 0) {
517
- lines.push(`Bound docs (${node.boundDocs.length}):`);
637
+ lines.push('', `### Bound docs (${node.boundDocs.length})`);
518
638
  node.boundDocs.forEach((docPath) => lines.push(`- ${docPath}`));
519
- lines.push('');
520
639
  }
521
640
  if (showTasks && node.boundTasks && node.boundTasks.length > 0) {
522
- lines.push(`Bound tasks (${node.boundTasks.length}):`);
641
+ lines.push('', `### Tasks (${node.boundTasks.length})`);
523
642
  for (const taskId of node.boundTasks) {
524
643
  try {
525
644
  const task = (await client().getTask(taskId));
@@ -534,24 +653,17 @@ export function registerFlowchartTools(server, _ctx) {
534
653
  lines.push(`- [${taskId}]`);
535
654
  }
536
655
  }
537
- lines.push('');
538
656
  }
539
657
  if (showFiles) {
540
658
  const files = node.boundFiles ?? [];
541
659
  const dirs = node.boundDirs ?? [];
542
660
  if (files.length > 0 || dirs.length > 0) {
543
- lines.push(`Bound code (${files.length} files / ${dirs.length} dirs):`);
544
- files.forEach((filePath) => lines.push(`- file: ${filePath}`));
545
- dirs.forEach((dirPath) => lines.push(`- dir: ${dirPath}`));
546
- lines.push('');
661
+ lines.push('', `### Code (${files.length} files / ${dirs.length} dirs)`);
662
+ files.forEach((filePath) => lines.push(`- ${filePath}`));
663
+ dirs.forEach((dirPath) => lines.push(`- ${dirPath}/`));
547
664
  }
548
665
  }
549
- if (node.versions && node.versions.length > 0) {
550
- lines.push(`Versions (${node.versions.length}):`);
551
- node.versions.slice(-5).forEach((version) => {
552
- lines.push(`- ${version.version} ${version.date}: ${version.changes}`);
553
- });
554
- }
666
+ // Versions 已由 docEntries 历史替代,不再输出
555
667
  return wrap(lines.join('\n').trim());
556
668
  }
557
669
  case 'update_node': {
@@ -574,6 +686,12 @@ export function registerFlowchartTools(server, _ctx) {
574
686
  changes.push('label');
575
687
  }
576
688
  if (decoded.description !== undefined) {
689
+ // 自动归档旧 description 到 docEntries
690
+ if (nextNode.description && nextNode.description !== decoded.description) {
691
+ const archiveResult = appendDocEntry(nextNode, '[auto] description更新前快照', nextNode.description);
692
+ nextNode = archiveResult.node;
693
+ changes.push(`docArchive(${archiveResult.version})`);
694
+ }
577
695
  nextNode.description = decoded.description;
578
696
  changes.push('description');
579
697
  }
@@ -1,14 +1,15 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 13 个工具, 7 个子模块
3
+ * 14 个工具, 8 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
7
  * 📚 知识: kg_projects, kg_workflow (2个)
8
8
  * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
9
9
  * 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
10
+ * 📘 文档查询: kg_doc(节点文档搜索/阅读/历史) (1个)
10
11
  * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
11
12
  * 🏛️ 协作: kg_meeting (1个)
12
13
  */
13
14
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
14
- export declare function registerTools(server: McpServer, projectId: string, user: string, onProjectChange?: (newProjectId: string, newApiUrl: string) => void): void;
15
+ export declare function registerTools(server: McpServer, projectId: string, user: string, agentId: string, onProjectChange?: (newProjectId: string, newApiUrl: string) => void): void;
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * MCP 工具注册入口
3
- * 13 个工具, 7 个子模块
3
+ * 14 个工具, 8 个子模块
4
4
  *
5
5
  * 🔗 初始化: kg_init (1个)
6
6
  * 📊 导航: kg_status (1个)
7
7
  * 📚 知识: kg_projects, kg_workflow (2个)
8
8
  * 📝 工作流: kg_task(任务记录), kg_files, kg_discuss(讨论区), kg_ref (4个)
9
9
  * 🔀 关系核心: kg_flowchart(逻辑流程图 — 关系型知识锚点) (1个)
10
+ * 📘 文档查询: kg_doc(节点文档搜索/阅读/历史) (1个)
10
11
  * 🔬 代码分析: code_scan, code_smart_context, code_full_path (3个)
11
12
  * 🏛️ 协作: kg_meeting (1个)
12
13
  */
@@ -22,8 +23,9 @@ import { registerReferenceTools } from './refs.js';
22
23
  import { registerAnalyzerTools } from './analyzer.js';
23
24
  import { registerMeetingTools } from './meeting.js';
24
25
  import { registerFlowchartTools } from './flowchart.js';
25
- export function registerTools(server, projectId, user, onProjectChange) {
26
- const ctx = createContext(projectId, user);
26
+ import { registerDocQueryTools } from './doc_query.js';
27
+ export function registerTools(server, projectId, user, agentId, onProjectChange) {
28
+ const ctx = createContext(projectId, user, agentId);
27
29
  // 🔗 初始化
28
30
  registerInitTool(server, ctx, onProjectChange || (() => { }));
29
31
  // 📊 导航
@@ -42,4 +44,6 @@ export function registerTools(server, projectId, user, onProjectChange) {
42
44
  registerMeetingTools(server, ctx);
43
45
  // 🔀 流程图
44
46
  registerFlowchartTools(server, ctx);
47
+ // 📘 文档查询
48
+ registerDocQueryTools(server, ctx);
45
49
  }
@@ -60,6 +60,7 @@ export function registerInitTool(server, ctx, onProjectChange) {
60
60
  // 4. 更新共享上下文 — 所有工具立刻获得新 projectId
61
61
  ctx.projectId = config.projectId;
62
62
  ctx.user = config.user;
63
+ ctx.agentId = process.env.PPDOCS_AGENT_ID || 'default';
63
64
  // 5. 通知主进程
64
65
  onProjectChange(config.projectId, config.apiUrl);
65
66
  return wrap(`✅ 项目上下文已初始化\n\n` +
@@ -6,9 +6,10 @@
6
6
  export interface McpContext {
7
7
  projectId: string;
8
8
  user: string;
9
+ agentId: string;
9
10
  }
10
11
  /** 创建可变上下文对象 (工具闭包捕获此对象的引用,kg_init 更新其属性) */
11
- export declare function createContext(projectId: string, user: string): McpContext;
12
+ export declare function createContext(projectId: string, user: string, agentId?: string): McpContext;
12
13
  export declare function wrap(text: string): {
13
14
  content: {
14
15
  type: "text";
@@ -3,8 +3,8 @@
3
3
  * 合并原 helpers.ts + 新增 safeTool / withCross 模式
4
4
  */
5
5
  /** 创建可变上下文对象 (工具闭包捕获此对象的引用,kg_init 更新其属性) */
6
- export function createContext(projectId, user) {
7
- return { projectId, user };
6
+ export function createContext(projectId, user, agentId = 'default') {
7
+ return { projectId, user, agentId };
8
8
  }
9
9
  // ==================== MCP 返回包装 ====================
10
10
  export function wrap(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",