@ppdocs/mcp 3.7.1 → 3.9.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/cli.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * 命令: init - 初始化项目配置 + 安装工作流模板 + 自动注册 MCP
4
4
  */
5
5
  import * as fs from 'fs';
6
+ import * as os from 'os';
6
7
  import * as path from 'path';
7
8
  import { fileURLToPath } from 'url';
8
9
  import { execSync } from 'child_process';
@@ -137,7 +138,7 @@ export async function runCli(args) {
137
138
  /** 轻量绑定: 仅创建 .ppdocs, 不注册 MCP 不安装模板 */
138
139
  function bindProject(opts) {
139
140
  writePpdocsConfig(process.cwd(), opts);
140
- console.log(`\n🔗 Project bound: ${opts.project}\n AI will auto-connect via kg_init when opening this directory.\n`);
141
+ console.log(`\n🔗 Project bound: ${opts.project}\n AI will auto-connect when opening this directory.\n`);
141
142
  }
142
143
  /** 写入 .ppdocs 配置文件 (init + bind 复用) */
143
144
  function writePpdocsConfig(cwd, opts) {
@@ -199,7 +200,7 @@ async function initProject(opts) {
199
200
  }
200
201
  }
201
202
  // 自动检测并注册 MCP (如果已写入 Antigravity 配置,跳过 gemini CLI 注册避免冲突)
202
- const registered = autoRegisterMcp(detectedIdes.includes('antigravity'));
203
+ const registered = autoRegisterMcp(cwd, apiUrl, opts.user || generateUser(), detectedIdes.includes('antigravity'));
203
204
  // 如果没有检测到任何 AI CLI,并且也没有检测到配置文件夹,创建 .mcp.json 作为备用
204
205
  if (!registered && !hasIdeDir) {
205
206
  createMcpJson(cwd, apiUrl, opts.user);
@@ -254,27 +255,29 @@ function execSilent(cmd) {
254
255
  }
255
256
  catch { /* ignore */ }
256
257
  }
257
- /** 自动检测 AI CLI 并注册全局 MCP (不注入 env,支持多项目通过 kg_init 切换) */
258
- function autoRegisterMcp(skipGemini = false) {
258
+ /** 自动检测 AI CLI 并注册全局 MCP,直接注入项目环境变量实现开箱即用 */
259
+ function autoRegisterMcp(cwd, apiUrl, user, skipGemini = false) {
259
260
  const detected = [];
260
261
  const serverName = 'ppdocs-kg';
261
262
  const addCmd = `-- npx -y @ppdocs/mcp@latest`;
262
- // Claude CLI (幂等:已注册则跳过)
263
+ const envPairs = [
264
+ `PPDOCS_API_URL=${apiUrl}`,
265
+ `PPDOCS_USER=${user}`,
266
+ `PPDOCS_PROJECT_ROOT=${cwd}`,
267
+ ];
268
+ const claudeEnvArgs = envPairs.map((pair) => `-e ${JSON.stringify(pair)}`).join(' ');
269
+ const codexEnvArgs = envPairs.map((pair) => `--env ${JSON.stringify(pair)}`).join(' ');
270
+ // Claude CLI
263
271
  if (commandExists('claude')) {
264
272
  detected.push('Claude');
265
273
  try {
266
- const list = execSync(`claude mcp list`, { encoding: 'utf-8' });
267
- if (list.includes(serverName)) {
268
- console.log(`✅ Claude MCP already configured`);
269
- }
270
- else {
271
- execSync(`claude mcp add ${serverName} ${addCmd}`, { stdio: 'inherit' });
272
- }
274
+ execSilent(`claude mcp remove ${serverName}`);
275
+ execSync(`claude mcp add ${claudeEnvArgs} ${serverName} ${addCmd}`, { stdio: 'inherit' });
273
276
  }
274
277
  catch {
275
278
  try {
276
279
  execSilent(`claude mcp remove ${serverName}`);
277
- execSync(`claude mcp add ${serverName} ${addCmd}`, { stdio: 'inherit' });
280
+ execSync(`claude mcp add ${claudeEnvArgs} ${serverName} ${addCmd}`, { stdio: 'inherit' });
278
281
  }
279
282
  catch {
280
283
  console.log(`⚠️ Claude MCP registration failed`);
@@ -286,18 +289,18 @@ function autoRegisterMcp(skipGemini = false) {
286
289
  detected.push('Codex');
287
290
  try {
288
291
  execSilent(`codex mcp remove ${serverName}`);
289
- execSync(`codex mcp add ${serverName} ${addCmd}`, { stdio: 'inherit' });
292
+ execSync(`codex mcp add ${codexEnvArgs} ${serverName} ${addCmd}`, { stdio: 'inherit' });
290
293
  }
291
294
  catch {
292
295
  console.log(`⚠️ Codex MCP registration failed`);
293
296
  }
294
297
  }
295
- // Gemini CLI
298
+ // Gemini CLI (gemini mcp add 不支持 --env,直接写 settings.json)
296
299
  if (commandExists('gemini') && !skipGemini) {
297
300
  detected.push('Gemini');
298
301
  try {
299
- execSilent(`gemini mcp remove ${serverName}`);
300
- execSync(`gemini mcp add ${serverName} "npx -y @ppdocs/mcp@latest"`, { stdio: 'inherit' });
302
+ const geminiSettings = path.join(os.homedir(), '.gemini', 'settings.json');
303
+ createMcpConfigAt(geminiSettings, apiUrl, { user, projectRoot: cwd });
301
304
  }
302
305
  catch {
303
306
  console.log(`⚠️ Gemini MCP registration failed`);
@@ -310,7 +313,7 @@ function autoRegisterMcp(skipGemini = false) {
310
313
  console.log(`✅ Registered MCP for: ${detected.join(', ')}`);
311
314
  return true;
312
315
  }
313
- /** 在指定路径创建或更新 mcp.json 配置,注入 PPDOCS_API_URL 环境变量实现启动即就绪 */
316
+ /** 在指定路径创建或更新 mcp.json 配置,注入 PPDOCS_* 环境变量实现启动即就绪 */
314
317
  function createMcpConfigAt(mcpPath, apiUrl, options) {
315
318
  let mcpConfig = {};
316
319
  if (fs.existsSync(mcpPath)) {
@@ -333,6 +336,8 @@ function createMcpConfigAt(mcpPath, apiUrl, options) {
333
336
  env.PPDOCS_API_URL = apiUrl;
334
337
  if (options?.user)
335
338
  env.PPDOCS_USER = options.user;
339
+ if (options?.projectRoot)
340
+ env.PPDOCS_PROJECT_ROOT = options.projectRoot;
336
341
  }
337
342
  const ppdocsServer = isWindows
338
343
  ? { command: 'cmd', args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'], ...(Object.keys(env).length > 0 && { env }) }
@@ -351,7 +356,7 @@ function createMcpConfigAt(mcpPath, apiUrl, options) {
351
356
  }
352
357
  /** 创建 .mcp.json (手动配置备用) */
353
358
  function createMcpJson(cwd, apiUrl, user) {
354
- createMcpConfigAt(path.join(cwd, '.mcp.json'), apiUrl, { user });
359
+ createMcpConfigAt(path.join(cwd, '.mcp.json'), apiUrl, { user, projectRoot: cwd });
355
360
  }
356
361
  /** 递归复制目录 */
357
362
  function copyDirRecursive(src, dest) {
@@ -400,6 +405,8 @@ function generateMcpPermissions() {
400
405
  'kg_ref',
401
406
  // 流程图
402
407
  'kg_flowchart',
408
+ // 文档查询
409
+ 'kg_doc',
403
410
  // 协作
404
411
  'kg_meeting',
405
412
  // 代码分析
@@ -485,7 +492,7 @@ function installGlobalRules(cwd, filename) {
485
492
  }
486
493
  /** 安装 Cursor 模板 */
487
494
  function installCursorTemplates(cwd, apiUrl, user) {
488
- createMcpConfigAt(path.join(cwd, '.cursor', 'mcp.json'), apiUrl, { user });
495
+ createMcpConfigAt(path.join(cwd, '.cursor', 'mcp.json'), apiUrl, { user, projectRoot: cwd });
489
496
  // 2. 生成 .cursor/rules/ppdocs.md (官方标准, .cursorrules 已废弃)
490
497
  const cursorRulesDir = path.join(cwd, '.cursor', 'rules');
491
498
  if (!fs.existsSync(cursorRulesDir)) {
@@ -578,7 +585,7 @@ function installWorkflows(cwd, extraDirs = []) {
578
585
  /** 安装 Antigravity (Gemini IDE) 模板 */
579
586
  function installAntigravityTemplates(cwd, apiUrl, user) {
580
587
  // Antigravity 由 gemini CLI 注册 env,文件配置不注入避免冲突
581
- createMcpConfigAt(path.join(cwd, '.gemini', 'settings.json'), apiUrl, { noEnv: true, user });
588
+ createMcpConfigAt(path.join(cwd, '.gemini', 'settings.json'), apiUrl, { noEnv: true, user, projectRoot: cwd });
582
589
  // 2. 生成 GEMINI.md 全局规则 (Gemini CLI 官方标准)
583
590
  installGlobalRules(cwd, 'GEMINI.md');
584
591
  // 3. Antigravity 全局 workflows 目录 (额外安装目标)
@@ -592,7 +599,7 @@ function installAntigravityTemplates(cwd, apiUrl, user) {
592
599
  }
593
600
  /** 安装 Kiro 模板 */
594
601
  function installKiroTemplates(cwd, apiUrl, user) {
595
- createMcpConfigAt(path.join(cwd, '.kiro', 'settings', 'mcp.json'), apiUrl, { user });
602
+ createMcpConfigAt(path.join(cwd, '.kiro', 'settings', 'mcp.json'), apiUrl, { user, projectRoot: cwd });
596
603
  // 2. 生成 Kiro Steering 文件 (官方标准: .kiro/steering/ 而非 rules/)
597
604
  const steeringDir = path.join(cwd, '.kiro', 'steering');
598
605
  if (!fs.existsSync(steeringDir)) {
package/dist/config.js CHANGED
@@ -24,9 +24,26 @@ function readEnvConfig() {
24
24
  source: 'env',
25
25
  };
26
26
  }
27
+ function findPpdocsFile(startDir = process.cwd()) {
28
+ let currentDir = path.resolve(startDir);
29
+ while (true) {
30
+ const configPath = path.join(currentDir, PPDOCS_CONFIG_FILE);
31
+ if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) {
32
+ return configPath;
33
+ }
34
+ const parentDir = path.dirname(currentDir);
35
+ if (parentDir === currentDir) {
36
+ return null;
37
+ }
38
+ currentDir = parentDir;
39
+ }
40
+ }
27
41
  function readPpdocsFile() {
28
- const configPath = path.join(process.cwd(), '.ppdocs');
29
- if (!fs.existsSync(configPath))
42
+ const hintedRoot = process.env.PPDOCS_PROJECT_ROOT;
43
+ const configPath = hintedRoot
44
+ ? path.join(path.resolve(hintedRoot), PPDOCS_CONFIG_FILE)
45
+ : findPpdocsFile();
46
+ if (!configPath || !fs.existsSync(configPath))
30
47
  return null;
31
48
  try {
32
49
  const c = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
@@ -9,6 +9,17 @@
9
9
  function cleanPath(p) {
10
10
  return p.replace(/^\//, '');
11
11
  }
12
+ async function readErrorMessage(response) {
13
+ const statusLine = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''}`;
14
+ const contentType = response.headers.get('content-type') || '';
15
+ if (contentType.includes('application/json')) {
16
+ const payload = await response.json().catch(() => null);
17
+ const detail = payload?.error || payload?.message;
18
+ return detail ? `${statusLine}: ${detail}` : statusLine;
19
+ }
20
+ const text = (await response.text().catch(() => '')).trim();
21
+ return text ? `${statusLine}: ${text}` : statusLine;
22
+ }
12
23
  /** 下载 zip 并解压到指定目录或 temp 目录 */
13
24
  async function fetchAndExtractZip(url, localPath) {
14
25
  const controller = new AbortController();
@@ -16,8 +27,7 @@ async function fetchAndExtractZip(url, localPath) {
16
27
  try {
17
28
  const response = await fetch(url, { signal: controller.signal });
18
29
  if (!response.ok) {
19
- const error = await response.json().catch(() => ({ error: response.statusText }));
20
- throw new Error(error.error || `HTTP ${response.status}`);
30
+ throw new Error(await readErrorMessage(response));
21
31
  }
22
32
  const buffer = Buffer.from(await response.arrayBuffer());
23
33
  const os = await import('os');
@@ -75,8 +85,7 @@ export class PpdocsApiClient {
75
85
  }
76
86
  });
77
87
  if (!response.ok) {
78
- const error = await response.json().catch(() => ({ error: response.statusText }));
79
- throw new Error(error.error || `HTTP ${response.status}`);
88
+ throw new Error(await readErrorMessage(response));
80
89
  }
81
90
  const json = await response.json();
82
91
  if (!json.success) {
@@ -250,7 +259,7 @@ export class PpdocsApiClient {
250
259
  async updateFlowchartNode(chartId, nodeId, node, changeDesc) {
251
260
  return this.request(`/flowcharts/${chartId}/nodes/${nodeId}`, {
252
261
  method: 'PUT',
253
- body: JSON.stringify({ node, change_desc: changeDesc || 'MCP update' })
262
+ body: JSON.stringify({ node, changeDesc: changeDesc || 'MCP update' })
254
263
  });
255
264
  }
256
265
  /** 原子删除单个节点 (后端自动清理关联边) */
@@ -469,9 +478,10 @@ export function initClient(apiUrl) {
469
478
  }
470
479
  export function getClient() {
471
480
  if (!client) {
472
- throw new Error('❌ 项目未初始化!请先调用 kg_init 工具绑定项目上下文。\n\n' +
473
- '用法: kg_init({ projectPath: "项目根目录绝对路径" })\n' +
474
- '该目录下需存在 .ppdocs 配置文件。');
481
+ throw new Error('❌ MCP 未连接!请检查:\n' +
482
+ '1. 环境变量 PPDOCS_API_URL 是否已设置\n' +
483
+ '2. 或当前目录下是否存在 .ppdocs 配置文件\n\n' +
484
+ '重新运行: npx @ppdocs/mcp init -p <projectId> -k <key>');
475
485
  }
476
486
  return client;
477
487
  }
@@ -92,13 +92,17 @@ export function registerDocQueryTools(server, _ctx) {
92
92
  .slice(0, decoded.limit ?? 10);
93
93
  if (matches.length === 0)
94
94
  return wrap(`No docs matched: ${rawQuery}`);
95
- const rows = matches.map(({ chart, node }) => {
95
+ const lines = [`Doc search: "${rawQuery}" (${matches.length} results)`, ''];
96
+ for (const { chart, node } of matches) {
96
97
  const docCount = node.docEntries?.length ?? 0;
97
98
  const latest = docCount > 0 ? node.docEntries[docCount - 1] : null;
98
- const summary = latest ? latest.summary.substring(0, 50) : (node.description?.substring(0, 50) || '');
99
- return `| ${node.label} | ${chart.id}/${node.id} | ${docCount} | ${latest ? shortDate(latest.date).slice(5) : '-'} | ${summary} |`;
100
- }).join('\n');
101
- return wrap(`Doc search: "${rawQuery}" (${matches.length} results)\n\n| 节点 | 图谱 | 文档数 | 日期 | 摘要 |\n|:-----|:-----|:-------|:-----|:-----|\n${rows}`);
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'));
102
106
  }
103
107
  case 'list': {
104
108
  const charts = decoded.chartId
@@ -109,43 +113,30 @@ export function registerDocQueryTools(server, _ctx) {
109
113
  .map((n) => ({ chart, node: n })));
110
114
  if (nodesWithDocs.length === 0)
111
115
  return wrap('No nodes with documentation found.');
112
- const rows = nodesWithDocs.map(({ chart, node }) => {
116
+ const lines = [`Documented nodes: ${nodesWithDocs.length}`, ''];
117
+ for (const { chart, node } of nodesWithDocs) {
113
118
  const docCount = node.docEntries?.length ?? 0;
114
119
  const latest = docCount > 0 ? node.docEntries[docCount - 1] : null;
115
- const summary = latest ? latest.summary.substring(0, 50) : '';
116
- return `| ${node.label} | ${chart.id}/${node.id} | ${docCount} | ${latest ? shortDate(latest.date).slice(5) : '-'} | ${summary} |`;
117
- }).join('\n');
118
- return wrap(`Documented nodes: ${nodesWithDocs.length}\n\n| 节点 | 图谱 | 文档数 | 日期 | 摘要 |\n|:-----|:-----|:-------|:-----|:-----|\n${rows}`);
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'));
119
124
  }
120
125
  case 'read': {
121
126
  if (!decoded.nodeId)
122
127
  return wrap('read requires nodeId.');
123
- let chart = null;
124
- let foundChartId = decoded.chartId || '';
125
- if (foundChartId) {
126
- chart = (await getClient().getFlowchart(foundChartId));
127
- }
128
- else {
129
- // 不指定 chartId 时自动搜全部图
130
- const all = await loadAllCharts();
131
- for (const c of all) {
132
- if (c.nodes.some(n => n.id === decoded.nodeId)) {
133
- chart = c;
134
- foundChartId = c.id;
135
- break;
136
- }
137
- }
138
- }
128
+ const chartId = decoded.chartId || 'main';
129
+ const chart = (await getClient().getFlowchart(chartId));
139
130
  if (!chart)
140
- return wrap(`Node not found: ${decoded.nodeId}${decoded.chartId ? ` in ${decoded.chartId}` : ' (searched all charts)'}`);
131
+ return wrap(`Flowchart not found: ${chartId}`);
141
132
  const node = chart.nodes.find((n) => n.id === decoded.nodeId);
142
133
  if (!node)
143
- return wrap(`Node not found: ${decoded.nodeId} in ${foundChartId}`);
134
+ return wrap(`Node not found: ${decoded.nodeId} in ${chartId}`);
144
135
  const entries = node.docEntries ?? [];
145
136
  // history=true → 列出历史记录(标题+日期)
146
137
  if (decoded.history) {
147
138
  if (entries.length === 0)
148
- return wrap(`No history for ${node.label} [${foundChartId}/${decoded.nodeId}]`);
139
+ return wrap(`No history for ${node.label} [${chartId}/${decoded.nodeId}]`);
149
140
  const lines = [`## ${node.label} — history (${entries.length})`, ''];
150
141
  for (let i = entries.length - 1; i >= 0; i--) {
151
142
  const e = entries[i];
@@ -171,7 +162,7 @@ export function registerDocQueryTools(server, _ctx) {
171
162
  return wrap(lines.join('\n'));
172
163
  }
173
164
  // 默认: 返回当前文档 (description + 最新 docEntry)
174
- const lines = [`## ${node.label} [${foundChartId}/${node.id}]`, ''];
165
+ const lines = [`## ${node.label} [${chartId}/${node.id}]`, ''];
175
166
  if (node.description) {
176
167
  lines.push(node.description, '');
177
168
  }
@@ -162,7 +162,6 @@ function buildTopologyTree(chart) {
162
162
  function appendDocEntry(node, summary, content) {
163
163
  const nextNode = ensureNodeShape(node);
164
164
  const entries = nextNode.docEntries ?? [];
165
- const version = `v${entries.length + 1}.0`;
166
165
  entries.push({
167
166
  date: new Date().toISOString(),
168
167
  summary: summary.replace(/\\n/g, '\n'),
@@ -170,7 +169,7 @@ function appendDocEntry(node, summary, content) {
170
169
  });
171
170
  nextNode.docEntries = entries;
172
171
  nextNode.lastUpdated = new Date().toISOString();
173
- return { node: nextNode, version };
172
+ return nextNode;
174
173
  }
175
174
  function collectNeighborLayers(chart, startNodeId, maxDepth) {
176
175
  const nodeMap = new Map(chart.nodes.map((node) => [node.id, node]));
@@ -364,10 +363,8 @@ export function registerFlowchartTools(server, _ctx) {
364
363
  return wrap('No flowcharts found.');
365
364
  }
366
365
  const lines = ['Flowcharts:', '', ...charts.map((chart) => {
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}`;
366
+ const parent = chart.parentChart ? ` parent=${chart.parentChart}` : '';
367
+ return `- ${chart.title} [${chart.id}] ${chart.nodeCount} nodes / ${chart.edgeCount} edges${parent}`;
371
368
  })];
372
369
  return wrap(lines.join('\n'));
373
370
  }
@@ -396,27 +393,26 @@ export function registerFlowchartTools(server, _ctx) {
396
393
  lines.push(` ${e.from} ${arrow} ${e.to}`);
397
394
  }
398
395
  lines.push('```');
399
- // 节点简介表 (MD table)
396
+ // 节点简介表
400
397
  lines.push('', `## Nodes (${chart.nodes.length})`, '');
401
- lines.push('| 节点 | 类型 | 简介 | 绑定文件 |');
402
- lines.push('|:-----|:-----|:-----|:---------|');
403
398
  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 ? '…' : '')
399
+ const meta = `${n.nodeType ?? 'process'}/${n.domain ?? 'system'}`;
400
+ const badges = [];
401
+ const fileCount = (n.boundFiles?.length ?? 0) + (n.boundDirs?.length ?? 0);
402
+ if (fileCount > 0)
403
+ badges.push(`files=${fileCount}`);
404
+ if (n.boundTasks?.length)
405
+ badges.push(`tasks=${n.boundTasks.length}`);
406
+ const docCount = n.docEntries?.length ?? 0;
407
+ if (docCount > 0)
408
+ badges.push(`docs=${docCount}`);
409
+ if (n.subFlowchart)
410
+ badges.push(`▶${n.subFlowchart}`);
411
+ const suffix = badges.length > 0 ? ` [${badges.join(' ')}]` : '';
412
+ const desc = n.description
413
+ ? `\n > ${n.description.slice(0, 120)}${n.description.length > 120 ? '…' : ''}`
410
414
  : '';
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} |`);
415
+ lines.push(`- **${n.label}** \`${n.id}\` (${meta})${suffix}${desc}`);
420
416
  }
421
417
  return wrap(lines.join('\n'));
422
418
  }
@@ -663,7 +659,6 @@ export function registerFlowchartTools(server, _ctx) {
663
659
  dirs.forEach((dirPath) => lines.push(`- ${dirPath}/`));
664
660
  }
665
661
  }
666
- // Versions 已由 docEntries 历史替代,不再输出
667
662
  return wrap(lines.join('\n').trim());
668
663
  }
669
664
  case 'update_node': {
@@ -688,9 +683,8 @@ export function registerFlowchartTools(server, _ctx) {
688
683
  if (decoded.description !== undefined) {
689
684
  // 自动归档旧 description 到 docEntries
690
685
  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})`);
686
+ nextNode = appendDocEntry(nextNode, '[auto] description更新前快照', nextNode.description);
687
+ changes.push('docArchive');
694
688
  }
695
689
  nextNode.description = decoded.description;
696
690
  changes.push('description');
@@ -719,9 +713,8 @@ export function registerFlowchartTools(server, _ctx) {
719
713
  if (!decoded.docSummary || !decoded.docContent) {
720
714
  return wrap('update_node requires both docSummary and docContent when updating docs.');
721
715
  }
722
- const result = appendDocEntry(nextNode, decoded.docSummary, decoded.docContent);
723
- nextNode = result.node;
724
- changes.push(`docEntries(${result.version})`);
716
+ nextNode = appendDocEntry(nextNode, decoded.docSummary, decoded.docContent);
717
+ changes.push('docEntries');
725
718
  }
726
719
  if (changes.length === 0) {
727
720
  return wrap('No fields were updated.');
@@ -14,10 +14,24 @@ import { initClient } from '../storage/httpClient.js';
14
14
  import { wrap, safeTool } from './shared.js';
15
15
  // 当前会话的 API URL (用于幂等检查)
16
16
  let currentApiUrl = null;
17
+ function findPpdocsPathFrom(dir) {
18
+ let currentDir = path.resolve(dir);
19
+ while (true) {
20
+ const configPath = path.join(currentDir, '.ppdocs');
21
+ if (fs.existsSync(configPath) && fs.statSync(configPath).isFile()) {
22
+ return configPath;
23
+ }
24
+ const parentDir = path.dirname(currentDir);
25
+ if (parentDir === currentDir) {
26
+ return null;
27
+ }
28
+ currentDir = parentDir;
29
+ }
30
+ }
17
31
  /** 尝试从指定目录读取 .ppdocs 配置 */
18
32
  function readPpdocsFrom(dir) {
19
- const configPath = path.join(dir, '.ppdocs');
20
- if (!fs.existsSync(configPath))
33
+ const configPath = findPpdocsPathFrom(dir);
34
+ if (!configPath)
21
35
  return null;
22
36
  try {
23
37
  const c = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "3.7.1",
3
+ "version": "3.9.0",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -3,7 +3,7 @@
3
3
  ## 0. ????
4
4
 
5
5
  ```text
6
- kg_init() -> ????
6
+ kg_init() -> optional fallback when auto-connect is missing
7
7
  kg_status() -> ???????
8
8
  kg_task(action:"get") -> ?? active ??
9
9
  kg_discuss(action:"list") -> ???????
@@ -27,7 +27,7 @@
27
27
 
28
28
  ### Phase 0: 环境准备
29
29
  ```
30
- kg_init() → 连接项目上下文
30
+ kg_init() → 仅在自动连接失效时手动兜底
31
31
  kg_status() → 查看仪表盘 (流程图/任务/状态)
32
32
  kg_flowchart(action:"list") → 已有流程图列表
33
33
  kg_flowchart(action:"get") → 主图结构总览
@@ -29,7 +29,7 @@
29
29
 
30
30
  **1.1 获取图谱现状**
31
31
  ```
32
- kg_init() → 确保项目连接
32
+ kg_init() → 仅在自动连接失效时手动兜底
33
33
  kg_flowchart(action:"list") → 所有流程图
34
34
  kg_flowchart(action:"get") → 主图节点+连线
35
35
  kg_flowchart(action:"search", query:"关键词") → 快速定位相关节点
@@ -3,7 +3,7 @@
3
3
  ## 0. ????
4
4
 
5
5
  ```text
6
- kg_init() -> ????
6
+ kg_init() -> optional fallback when auto-connect is missing
7
7
  kg_status() -> ???????
8
8
  kg_task(action:"get") -> ?? active ??
9
9
  kg_discuss(action:"list") -> ???????
@@ -3,7 +3,7 @@
3
3
  ## 0. ????
4
4
 
5
5
  ```text
6
- kg_init() -> ????
6
+ kg_init() -> optional fallback when auto-connect is missing
7
7
  kg_status() -> ???????
8
8
  kg_task(action:"get") -> ?? active ??
9
9
  kg_discuss(action:"list") -> ???????