@ppdocs/mcp 2.6.21 → 2.6.23
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 +58 -10
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -1
- package/dist/index.js +9 -2
- package/dist/storage/types.d.ts +1 -0
- package/dist/tools/index.js +65 -16
- package/dist/utils.d.ts +0 -12
- package/dist/utils.js +0 -18
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -6,13 +6,9 @@ import * as fs from 'fs';
|
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { execSync } from 'child_process';
|
|
9
|
+
import { generateUser } from './config.js';
|
|
9
10
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
11
|
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
11
|
-
/** 生成随机用户名 (8位字母数字) */
|
|
12
|
-
function generateUser() {
|
|
13
|
-
const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
14
|
-
return Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
15
|
-
}
|
|
16
12
|
function parseArgs(args) {
|
|
17
13
|
const opts = { port: 20001, api: 'localhost', codex: false };
|
|
18
14
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -151,12 +147,26 @@ function autoRegisterMcp(apiUrl, user) {
|
|
|
151
147
|
detected.push('Claude');
|
|
152
148
|
try {
|
|
153
149
|
console.log(`✅ Detected Claude CLI, registering MCP...`);
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
|
|
150
|
+
// 先检查是否已存在
|
|
151
|
+
const checkResult = execSync(`claude mcp list`, { encoding: 'utf-8' });
|
|
152
|
+
if (checkResult.includes(serverName)) {
|
|
153
|
+
console.log(`✅ Claude MCP already configured`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const cmd = `claude mcp add ${serverName} -- npx -y @ppdocs/mcp@latest`;
|
|
157
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
158
|
+
}
|
|
157
159
|
}
|
|
158
160
|
catch (e) {
|
|
159
|
-
|
|
161
|
+
// 如果 list 失败,尝试添加
|
|
162
|
+
try {
|
|
163
|
+
execSilent(`claude mcp remove ${serverName}`);
|
|
164
|
+
const cmd = `claude mcp add ${serverName} -- npx -y @ppdocs/mcp@latest`;
|
|
165
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
console.log(`⚠️ Claude MCP registration failed`);
|
|
169
|
+
}
|
|
160
170
|
}
|
|
161
171
|
}
|
|
162
172
|
// 检测 Codex CLI (OpenAI)
|
|
@@ -246,6 +256,33 @@ function generateHooksConfig() {
|
|
|
246
256
|
}
|
|
247
257
|
};
|
|
248
258
|
}
|
|
259
|
+
/** 生成 MCP 权限配置 (允许全部 ppdocs-kg 方法) */
|
|
260
|
+
function generateMcpPermissions() {
|
|
261
|
+
const mcpMethods = [
|
|
262
|
+
// 知识图谱操作
|
|
263
|
+
'kg_create_node',
|
|
264
|
+
'kg_delete_node',
|
|
265
|
+
'kg_update_node',
|
|
266
|
+
'kg_update_root',
|
|
267
|
+
'kg_get_rules',
|
|
268
|
+
'kg_save_rules',
|
|
269
|
+
'kg_lock_node',
|
|
270
|
+
'kg_search',
|
|
271
|
+
'kg_find_path',
|
|
272
|
+
'kg_list_nodes',
|
|
273
|
+
'kg_get_relations',
|
|
274
|
+
'kg_read_node',
|
|
275
|
+
'kg_get_tree',
|
|
276
|
+
'kg_find_by_file',
|
|
277
|
+
// 任务管理
|
|
278
|
+
'task_create',
|
|
279
|
+
'task_list',
|
|
280
|
+
'task_get',
|
|
281
|
+
'task_add_log',
|
|
282
|
+
'task_complete',
|
|
283
|
+
];
|
|
284
|
+
return mcpMethods.map(m => `mcp__ppdocs-kg__${m}`);
|
|
285
|
+
}
|
|
249
286
|
/** 安装 Claude Code 模板 */
|
|
250
287
|
function installClaudeTemplates(cwd) {
|
|
251
288
|
const claudeDir = path.join(cwd, '.claude');
|
|
@@ -273,13 +310,24 @@ function installClaudeTemplates(cwd) {
|
|
|
273
310
|
catch { /* ignore */ }
|
|
274
311
|
}
|
|
275
312
|
const hooksConfig = generateHooksConfig();
|
|
313
|
+
// 合并 MCP 权限到 permissions.allow
|
|
314
|
+
const existingPermissions = existingSettings.permissions || {};
|
|
315
|
+
const existingAllow = existingPermissions.allow || [];
|
|
316
|
+
const mcpPermissions = generateMcpPermissions();
|
|
317
|
+
// 去重合并
|
|
318
|
+
const mergedAllow = [...new Set([...existingAllow, ...mcpPermissions])];
|
|
276
319
|
const mergedSettings = {
|
|
277
320
|
...existingSettings,
|
|
278
|
-
...hooksConfig
|
|
321
|
+
...hooksConfig,
|
|
322
|
+
permissions: {
|
|
323
|
+
...existingPermissions,
|
|
324
|
+
allow: mergedAllow,
|
|
325
|
+
}
|
|
279
326
|
};
|
|
280
327
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
281
328
|
fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2));
|
|
282
329
|
console.log(`✅ Configured .claude/settings.json hooks (${process.platform})`);
|
|
330
|
+
console.log(`✅ Added ${mcpPermissions.length} MCP method permissions`);
|
|
283
331
|
}
|
|
284
332
|
/** 安装 Codex 模板 (生成 AGENTS.md) */
|
|
285
333
|
function installCodexTemplates(cwd) {
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
/** 生成随机用户名 (8位字母数字) */
|
|
8
|
-
function generateUser() {
|
|
8
|
+
export function generateUser() {
|
|
9
9
|
const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
|
|
10
10
|
return Array.from({ length: 8 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
|
|
11
11
|
}
|
package/dist/index.js
CHANGED
|
@@ -6,12 +6,19 @@
|
|
|
6
6
|
* 1. CLI 初始化: npx @ppdocs/mcp init -p <projectId> -k <key>
|
|
7
7
|
* 2. MCP 服务: 自动读取 .ppdocs 配置或 PPDOCS_API_URL 环境变量
|
|
8
8
|
*/
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
9
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
14
|
import { registerTools } from './tools/index.js';
|
|
12
15
|
import { initClient } from './storage/httpClient.js';
|
|
13
16
|
import { runCli } from './cli.js';
|
|
14
17
|
import { loadConfig } from './config.js';
|
|
18
|
+
// 从 package.json 读取版本号
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
21
|
+
const VERSION = pkg.version;
|
|
15
22
|
// 检查是否为 CLI 命令
|
|
16
23
|
const args = process.argv.slice(2);
|
|
17
24
|
if (args.length > 0 && runCli(args)) {
|
|
@@ -21,11 +28,11 @@ if (args.length > 0 && runCli(args)) {
|
|
|
21
28
|
async function main() {
|
|
22
29
|
const config = loadConfig();
|
|
23
30
|
initClient(config.apiUrl);
|
|
24
|
-
const server = new McpServer({ name: `ppdocs [${config.projectId}]`, version:
|
|
31
|
+
const server = new McpServer({ name: `ppdocs [${config.projectId}]`, version: VERSION }, { capabilities: { tools: {} } });
|
|
25
32
|
registerTools(server, config.projectId, config.user);
|
|
26
33
|
const transport = new StdioServerTransport();
|
|
27
34
|
await server.connect(transport);
|
|
28
|
-
console.error(`ppdocs MCP
|
|
35
|
+
console.error(`ppdocs MCP v${VERSION} | project: ${config.projectId} | user: ${config.user}`);
|
|
29
36
|
}
|
|
30
37
|
main().catch((err) => {
|
|
31
38
|
console.error('Fatal error:', err);
|
package/dist/storage/types.d.ts
CHANGED
package/dist/tools/index.js
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import * as storage from '../storage/httpClient.js';
|
|
3
|
-
import { decodeUnicodeEscapes, decodeObjectStrings,
|
|
3
|
+
import { decodeUnicodeEscapes, decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
|
|
4
4
|
// 辅助函数: 包装返回结果
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return { content: [{ type: 'text', text: wrapped }] };
|
|
5
|
+
function wrap(_projectId, text) {
|
|
6
|
+
return { content: [{ type: 'text', text }] };
|
|
8
7
|
}
|
|
9
8
|
export function registerTools(server, projectId, _user) {
|
|
10
9
|
// 1. 创建节点
|
|
11
|
-
server.tool('kg_create_node', '创建知识节点。type: logic=逻辑/函数, data=数据结构, intro
|
|
10
|
+
server.tool('kg_create_node', '创建知识节点。type: logic=逻辑/函数, data=数据结构, intro=概念介绍。⚠️ tags至少提供3个分类标签', {
|
|
12
11
|
title: z.string().describe('节点标题'),
|
|
13
|
-
type: z.enum(['logic', 'data', 'intro']).describe('节点类型'),
|
|
12
|
+
type: z.enum(['logic', 'data', 'intro']).describe('节点类型(logic/data/intro)'),
|
|
14
13
|
description: z.string().describe('Markdown描述(用Mermaid流程图+表格,禁止纯文字)'),
|
|
15
14
|
path: z.string().min(1).describe('目录路径(必填,如"/前端/组件")'),
|
|
16
|
-
tags: z.array(z.string()).min(3).describe('分类标签(
|
|
15
|
+
tags: z.array(z.string()).min(3).describe('⚠️ 分类标签(必填,至少3个)'),
|
|
17
16
|
signature: z.string().optional().describe('唯一签名(用于依赖匹配,默认=title)'),
|
|
17
|
+
summary: z.string().optional().describe('一句话简介(显示在标题下方)'),
|
|
18
18
|
dependencies: z.array(z.object({
|
|
19
19
|
name: z.string().describe('目标节点的signature'),
|
|
20
20
|
description: z.string().describe('依赖说明')
|
|
@@ -27,6 +27,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
27
27
|
type: decoded.type,
|
|
28
28
|
status: 'incomplete',
|
|
29
29
|
description: decoded.description || '',
|
|
30
|
+
summary: decoded.summary || '',
|
|
30
31
|
// x, y 不传递,由 httpClient 智能计算位置
|
|
31
32
|
locked: false,
|
|
32
33
|
signature: decoded.signature || decoded.title,
|
|
@@ -43,16 +44,17 @@ export function registerTools(server, projectId, _user) {
|
|
|
43
44
|
return wrap(projectId, success ? '删除成功' : '删除失败(节点不存在/已锁定/是根节点)');
|
|
44
45
|
});
|
|
45
46
|
// 3. 更新节点
|
|
46
|
-
server.tool('kg_update_node', '更新节点内容(锁定节点不可更新)', {
|
|
47
|
+
server.tool('kg_update_node', '更新节点内容(锁定节点不可更新)。⚠️ 更新tags时至少提供3个分类标签', {
|
|
47
48
|
nodeId: z.string().describe('节点ID'),
|
|
48
49
|
title: z.string().optional().describe('新标题'),
|
|
49
50
|
signature: z.string().optional().describe('新签名'),
|
|
50
51
|
description: z.string().optional().describe('新描述(Markdown)'),
|
|
52
|
+
summary: z.string().optional().describe('一句话简介(显示在标题下方)'),
|
|
51
53
|
path: z.string().optional().describe('目录路径(如"/前端/组件")'),
|
|
52
54
|
x: z.number().optional().describe('X坐标'),
|
|
53
55
|
y: z.number().optional().describe('Y坐标'),
|
|
54
56
|
status: z.enum(['incomplete', 'complete', 'fixing', 'refactoring', 'deprecated']).optional().describe('状态'),
|
|
55
|
-
tags: z.array(z.string()).min(3).optional().describe('分类标签(
|
|
57
|
+
tags: z.array(z.string()).min(3).optional().describe('⚠️ 分类标签(更新时至少3个)'),
|
|
56
58
|
dependencies: z.array(z.object({
|
|
57
59
|
name: z.string(),
|
|
58
60
|
description: z.string()
|
|
@@ -99,7 +101,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
99
101
|
}, async (args) => {
|
|
100
102
|
const decoded = decodeObjectStrings(args);
|
|
101
103
|
if (decoded.title === undefined && decoded.description === undefined) {
|
|
102
|
-
return wrap(projectId, '❌
|
|
104
|
+
return wrap(projectId, '❌ 请至少提供 title 或 description');
|
|
103
105
|
}
|
|
104
106
|
const node = await storage.updateRoot(projectId, {
|
|
105
107
|
title: decoded.title,
|
|
@@ -146,10 +148,15 @@ export function registerTools(server, projectId, _user) {
|
|
|
146
148
|
}, async (args) => {
|
|
147
149
|
const results = await storage.searchNodes(projectId, args.keywords, args.limit || 10);
|
|
148
150
|
const output = results.map(r => ({
|
|
149
|
-
id: r.node.id,
|
|
150
|
-
|
|
151
|
+
id: r.node.id,
|
|
152
|
+
title: r.node.title,
|
|
153
|
+
tags: r.node.categories || [],
|
|
154
|
+
status: r.node.status,
|
|
155
|
+
summary: r.node.summary || '',
|
|
156
|
+
hitRate: `${Math.round(r.score)}%`
|
|
151
157
|
}));
|
|
152
|
-
|
|
158
|
+
const json = JSON.stringify(output, null, 2);
|
|
159
|
+
return wrap(projectId, `${json}\n\n💡 需要详细内容请使用 kg_read_node(nodeId) 获取节点详情`);
|
|
153
160
|
});
|
|
154
161
|
// 6. 路径查找
|
|
155
162
|
server.tool('kg_find_path', '查找两节点间的依赖路径', {
|
|
@@ -176,7 +183,7 @@ export function registerTools(server, projectId, _user) {
|
|
|
176
183
|
? { status: args.status, minEdges: args.minEdges, maxEdges: args.maxEdges }
|
|
177
184
|
: undefined;
|
|
178
185
|
const nodes = await storage.listNodes(projectId, filter);
|
|
179
|
-
const output = nodes.map(n => ({ id: n.id, title: n.title, type: n.type, status: n.status, locked: n.locked }));
|
|
186
|
+
const output = nodes.map(n => ({ id: n.id, title: n.title, type: n.type, status: n.status, locked: n.locked, summary: n.summary || '' }));
|
|
180
187
|
return wrap(projectId, JSON.stringify(output, null, 2));
|
|
181
188
|
});
|
|
182
189
|
// 8. 查询节点关系网 (支持多层)
|
|
@@ -216,8 +223,9 @@ export function registerTools(server, projectId, _user) {
|
|
|
216
223
|
return wrap(projectId, JSON.stringify({ outgoing, incoming }, null, 2));
|
|
217
224
|
});
|
|
218
225
|
// 9. 读取单个节点详情
|
|
219
|
-
server.tool('kg_read_node', '读取单个节点的完整内容(描述、关联文件、依赖、历史记录)', {
|
|
220
|
-
nodeId: z.string().describe('节点ID')
|
|
226
|
+
server.tool('kg_read_node', '读取单个节点的完整内容(描述、关联文件、依赖、历史记录),可选包含上下游关系', {
|
|
227
|
+
nodeId: z.string().describe('节点ID'),
|
|
228
|
+
depth: z.number().min(0).max(3).optional().describe('关系查询层数(0=不含关系,1-3=含上下游关系,默认0)')
|
|
221
229
|
}, async (args) => {
|
|
222
230
|
const node = await storage.getNode(projectId, args.nodeId);
|
|
223
231
|
if (!node) {
|
|
@@ -227,6 +235,9 @@ export function registerTools(server, projectId, _user) {
|
|
|
227
235
|
const lines = [];
|
|
228
236
|
// 基础信息
|
|
229
237
|
lines.push(`## ${node.title}\n`);
|
|
238
|
+
if (node.summary) {
|
|
239
|
+
lines.push(`> ${node.summary}\n`);
|
|
240
|
+
}
|
|
230
241
|
lines.push('**基础信息**');
|
|
231
242
|
lines.push(`| 字段 | 值 |`);
|
|
232
243
|
lines.push(`|:---|:---|`);
|
|
@@ -272,6 +283,44 @@ export function registerTools(server, projectId, _user) {
|
|
|
272
283
|
node.bugfixes.forEach(b => lines.push(`| ${b.id} | ${b.date} | ${b.issue} | ${b.solution} |`));
|
|
273
284
|
lines.push('');
|
|
274
285
|
}
|
|
286
|
+
// 上下游关系 (depth > 0 时获取)
|
|
287
|
+
const depth = args.depth || 0;
|
|
288
|
+
if (depth > 0) {
|
|
289
|
+
const visited = new Set();
|
|
290
|
+
const outgoing = [];
|
|
291
|
+
const incoming = [];
|
|
292
|
+
async function fetchRelations(nodeId, currentDepth, direction) {
|
|
293
|
+
if (currentDepth > depth || visited.has(`${nodeId}-${direction}`))
|
|
294
|
+
return;
|
|
295
|
+
visited.add(`${nodeId}-${direction}`);
|
|
296
|
+
const relations = await storage.getRelations(projectId, nodeId);
|
|
297
|
+
for (const r of relations) {
|
|
298
|
+
if (r.direction === 'outgoing' && (direction === 'outgoing' || direction === 'both')) {
|
|
299
|
+
if (!outgoing.some(o => o.id === r.nodeId)) {
|
|
300
|
+
outgoing.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
|
|
301
|
+
await fetchRelations(r.nodeId, currentDepth + 1, 'outgoing');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (r.direction === 'incoming' && (direction === 'incoming' || direction === 'both')) {
|
|
305
|
+
if (!incoming.some(i => i.id === r.nodeId)) {
|
|
306
|
+
incoming.push({ id: r.nodeId, title: r.title, edge: r.edgeType });
|
|
307
|
+
await fetchRelations(r.nodeId, currentDepth + 1, 'incoming');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
await fetchRelations(args.nodeId, 1, 'both');
|
|
313
|
+
if (outgoing.length > 0 || incoming.length > 0) {
|
|
314
|
+
lines.push('**上下游关系**');
|
|
315
|
+
if (outgoing.length > 0) {
|
|
316
|
+
lines.push(`- 依赖 (${outgoing.length}): ${outgoing.map(o => o.title).join(', ')}`);
|
|
317
|
+
}
|
|
318
|
+
if (incoming.length > 0) {
|
|
319
|
+
lines.push(`- 被依赖 (${incoming.length}): ${incoming.map(i => i.title).join(', ')}`);
|
|
320
|
+
}
|
|
321
|
+
lines.push('');
|
|
322
|
+
}
|
|
323
|
+
}
|
|
275
324
|
return wrap(projectId, lines.join('\n'));
|
|
276
325
|
});
|
|
277
326
|
// 10. 获取知识库树状图
|
package/dist/utils.d.ts
CHANGED
|
@@ -7,18 +7,6 @@ export declare const RULE_TYPE_LABELS: Record<RuleType, string>;
|
|
|
7
7
|
* 获取指定类型的规则 (从独立文件读取)
|
|
8
8
|
*/
|
|
9
9
|
export declare function getRules(projectId: string, ruleType?: RuleType): Promise<string>;
|
|
10
|
-
/**
|
|
11
|
-
* 获取根节点的用户风格 (兼容旧接口)
|
|
12
|
-
*/
|
|
13
|
-
export declare function getRootStyle(projectId: string): Promise<string>;
|
|
14
|
-
/**
|
|
15
|
-
* 清除规则缓存 (保留接口兼容性,实际无需缓存)
|
|
16
|
-
*/
|
|
17
|
-
export declare function clearStyleCache(): void;
|
|
18
|
-
/**
|
|
19
|
-
* 包装工具返回结果 (不再自动注入规则,使用 kg_get_rules 按需获取)
|
|
20
|
-
*/
|
|
21
|
-
export declare function wrapResult(_projectId: string, result: string): Promise<string>;
|
|
22
10
|
/**
|
|
23
11
|
* 解码 Unicode 转义序列
|
|
24
12
|
* 将 \uXXXX 格式的转义序列转换为实际字符
|
package/dist/utils.js
CHANGED
|
@@ -39,24 +39,6 @@ export async function getRules(projectId, ruleType) {
|
|
|
39
39
|
}
|
|
40
40
|
return allRules.join('\n\n---\n\n');
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
* 获取根节点的用户风格 (兼容旧接口)
|
|
44
|
-
*/
|
|
45
|
-
export async function getRootStyle(projectId) {
|
|
46
|
-
return getRules(projectId, 'userStyles');
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* 清除规则缓存 (保留接口兼容性,实际无需缓存)
|
|
50
|
-
*/
|
|
51
|
-
export function clearStyleCache() {
|
|
52
|
-
// 独立文件存储无需缓存
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* 包装工具返回结果 (不再自动注入规则,使用 kg_get_rules 按需获取)
|
|
56
|
-
*/
|
|
57
|
-
export async function wrapResult(_projectId, result) {
|
|
58
|
-
return result;
|
|
59
|
-
}
|
|
60
42
|
/**
|
|
61
43
|
* 解码 Unicode 转义序列
|
|
62
44
|
* 将 \uXXXX 格式的转义序列转换为实际字符
|