@ppdocs/mcp 2.8.2 → 2.8.4

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.
@@ -22,6 +22,8 @@ export declare class PpdocsApiClient {
22
22
  updateDoc(docPath: string, updates: Partial<DocData>): Promise<DocData | null>;
23
23
  deleteDoc(docPath: string): Promise<boolean>;
24
24
  searchDocs(keywords: string[], limit?: number): Promise<DocSearchResult[]>;
25
+ /** 按状态筛选文档 */
26
+ getDocsByStatus(statusList: string[]): Promise<DocNode[]>;
25
27
  /** 获取目录树结构 */
26
28
  getTree(): Promise<TreeNode[]>;
27
29
  getRulesApi(ruleType: string): Promise<string[]>;
@@ -35,6 +37,23 @@ export declare class PpdocsApiClient {
35
37
  }, creator: string): Promise<Task>;
36
38
  addTaskLog(taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
37
39
  completeTask(taskId: string, experience: TaskExperience): Promise<Task | null>;
40
+ /** 列出所有可访问的项目 */
41
+ crossListProjects(): Promise<{
42
+ id: string;
43
+ name: string;
44
+ description: string;
45
+ updated_at: string;
46
+ }[]>;
47
+ /** 跨项目: 列出文档 */
48
+ crossListDocs(target: string): Promise<DocNode[]>;
49
+ /** 跨项目: 获取文档 */
50
+ crossGetDoc(target: string, docPath: string): Promise<DocData | null>;
51
+ /** 跨项目: 获取目录树 */
52
+ crossGetTree(target: string): Promise<TreeNode[]>;
53
+ /** 跨项目: 按状态筛选文档 */
54
+ crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
55
+ /** 跨项目: 获取规则 */
56
+ crossGetRules(target: string, ruleType: string): Promise<string[]>;
38
57
  }
39
58
  export declare function initClient(apiUrl: string): void;
40
59
  export declare function listDocs(_projectId: string): Promise<DocNode[]>;
@@ -43,6 +62,7 @@ export declare function createDoc(_projectId: string, docPath: string, doc: Part
43
62
  export declare function updateDoc(_projectId: string, docPath: string, updates: Partial<DocData>): Promise<DocData | null>;
44
63
  export declare function deleteDoc(_projectId: string, docPath: string): Promise<boolean>;
45
64
  export declare function searchDocs(_projectId: string, keywords: string[], limit?: number): Promise<DocSearchResult[]>;
65
+ export declare function getDocsByStatus(_projectId: string, statusList: string[]): Promise<DocNode[]>;
46
66
  export declare function getTree(_projectId: string): Promise<TreeNode[]>;
47
67
  export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
48
68
  export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
@@ -55,4 +75,14 @@ export declare function createTask(_projectId: string, task: {
55
75
  }, creator: string): Promise<Task>;
56
76
  export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
57
77
  export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
78
+ export declare function crossListProjects(): Promise<{
79
+ id: string;
80
+ name: string;
81
+ description: string;
82
+ updated_at: string;
83
+ }[]>;
84
+ export declare function crossGetDoc(target: string, docPath: string): Promise<DocData | null>;
85
+ export declare function crossGetTree(target: string): Promise<TreeNode[]>;
86
+ export declare function crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
87
+ export declare function crossGetRules(target: string, ruleType: string): Promise<string[]>;
58
88
  export type { TreeNode };
@@ -102,7 +102,8 @@ export class PpdocsApiClient {
102
102
  date: new Date().toISOString(),
103
103
  changes: '初始创建'
104
104
  }],
105
- bugfixes: doc.bugfixes || []
105
+ bugfixes: doc.bugfixes || [],
106
+ status: doc.status || '已完成' // 默认状态
106
107
  };
107
108
  return this.request(`/docs/${cleanPath}`, {
108
109
  method: 'POST',
@@ -120,7 +121,8 @@ export class PpdocsApiClient {
120
121
  summary: updates.summary ?? existing.summary,
121
122
  content: updates.content ?? existing.content,
122
123
  versions: updates.versions ?? existing.versions,
123
- bugfixes: updates.bugfixes ?? existing.bugfixes
124
+ bugfixes: updates.bugfixes ?? existing.bugfixes,
125
+ status: updates.status ?? existing.status ?? '已完成'
124
126
  };
125
127
  return await this.request(`/docs/${cleanPath}`, {
126
128
  method: 'PUT',
@@ -147,6 +149,13 @@ export class PpdocsApiClient {
147
149
  body: JSON.stringify({ keywords, limit })
148
150
  });
149
151
  }
152
+ /** 按状态筛选文档 */
153
+ async getDocsByStatus(statusList) {
154
+ return this.request('/docs/by-status', {
155
+ method: 'POST',
156
+ body: JSON.stringify({ status_list: statusList })
157
+ });
158
+ }
150
159
  /** 获取目录树结构 */
151
160
  async getTree() {
152
161
  const docs = await this.listDocs();
@@ -223,6 +232,46 @@ export class PpdocsApiClient {
223
232
  return null;
224
233
  }
225
234
  }
235
+ // ============ 跨项目只读访问 ============
236
+ /** 列出所有可访问的项目 */
237
+ async crossListProjects() {
238
+ return this.request('/cross/projects');
239
+ }
240
+ /** 跨项目: 列出文档 */
241
+ async crossListDocs(target) {
242
+ return this.request(`/cross/${encodeURIComponent(target)}/docs`);
243
+ }
244
+ /** 跨项目: 获取文档 */
245
+ async crossGetDoc(target, docPath) {
246
+ try {
247
+ const cleanPath = docPath.replace(/^\//, '');
248
+ return await this.request(`/cross/${encodeURIComponent(target)}/docs/${cleanPath}`);
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ /** 跨项目: 获取目录树 */
255
+ async crossGetTree(target) {
256
+ const docs = await this.crossListDocs(target);
257
+ return buildTree(docs);
258
+ }
259
+ /** 跨项目: 按状态筛选文档 */
260
+ async crossGetDocsByStatus(target, statusList) {
261
+ return this.request(`/cross/${encodeURIComponent(target)}/docs/by-status`, {
262
+ method: 'POST',
263
+ body: JSON.stringify({ status_list: statusList })
264
+ });
265
+ }
266
+ /** 跨项目: 获取规则 */
267
+ async crossGetRules(target, ruleType) {
268
+ try {
269
+ return await this.request(`/cross/${encodeURIComponent(target)}/rules/${ruleType}`);
270
+ }
271
+ catch {
272
+ return [];
273
+ }
274
+ }
226
275
  }
227
276
  // ============ 模块级 API ============
228
277
  let client = null;
@@ -254,6 +303,9 @@ export async function deleteDoc(_projectId, docPath) {
254
303
  export async function searchDocs(_projectId, keywords, limit) {
255
304
  return getClient().searchDocs(keywords, limit);
256
305
  }
306
+ export async function getDocsByStatus(_projectId, statusList) {
307
+ return getClient().getDocsByStatus(statusList);
308
+ }
257
309
  export async function getTree(_projectId) {
258
310
  return getClient().getTree();
259
311
  }
@@ -280,3 +332,19 @@ export async function addTaskLog(_projectId, taskId, logType, content) {
280
332
  export async function completeTask(_projectId, taskId, experience) {
281
333
  return getClient().completeTask(taskId, experience);
282
334
  }
335
+ // ============ 跨项目只读访问 ============
336
+ export async function crossListProjects() {
337
+ return getClient().crossListProjects();
338
+ }
339
+ export async function crossGetDoc(target, docPath) {
340
+ return getClient().crossGetDoc(target, docPath);
341
+ }
342
+ export async function crossGetTree(target) {
343
+ return getClient().crossGetTree(target);
344
+ }
345
+ export async function crossGetDocsByStatus(target, statusList) {
346
+ return getClient().crossGetDocsByStatus(target, statusList);
347
+ }
348
+ export async function crossGetRules(target, ruleType) {
349
+ return getClient().crossGetRules(target, ruleType);
350
+ }
@@ -10,12 +10,13 @@ export interface BugfixRecord {
10
10
  issue: string;
11
11
  solution: string;
12
12
  }
13
- /** 文档数据 (4个核心字段) */
13
+ /** 文档数据 (5个核心字段) */
14
14
  export interface DocData {
15
15
  summary: string;
16
16
  content: string;
17
17
  versions: VersionRecord[];
18
18
  bugfixes: BugfixRecord[];
19
+ status?: string;
19
20
  }
20
21
  /** 文档节点 (含路径信息,用于列表展示) */
21
22
  export interface DocNode {
@@ -23,6 +24,7 @@ export interface DocNode {
23
24
  name: string;
24
25
  summary: string;
25
26
  isDir: boolean;
27
+ status?: string;
26
28
  }
27
29
  /** 文档搜索结果 */
28
30
  export interface DocSearchResult {
@@ -55,7 +55,7 @@ export function formatTreeText(tree) {
55
55
  const connector = isLast ? '└── ' : '├── ';
56
56
  const childPrefix = isLast ? ' ' : '│ ';
57
57
  const icon = node.isDir ? '📁' : '📄';
58
- const summary = node.summary ? ` - ${node.summary}` : '';
58
+ const summary = node.summary ? ` # ${node.summary}` : '';
59
59
  lines.push(`${prefix}${connector}${icon} ${node.name}${summary}`);
60
60
  if (node.children && node.children.length > 0) {
61
61
  lines.push(...format(node.children, prefix + childPrefix));
@@ -3,12 +3,31 @@ import * as storage from '../storage/httpClient.js';
3
3
  import { decodeObjectStrings, getRules, RULE_TYPE_LABELS } from '../utils.js';
4
4
  import { wrap, formatDocMarkdown, formatTreeText, countTreeDocs } from './helpers.js';
5
5
  export function registerTools(server, projectId, _user) {
6
+ // ===================== 跨项目访问 =====================
7
+ // 0. 列出所有可访问的项目
8
+ server.tool('kg_list_projects', '列出所有可访问的项目(用于跨项目访问)', {}, 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
+ });
6
24
  // ===================== 文档管理 =====================
7
25
  // 1. 创建文档
8
26
  server.tool('kg_create_node', '创建知识文档。使用目录路径分类,文件名作为文档名', {
9
27
  path: z.string().min(1).describe('完整文档路径(如"/前端/组件/Modal")'),
10
28
  summary: z.string().optional().describe('一句话简介'),
11
- content: z.string().describe('Markdown内容')
29
+ content: z.string().describe('Markdown内容'),
30
+ status: z.string().optional().describe('文档状态(默认"已完成")')
12
31
  }, async (args) => {
13
32
  const decoded = decodeObjectStrings(args);
14
33
  const doc = await storage.createDoc(projectId, decoded.path, {
@@ -19,12 +38,13 @@ export function registerTools(server, projectId, _user) {
19
38
  date: new Date().toISOString(),
20
39
  changes: '初始创建'
21
40
  }],
22
- bugfixes: []
41
+ bugfixes: [],
42
+ status: decoded.status || '已完成'
23
43
  });
24
44
  return wrap(`✅ 文档已创建: ${decoded.path}\n\n${JSON.stringify(doc, null, 2)}`);
25
45
  });
26
- // 2. 删除文档 (支持批量)
27
- server.tool('kg_delete_node', '删除文档(支持批量,根文档不可删除)', { nodeId: z.union([z.string(), z.array(z.string())]).describe('文档路径或路径数组') }, async (args) => {
46
+ // 2. 删除文档或目录 (支持批量)
47
+ server.tool('kg_delete_node', '删除文档或目录(支持批量,根不可删除,目录会递归删除)', { nodeId: z.union([z.string(), z.array(z.string())]).describe('文档路径或路径数组') }, async (args) => {
28
48
  const paths = Array.isArray(args.nodeId) ? args.nodeId : [args.nodeId];
29
49
  const results = [];
30
50
  for (const path of paths) {
@@ -47,6 +67,7 @@ export function registerTools(server, projectId, _user) {
47
67
  nodeId: z.string().optional().describe('【单个模式】文档路径,如"/前端/组件/Button"'),
48
68
  summary: z.string().optional().describe('【单个模式】一句话简介'),
49
69
  content: z.string().optional().describe('【单个模式】Markdown内容'),
70
+ status: z.string().optional().describe('【单个模式】文档状态'),
50
71
  versions: z.array(z.object({
51
72
  version: z.number(),
52
73
  date: z.string(),
@@ -61,6 +82,7 @@ export function registerTools(server, projectId, _user) {
61
82
  nodeId: z.string().describe('文档路径'),
62
83
  summary: z.string().optional(),
63
84
  content: z.string().optional(),
85
+ status: z.string().optional(),
64
86
  versions: z.array(z.object({
65
87
  version: z.number(),
66
88
  date: z.string(),
@@ -92,6 +114,8 @@ export function registerTools(server, projectId, _user) {
92
114
  updates.summary = item.summary;
93
115
  if (item.content !== undefined)
94
116
  updates.content = item.content;
117
+ if (item.status !== undefined)
118
+ updates.status = item.status;
95
119
  if (item.versions !== undefined)
96
120
  updates.versions = item.versions;
97
121
  if (item.bugfixes !== undefined)
@@ -108,7 +132,7 @@ export function registerTools(server, projectId, _user) {
108
132
  return wrap(msg);
109
133
  }
110
134
  // 单个更新模式: 必须提供 nodeId
111
- const { nodeId, summary, content, versions, bugfixes } = decoded;
135
+ const { nodeId, summary, content, status, versions, bugfixes } = decoded;
112
136
  if (!nodeId) {
113
137
  return wrap('❌ 请提供 nodeId(单个模式)或 updates 数组(批量模式)');
114
138
  }
@@ -127,6 +151,8 @@ export function registerTools(server, projectId, _user) {
127
151
  updates.summary = summary;
128
152
  if (content !== undefined)
129
153
  updates.content = content;
154
+ if (status !== undefined)
155
+ updates.status = status;
130
156
  if (versions !== undefined)
131
157
  updates.versions = versions;
132
158
  if (bugfixes !== undefined)
@@ -156,10 +182,34 @@ export function registerTools(server, projectId, _user) {
156
182
  return wrap(doc ? '✅ 项目介绍已更新' : '更新失败');
157
183
  });
158
184
  // 3.6 获取项目规则
159
- server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)', {
185
+ server.tool('kg_get_rules', '获取项目规则(可指定类型或获取全部)。支持跨项目只读访问', {
160
186
  ruleType: z.enum(['userStyles', 'testRules', 'reviewRules', 'codeStyle', 'unitTests']).optional()
161
- .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则。不传则返回全部')
187
+ .describe('规则类型: userStyles=用户沟通规则, codeStyle=编码风格规则, reviewRules=代码审查规则, testRules=错误分析规则, unitTests=代码测试规则。不传则返回全部'),
188
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
162
189
  }, async (args) => {
190
+ // 跨项目访问
191
+ if (args.targetProject) {
192
+ if (args.ruleType) {
193
+ const rules = await storage.crossGetRules(args.targetProject, args.ruleType);
194
+ if (rules.length === 0) {
195
+ return { content: [{ type: 'text', text: `暂无${RULE_TYPE_LABELS[args.ruleType]}规则(项目: ${args.targetProject})` }] };
196
+ }
197
+ return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${rules.join('\n')}` }] };
198
+ }
199
+ // 获取全部规则
200
+ const allRules = [];
201
+ for (const type of ['userStyles', 'codeStyle', 'reviewRules', 'testRules', 'unitTests']) {
202
+ const rules = await storage.crossGetRules(args.targetProject, type);
203
+ if (rules.length > 0) {
204
+ allRules.push(`## ${RULE_TYPE_LABELS[type]}\n${rules.join('\n')}`);
205
+ }
206
+ }
207
+ if (allRules.length === 0) {
208
+ return { content: [{ type: 'text', text: `暂无项目规则(项目: ${args.targetProject})` }] };
209
+ }
210
+ return { content: [{ type: 'text', text: `[跨项目: ${args.targetProject}]\n\n${allRules.join('\n\n')}` }] };
211
+ }
212
+ // 当前项目
163
213
  const rules = await getRules(projectId, args.ruleType);
164
214
  if (!rules || rules.trim() === '') {
165
215
  const typeName = args.ruleType ? RULE_TYPE_LABELS[args.ruleType] : '项目';
@@ -181,9 +231,20 @@ export function registerTools(server, projectId, _user) {
181
231
  return wrap(`✅ ${RULE_TYPE_LABELS[decoded.ruleType]}已保存 (${decoded.rules.length} 条)`);
182
232
  });
183
233
  // 5. 读取单个文档详情
184
- server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)', {
185
- nodeId: z.string().describe('文档路径')
234
+ server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)。支持跨项目只读访问', {
235
+ nodeId: z.string().describe('文档路径'),
236
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
186
237
  }, async (args) => {
238
+ // 跨项目访问
239
+ if (args.targetProject) {
240
+ const doc = await storage.crossGetDoc(args.targetProject, args.nodeId);
241
+ if (!doc) {
242
+ return wrap(`文档不存在(项目: ${args.targetProject})`);
243
+ }
244
+ const lines = formatDocMarkdown(doc, args.nodeId);
245
+ return wrap(`[跨项目: ${args.targetProject}]\n\n${lines.join('\n')}`);
246
+ }
247
+ // 当前项目
187
248
  const doc = await storage.getDoc(projectId, args.nodeId);
188
249
  if (!doc) {
189
250
  return wrap('文档不存在');
@@ -193,8 +254,21 @@ export function registerTools(server, projectId, _user) {
193
254
  return wrap(lines.join('\n'));
194
255
  });
195
256
  // 10. 获取知识库树状图
196
- server.tool('kg_get_tree', '获取知识库的目录树结构(按 path 分组)', {}, async () => {
257
+ server.tool('kg_get_tree', '获取知识库目录树。格式: 📄 文档名 # 简介。支持跨项目只读访问', {
258
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
259
+ }, async (args) => {
197
260
  try {
261
+ // 跨项目访问
262
+ if (args.targetProject) {
263
+ const tree = await storage.crossGetTree(args.targetProject);
264
+ if (tree && tree.length > 0) {
265
+ const treeText = formatTreeText(tree);
266
+ const docCount = countTreeDocs(tree);
267
+ return wrap(`[跨项目: ${args.targetProject}]\n\n共 ${docCount} 个文档\n\n${treeText}`);
268
+ }
269
+ return wrap(`知识库为空(项目: ${args.targetProject})`);
270
+ }
271
+ // 当前项目
198
272
  const tree = await storage.getTree(projectId);
199
273
  if (tree && tree.length > 0) {
200
274
  const treeText = formatTreeText(tree);
@@ -211,6 +285,28 @@ export function registerTools(server, projectId, _user) {
211
285
  }, null, 2));
212
286
  }
213
287
  });
288
+ // 11. 按状态筛选文档
289
+ server.tool('kg_get_docs_by_status', '获取指定状态的文档列表。支持跨项目只读访问', {
290
+ statusList: z.array(z.string()).describe('状态列表(如["未完成","待修复"])'),
291
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目,跨项目只读)')
292
+ }, async (args) => {
293
+ // 跨项目访问
294
+ if (args.targetProject) {
295
+ const docs = await storage.crossGetDocsByStatus(args.targetProject, args.statusList);
296
+ if (docs.length === 0) {
297
+ return wrap(`未找到状态为 [${args.statusList.join(', ')}] 的文档(项目: ${args.targetProject})`);
298
+ }
299
+ const lines = docs.map(d => `- ${d.path} (${d.status || '已完成'}): ${d.summary || '无简介'}`);
300
+ return wrap(`[跨项目: ${args.targetProject}]\n\n找到 ${docs.length} 个文档:\n\n${lines.join('\n')}`);
301
+ }
302
+ // 当前项目
303
+ const docs = await storage.getDocsByStatus(projectId, args.statusList);
304
+ if (docs.length === 0) {
305
+ return wrap(`未找到状态为 [${args.statusList.join(', ')}] 的文档`);
306
+ }
307
+ const lines = docs.map(d => `- ${d.path} (${d.status || '已完成'}): ${d.summary || '无简介'}`);
308
+ return wrap(`找到 ${docs.length} 个文档:\n\n${lines.join('\n')}`);
309
+ });
214
310
  // ===================== 任务管理 =====================
215
311
  // 7. 创建任务
216
312
  server.tool('task_create', '创建开发任务', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ppdocs/mcp",
3
- "version": "2.8.2",
3
+ "version": "2.8.4",
4
4
  "description": "ppdocs MCP Server - Knowledge Graph for Claude",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",