@ppdocs/mcp 3.1.7 → 3.1.9

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
@@ -356,13 +356,22 @@ function createMcpConfigAt(mcpPath, apiUrl) {
356
356
  }
357
357
  // Windows 需要 cmd /c 包装才能执行 npx
358
358
  const isWindows = process.platform === 'win32';
359
- const ppdocsServer = {
360
- command: isWindows ? 'cmd' : 'npx',
361
- args: isWindows ? ['/c', 'npx', '-y', '@ppdocs/mcp@latest'] : ['-y', '@ppdocs/mcp@latest'],
362
- env: {
363
- "PPDOCS_API_URL": apiUrl
364
- }
365
- };
359
+ // 对于 Mac/Linux,IDE 可能没有用户的完整 PATH,导致找不到 npx
360
+ const ppdocsServer = isWindows
361
+ ? {
362
+ command: 'cmd',
363
+ args: ['/c', 'npx', '-y', '@ppdocs/mcp@latest'],
364
+ env: { "PPDOCS_API_URL": apiUrl }
365
+ }
366
+ : {
367
+ command: 'npx',
368
+ args: ['-y', '@ppdocs/mcp@latest'],
369
+ env: {
370
+ "PPDOCS_API_URL": apiUrl,
371
+ // 强行注入常见路径,防止找不到 node/npx
372
+ "PATH": "$PATH:/usr/local/bin:/opt/homebrew/bin:/home/linuxbrew/.linuxbrew/bin:$HOME/.nvm/versions/node/current/bin:$HOME/.local/share/fnm/aliases/default/bin"
373
+ }
374
+ };
366
375
  mcpConfig.mcpServers = {
367
376
  ...(mcpConfig.mcpServers || {}),
368
377
  "ppdocs-kg": ppdocsServer,
package/dist/config.d.ts CHANGED
@@ -7,7 +7,7 @@ export interface PpdocsConfig {
7
7
  projectId: string;
8
8
  user: string;
9
9
  }
10
- export declare const PPDOCS_DIR = ".ppdocs";
10
+ export declare const PPDOCS_CONFIG_FILE = ".ppdocs";
11
11
  /** 生成随机用户名 (8位字母数字) */
12
12
  export declare function generateUser(): string;
13
13
  /**
package/dist/config.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import * as fs from 'fs';
6
6
  import * as path from 'path';
7
- export const PPDOCS_DIR = '.ppdocs';
7
+ export const PPDOCS_CONFIG_FILE = '.ppdocs';
8
8
  /** 生成随机用户名 (8位字母数字) */
9
9
  export function generateUser() {
10
10
  const chars = 'abcdefghjkmnpqrstuvwxyz23456789';
@@ -99,74 +99,4 @@ export declare class PpdocsApiClient {
99
99
  }
100
100
  export declare function initClient(apiUrl: string): void;
101
101
  export declare function getClient(): PpdocsApiClient;
102
- export declare function listDocs(_projectId: string): Promise<DocNode[]>;
103
- export declare function getDoc(_projectId: string, docPath: string): Promise<DocData | null>;
104
- export declare function createDoc(_projectId: string, docPath: string, doc: Partial<DocData>): Promise<DocData>;
105
- export declare function updateDoc(_projectId: string, docPath: string, updates: Partial<DocData>): Promise<DocData | null>;
106
- export declare function deleteDoc(_projectId: string, docPath: string): Promise<boolean>;
107
- export declare function searchDocs(_projectId: string, keywords: string[], limit?: number): Promise<DocSearchResult[]>;
108
- export declare function getDocsByStatus(_projectId: string, statusList: string[]): Promise<DocNode[]>;
109
- export declare function getTree(_projectId: string): Promise<TreeNode[]>;
110
- export declare function getRules(_projectId: string, ruleType: string): Promise<string[]>;
111
- export declare function saveRules(_projectId: string, ruleType: string, rules: string[]): Promise<boolean>;
112
- export declare function appendRules(_projectId: string, ruleType: string, newRules: string[]): Promise<{
113
- success: boolean;
114
- total: number;
115
- added: number;
116
- }>;
117
- export declare function getRulesMeta(): Promise<Record<string, RuleMeta>>;
118
- export declare function saveRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
119
- export declare function mergeRulesMeta(newMeta: Record<string, RuleMeta>): Promise<{
120
- success: boolean;
121
- total: number;
122
- }>;
123
- export declare function crossGetRulesMeta(target: string): Promise<Record<string, RuleMeta>>;
124
- export declare function getGlobalRulesMeta(): Promise<Record<string, RuleMeta>>;
125
- export declare function saveGlobalRulesMeta(meta: Record<string, RuleMeta>): Promise<boolean>;
126
- export declare function mergeGlobalRulesMeta(newMeta: Record<string, RuleMeta>): Promise<{
127
- success: boolean;
128
- total: number;
129
- }>;
130
- export declare function listTasks(_projectId: string, status?: 'active' | 'archived'): Promise<TaskSummary[]>;
131
- export declare function getTask(_projectId: string, taskId: string): Promise<Task | null>;
132
- export declare function createTask(_projectId: string, task: {
133
- title: string;
134
- description: string;
135
- goals: string[];
136
- }, creator: string): Promise<Task>;
137
- export declare function addTaskLog(_projectId: string, taskId: string, logType: TaskLogType, content: string): Promise<Task | null>;
138
- export declare function completeTask(_projectId: string, taskId: string, experience: TaskExperience): Promise<Task | null>;
139
- export declare function createProject(id: string, name: string, description?: string, projectPath?: string): Promise<{
140
- project: {
141
- id: string;
142
- name: string;
143
- };
144
- password: string;
145
- }>;
146
- export declare function crossListProjects(): Promise<{
147
- id: string;
148
- name: string;
149
- description: string;
150
- updatedAt: string;
151
- }[]>;
152
- export declare function crossGetDoc(target: string, docPath: string): Promise<DocData | null>;
153
- export declare function crossGetTree(target: string): Promise<TreeNode[]>;
154
- export declare function crossGetDocsByStatus(target: string, statusList: string[]): Promise<DocNode[]>;
155
- export declare function crossGetRules(target: string, ruleType: string): Promise<string[]>;
156
- export declare function listFiles(dir?: string): Promise<FileInfo[]>;
157
- export declare function readFile(filePath: string): Promise<string>;
158
- export declare function download(remotePath: string, localPath?: string): Promise<{
159
- localPath: string;
160
- fileCount: number;
161
- }>;
162
- export declare function crossListFiles(target: string, dir?: string): Promise<FileInfo[]>;
163
- export declare function crossReadFile(target: string, filePath: string): Promise<string>;
164
- export declare function crossDownload(target: string, remotePath: string, localPath?: string): Promise<{
165
- localPath: string;
166
- fileCount: number;
167
- }>;
168
- export declare function uploadFiles(localDir: string, remoteDir?: string): Promise<{
169
- fileCount: number;
170
- }>;
171
- export declare function clearFiles(): Promise<void>;
172
102
  export type { TreeNode };
@@ -492,131 +492,3 @@ export function getClient() {
492
492
  }
493
493
  return client;
494
494
  }
495
- // ============ 文档管理 ============
496
- export async function listDocs(_projectId) {
497
- return getClient().listDocs();
498
- }
499
- export async function getDoc(_projectId, docPath) {
500
- return getClient().getDoc(docPath);
501
- }
502
- export async function createDoc(_projectId, docPath, doc) {
503
- return getClient().createDoc(docPath, doc);
504
- }
505
- export async function updateDoc(_projectId, docPath, updates) {
506
- return getClient().updateDoc(docPath, updates);
507
- }
508
- export async function deleteDoc(_projectId, docPath) {
509
- return getClient().deleteDoc(docPath);
510
- }
511
- export async function searchDocs(_projectId, keywords, limit) {
512
- return getClient().searchDocs(keywords, limit);
513
- }
514
- export async function getDocsByStatus(_projectId, statusList) {
515
- return getClient().getDocsByStatus(statusList);
516
- }
517
- export async function getTree(_projectId) {
518
- return getClient().getTree();
519
- }
520
- // ============ 规则管理 ============
521
- export async function getRules(_projectId, ruleType) {
522
- return getClient().getRulesApi(ruleType);
523
- }
524
- export async function saveRules(_projectId, ruleType, rules) {
525
- return getClient().saveRulesApi(ruleType, rules);
526
- }
527
- export async function appendRules(_projectId, ruleType, newRules) {
528
- const existing = await getClient().getRulesApi(ruleType);
529
- const existingSet = new Set(existing.map(r => r.trim()));
530
- const toAdd = newRules.filter(r => !existingSet.has(r.trim()));
531
- const merged = [...existing, ...toAdd];
532
- const success = await getClient().saveRulesApi(ruleType, merged);
533
- return { success, total: merged.length, added: toAdd.length };
534
- }
535
- export async function getRulesMeta() {
536
- return getClient().getRulesMeta();
537
- }
538
- export async function saveRulesMeta(meta) {
539
- return getClient().saveRulesMeta(meta);
540
- }
541
- export async function mergeRulesMeta(newMeta) {
542
- const existing = await getClient().getRulesMeta();
543
- const merged = { ...existing, ...newMeta };
544
- const success = await getClient().saveRulesMeta(merged);
545
- return { success, total: Object.keys(merged).length };
546
- }
547
- export async function crossGetRulesMeta(target) {
548
- return getClient().crossGetRulesMeta(target);
549
- }
550
- export async function getGlobalRulesMeta() {
551
- return getClient().getGlobalRulesMeta();
552
- }
553
- export async function saveGlobalRulesMeta(meta) {
554
- return getClient().saveGlobalRulesMeta(meta);
555
- }
556
- export async function mergeGlobalRulesMeta(newMeta) {
557
- const existing = await getClient().getGlobalRulesMeta();
558
- const merged = { ...existing, ...newMeta };
559
- const success = await getClient().saveGlobalRulesMeta(merged);
560
- return { success, total: Object.keys(merged).length };
561
- }
562
- // ============ 任务管理 ============
563
- export async function listTasks(_projectId, status) {
564
- return getClient().listTasks(status);
565
- }
566
- export async function getTask(_projectId, taskId) {
567
- return getClient().getTask(taskId);
568
- }
569
- export async function createTask(_projectId, task, creator) {
570
- return getClient().createTask(task, creator);
571
- }
572
- export async function addTaskLog(_projectId, taskId, logType, content) {
573
- return getClient().addTaskLog(taskId, logType, content);
574
- }
575
- export async function completeTask(_projectId, taskId, experience) {
576
- return getClient().completeTask(taskId, experience);
577
- }
578
- // ============ 项目管理 ============
579
- export async function createProject(id, name, description, projectPath) {
580
- return getClient().createProject(id, name, description, projectPath);
581
- }
582
- // ============ 跨项目只读访问 ============
583
- export async function crossListProjects() {
584
- return getClient().crossListProjects();
585
- }
586
- export async function crossGetDoc(target, docPath) {
587
- return getClient().crossGetDoc(target, docPath);
588
- }
589
- export async function crossGetTree(target) {
590
- return getClient().crossGetTree(target);
591
- }
592
- export async function crossGetDocsByStatus(target, statusList) {
593
- return getClient().crossGetDocsByStatus(target, statusList);
594
- }
595
- export async function crossGetRules(target, ruleType) {
596
- return getClient().crossGetRules(target, ruleType);
597
- }
598
- // ============ 项目文件访问 ============
599
- export async function listFiles(dir) {
600
- return getClient().listFiles(dir);
601
- }
602
- export async function readFile(filePath) {
603
- return getClient().readFile(filePath);
604
- }
605
- export async function download(remotePath, localPath) {
606
- return getClient().download(remotePath, localPath);
607
- }
608
- export async function crossListFiles(target, dir) {
609
- return getClient().crossListFiles(target, dir);
610
- }
611
- export async function crossReadFile(target, filePath) {
612
- return getClient().crossReadFile(target, filePath);
613
- }
614
- export async function crossDownload(target, remotePath, localPath) {
615
- return getClient().crossDownload(target, remotePath, localPath);
616
- }
617
- export async function uploadFiles(localDir, remoteDir) {
618
- return getClient().uploadFiles(localDir, remoteDir);
619
- }
620
- export async function clearFiles() {
621
- return getClient().clearFiles();
622
- }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 文档管理工具 (7个)
3
+ * kg_create_node, kg_delete_node, kg_update_node, kg_read_node,
4
+ * kg_get_tree, kg_get_docs_by_status, kg_copy_node
5
+ */
6
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ export declare function registerDocTools(server: McpServer, projectId: string): void;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * 文档管理工具 (7个)
3
+ * kg_create_node, kg_delete_node, kg_update_node, kg_read_node,
4
+ * kg_get_tree, kg_get_docs_by_status, kg_copy_node
5
+ */
6
+ import { z } from 'zod';
7
+ import { getClient } from '../storage/httpClient.js';
8
+ import { decodeObjectStrings } from '../utils.js';
9
+ import { wrap, safeTool, crossPrefix, formatDocMarkdown, formatTreeText, countTreeDocs } from './shared.js';
10
+ export function registerDocTools(server, projectId) {
11
+ const client = () => getClient();
12
+ // 创建文档
13
+ server.tool('kg_create_node', '创建知识文档。使用目录路径分类,文件名作为文档名', {
14
+ path: z.string().min(1).describe('完整文档路径(如"/前端/组件/Modal")'),
15
+ summary: z.string().optional().describe('一句话简介'),
16
+ content: z.string().describe('Markdown内容'),
17
+ status: z.string().optional().describe('文档状态(默认"已完成")')
18
+ }, async (args) => safeTool(async () => {
19
+ const d = decodeObjectStrings(args);
20
+ const doc = await client().createDoc(d.path, {
21
+ summary: d.summary || '',
22
+ content: d.content,
23
+ versions: [{ version: 0.1, date: new Date().toISOString(), changes: '初始创建' }],
24
+ bugfixes: [],
25
+ status: d.status || '已完成'
26
+ });
27
+ return wrap(`✅ 文档已创建: ${d.path}\n\n${JSON.stringify(doc, null, 2)}`);
28
+ }));
29
+ // 删除文档或目录 (支持批量)
30
+ server.tool('kg_delete_node', '删除文档或目录(支持批量,根不可删除,目录会递归删除)', { nodeId: z.union([z.string(), z.array(z.string())]).describe('文档路径或路径数组') }, async (args) => safeTool(async () => {
31
+ const paths = Array.isArray(args.nodeId) ? args.nodeId : [args.nodeId];
32
+ const results = [];
33
+ for (const p of paths) {
34
+ results.push({ path: p, success: await client().deleteDoc(p) });
35
+ }
36
+ const successCount = results.filter(r => r.success).length;
37
+ if (paths.length === 1) {
38
+ return wrap(results[0].success ? '删除成功' : '删除失败(文档不存在或是根文档)');
39
+ }
40
+ const failedPaths = results.filter(r => !r.success).map(r => r.path);
41
+ let msg = `✅ 批量删除完成: ${successCount}/${paths.length} 成功`;
42
+ if (failedPaths.length > 0)
43
+ msg += `\n❌ 失败: ${failedPaths.join(', ')}`;
44
+ return wrap(msg);
45
+ }));
46
+ // 更新文档 (支持单个/批量)
47
+ server.tool('kg_update_node', '更新文档内容。【单个模式】nodeId+字段;【批量模式】仅传updates数组。两种模式二选一', {
48
+ nodeId: z.string().optional().describe('【单个模式】文档路径,如"/前端/组件/Button"'),
49
+ summary: z.string().optional().describe('【单个模式】一句话简介'),
50
+ content: z.string().optional().describe('【单个模式】Markdown内容'),
51
+ status: z.string().optional().describe('【单个模式】文档状态'),
52
+ versions: z.array(z.object({
53
+ version: z.number(), date: z.string(), changes: z.string()
54
+ })).optional().describe('【单个模式】版本记录数组'),
55
+ bugfixes: z.array(z.object({
56
+ date: z.string(), issue: z.string(), solution: z.string()
57
+ })).optional().describe('【单个模式】修复记录数组'),
58
+ updates: z.array(z.object({
59
+ nodeId: z.string().describe('文档路径'),
60
+ summary: z.string().optional(),
61
+ content: z.string().optional(),
62
+ status: z.string().optional(),
63
+ versions: z.array(z.object({
64
+ version: z.number(), date: z.string(), changes: z.string()
65
+ })).optional(),
66
+ bugfixes: z.array(z.object({
67
+ date: z.string(), issue: z.string(), solution: z.string()
68
+ })).optional()
69
+ })).optional().describe('【批量模式】更新数组,每项包含nodeId和要更新的字段')
70
+ }, async (args) => safeTool(async () => {
71
+ const decoded = decodeObjectStrings(args);
72
+ // 批量更新模式
73
+ if (decoded.updates && Array.isArray(decoded.updates)) {
74
+ const results = [];
75
+ for (const item of decoded.updates) {
76
+ if (item.nodeId === '/' || item.nodeId === '_root') {
77
+ results.push({ path: item.nodeId, success: false });
78
+ continue;
79
+ }
80
+ const existing = await client().getDoc(item.nodeId);
81
+ if (!existing) {
82
+ results.push({ path: item.nodeId, success: false });
83
+ continue;
84
+ }
85
+ const updates = {};
86
+ if (item.summary !== undefined)
87
+ updates.summary = item.summary;
88
+ if (item.content !== undefined)
89
+ updates.content = item.content;
90
+ if (item.status !== undefined)
91
+ updates.status = item.status;
92
+ if (item.versions !== undefined)
93
+ updates.versions = item.versions;
94
+ if (item.bugfixes !== undefined)
95
+ updates.bugfixes = item.bugfixes;
96
+ const doc = await client().updateDoc(item.nodeId, updates);
97
+ results.push({ path: item.nodeId, success: !!doc });
98
+ }
99
+ const successCount = results.filter(r => r.success).length;
100
+ const failedPaths = results.filter(r => !r.success).map(r => r.path);
101
+ let msg = `✅ 批量更新完成: ${successCount}/${decoded.updates.length} 成功`;
102
+ if (failedPaths.length > 0)
103
+ msg += `\n❌ 失败: ${failedPaths.join(', ')}`;
104
+ return wrap(msg);
105
+ }
106
+ // 单个更新模式
107
+ const { nodeId, summary, content, status, versions, bugfixes } = decoded;
108
+ if (!nodeId)
109
+ return wrap('❌ 请提供 nodeId(单个模式)或 updates 数组(批量模式)');
110
+ if (nodeId === '/' || nodeId === '_root')
111
+ return wrap('❌ 根文档请使用 kg_update_root 方法更新');
112
+ const existing = await client().getDoc(nodeId);
113
+ if (!existing)
114
+ return wrap('更新失败(文档不存在)');
115
+ const updates = {};
116
+ if (summary !== undefined)
117
+ updates.summary = summary;
118
+ if (content !== undefined)
119
+ updates.content = content;
120
+ if (status !== undefined)
121
+ updates.status = status;
122
+ if (versions !== undefined)
123
+ updates.versions = versions;
124
+ if (bugfixes !== undefined)
125
+ updates.bugfixes = bugfixes;
126
+ const doc = await client().updateDoc(nodeId, updates);
127
+ return wrap(doc ? JSON.stringify(doc, null, 2) : '更新失败');
128
+ }));
129
+ // 读取单个文档详情
130
+ server.tool('kg_read_node', '读取文档完整内容(简介、正文、版本历史、修复记录)。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
131
+ nodeId: z.string().describe('文档路径'),
132
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
133
+ }, async (args) => safeTool(async () => {
134
+ const doc = args.targetProject
135
+ ? await client().crossGetDoc(args.targetProject, args.nodeId)
136
+ : await client().getDoc(args.nodeId);
137
+ if (!doc)
138
+ return wrap(args.targetProject ? `文档不存在(项目: ${args.targetProject})` : '文档不存在');
139
+ const lines = formatDocMarkdown(doc, args.nodeId);
140
+ return wrap(args.targetProject ? crossPrefix(args.targetProject) + lines.join('\n') : lines.join('\n'));
141
+ }));
142
+ // 获取知识库树状图
143
+ server.tool('kg_get_tree', '获取知识库目录树。格式: 📄 文档名 # 简介。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
144
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
145
+ }, async (args) => safeTool(async () => {
146
+ const tree = args.targetProject
147
+ ? await client().crossGetTree(args.targetProject)
148
+ : await client().getTree();
149
+ if (!tree || tree.length === 0) {
150
+ return wrap(args.targetProject ? `知识库为空(项目: ${args.targetProject})` : '知识库为空');
151
+ }
152
+ const treeText = formatTreeText(tree);
153
+ const docCount = countTreeDocs(tree);
154
+ const prefix = args.targetProject ? crossPrefix(args.targetProject) : '';
155
+ return wrap(`${prefix}共 ${docCount} 个文档\n\n${treeText}`);
156
+ }));
157
+ // 按状态筛选文档
158
+ server.tool('kg_get_docs_by_status', '获取指定状态的文档列表。支持跨项目只读访问(需先用kg_list_projects获取项目ID)', {
159
+ statusList: z.array(z.string()).describe('状态列表(如["未完成","待修复"])'),
160
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
161
+ }, async (args) => safeTool(async () => {
162
+ const docs = args.targetProject
163
+ ? await client().crossGetDocsByStatus(args.targetProject, args.statusList)
164
+ : await client().getDocsByStatus(args.statusList);
165
+ if (docs.length === 0) {
166
+ return wrap(`未找到状态为 [${args.statusList.join(', ')}] 的文档${args.targetProject ? `(项目: ${args.targetProject})` : ''}`);
167
+ }
168
+ const lines = docs.map(d => `- ${d.path} (${d.status || '已完成'}): ${d.summary || '无简介'}`);
169
+ const prefix = args.targetProject ? crossPrefix(args.targetProject) : '';
170
+ return wrap(`${prefix}找到 ${docs.length} 个文档:\n\n${lines.join('\n')}`);
171
+ }));
172
+ // 跨项目复制文档
173
+ server.tool('kg_copy_node', '从其他项目复制文档到当前项目。支持单个或批量复制,可指定新路径。用于跨项目协作:拉取别的项目的知识文档到当前项目', {
174
+ sourceProject: z.string().describe('源项目ID或名称(从kg_list_projects获取)'),
175
+ sourcePath: z.union([z.string(), z.array(z.string())]).describe('源文档路径(单个或数组)'),
176
+ targetPath: z.string().optional().describe('目标路径前缀(如"/参考/项目B")。不填则保持原路径'),
177
+ }, async (args) => safeTool(async () => {
178
+ const decoded = decodeObjectStrings(args);
179
+ const paths = Array.isArray(decoded.sourcePath) ? decoded.sourcePath : [decoded.sourcePath];
180
+ const results = [];
181
+ for (const srcPath of paths) {
182
+ try {
183
+ const doc = await client().crossGetDoc(decoded.sourceProject, srcPath);
184
+ if (!doc) {
185
+ results.push({ path: srcPath, success: false, error: '源文档不存在' });
186
+ continue;
187
+ }
188
+ const destPath = decoded.targetPath
189
+ ? `${decoded.targetPath.replace(/\/$/, '')}${srcPath}`
190
+ : srcPath;
191
+ const existing = await client().getDoc(destPath);
192
+ if (existing) {
193
+ await client().updateDoc(destPath, { summary: doc.summary, content: doc.content });
194
+ }
195
+ else {
196
+ await client().createDoc(destPath, { summary: doc.summary, content: doc.content, status: doc.status });
197
+ }
198
+ results.push({ path: destPath, success: true });
199
+ }
200
+ catch (e) {
201
+ results.push({ path: srcPath, success: false, error: String(e) });
202
+ }
203
+ }
204
+ const successCount = results.filter(r => r.success).length;
205
+ if (paths.length === 1) {
206
+ return wrap(results[0].success
207
+ ? `✅ 已复制文档: ${results[0].path}\n\n来源: [${decoded.sourceProject}] ${paths[0]}`
208
+ : `❌ 复制失败: ${results[0].error}`);
209
+ }
210
+ const failedItems = results.filter(r => !r.success);
211
+ let msg = `✅ 批量复制完成: ${successCount}/${paths.length} 成功`;
212
+ if (failedItems.length > 0) {
213
+ msg += `\n❌ 失败:\n${failedItems.map(f => ` - ${f.path}: ${f.error}`).join('\n')}`;
214
+ }
215
+ return wrap(msg);
216
+ }));
217
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * 项目文件访问工具 (5个)
3
+ * project_upload, project_clear_files, project_list_files,
4
+ * project_read_file, project_download
5
+ */
6
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+ export declare function registerFileTools(server: McpServer): void;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 项目文件访问工具 (5个)
3
+ * project_upload, project_clear_files, project_list_files,
4
+ * project_read_file, project_download
5
+ */
6
+ import { z } from 'zod';
7
+ import { getClient } from '../storage/httpClient.js';
8
+ import { wrap, safeTool, crossPrefix, formatFileSize } from './shared.js';
9
+ export function registerFileTools(server) {
10
+ const client = () => getClient();
11
+ // 上传本地目录到中心服务器
12
+ server.tool('project_upload', '上传本地项目目录到中心服务器(打包zip上传,自动排除node_modules/.git等)。上传后其他客户端可通过跨项目访问下载', {
13
+ localDir: z.string().describe('本地目录的绝对路径(如"/home/user/project/src")'),
14
+ remoteDir: z.string().optional().describe('上传到服务器的目标子目录(如"src"),不填则上传到根目录'),
15
+ }, async (args) => safeTool(async () => {
16
+ const result = await client().uploadFiles(args.localDir, args.remoteDir);
17
+ return wrap(`✅ 上传成功\n\n- 文件数量: ${result.fileCount}\n- 目标目录: ${args.remoteDir || '/'}\n\n其他客户端可通过 project_read_file / project_download 访问`);
18
+ }));
19
+ // 清空项目文件存储区
20
+ server.tool('project_clear_files', '清空当前项目在中心服务器上的文件存储区', {}, async () => safeTool(async () => {
21
+ await client().clearFiles();
22
+ return wrap('✅ 文件存储已清空');
23
+ }));
24
+ // 列出项目文件
25
+ server.tool('project_list_files', '浏览项目源码目录结构(仅列出文件名和大小,不读内容)。目标项目需已上传文件或关联源码目录。跨项目时先用kg_list_projects获取项目ID。查看文件内容请用project_read_file', {
26
+ dir: z.string().optional().describe('子目录路径(如"src/components"),不填则列出根目录'),
27
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
28
+ }, async (args) => safeTool(async () => {
29
+ const files = args.targetProject
30
+ ? await client().crossListFiles(args.targetProject, args.dir)
31
+ : await client().listFiles(args.dir);
32
+ if (files.length === 0)
33
+ return wrap('目录为空');
34
+ const lines = files.map(f => {
35
+ const icon = f.isDir ? '📁' : '📄';
36
+ const size = f.isDir ? '' : ` (${formatFileSize(f.size)})`;
37
+ return `${icon} ${f.name}${size}`;
38
+ });
39
+ const prefix = args.targetProject ? crossPrefix(args.targetProject) : '';
40
+ const dirLabel = args.dir || '/';
41
+ return wrap(`${prefix}📂 ${dirLabel} (${files.length} 项)\n\n${lines.join('\n')}`);
42
+ }));
43
+ // 读取项目文件
44
+ server.tool('project_read_file', '读取单个源码文件的文本内容(限1MB)。适合查看具体代码实现。跨项目时先用kg_list_projects获取项目ID。浏览目录结构请用project_list_files', {
45
+ path: z.string().describe('文件路径(如"src/main.ts")'),
46
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
47
+ }, async (args) => safeTool(async () => {
48
+ const content = args.targetProject
49
+ ? await client().crossReadFile(args.targetProject, args.path)
50
+ : await client().readFile(args.path);
51
+ const prefix = args.targetProject ? crossPrefix(args.targetProject) : '';
52
+ return wrap(`${prefix}📄 ${args.path}\n\n\`\`\`\n${content}\n\`\`\``);
53
+ }));
54
+ // 下载项目文件或目录
55
+ server.tool('project_download', '下载项目源码到本地(自动识别文件/目录,打包zip解压)。目标项目需已上传文件或关联源码目录。跨项目时先用kg_list_projects获取项目ID', {
56
+ remotePath: z.string().describe('远程路径,可以是目录(如"src")或单个文件(如"src/main.ts")'),
57
+ localPath: z.string().optional().describe('本地保存路径(不填则保存到系统临时目录,目录不存在会自动创建)'),
58
+ targetProject: z.string().optional().describe('目标项目ID或名称(不填=当前项目)。ID可从kg_list_projects获取')
59
+ }, async (args) => safeTool(async () => {
60
+ const result = args.targetProject
61
+ ? await client().crossDownload(args.targetProject, args.remotePath, args.localPath)
62
+ : await client().download(args.remotePath, args.localPath);
63
+ const prefix = args.targetProject ? crossPrefix(args.targetProject) : '';
64
+ return wrap(`${prefix}✅ 已下载并解压\n\n- 本地路径: ${result.localPath}\n- 文件数量: ${result.fileCount}`);
65
+ }));
66
+ }
@@ -1,2 +1,6 @@
1
+ /**
2
+ * MCP 工具注册入口
3
+ * 将 25 个工具拆分为 5 个子模块,统一在此注册
4
+ */
1
5
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- export declare function registerTools(server: McpServer, projectId: string, _user: string): void;
6
+ export declare function registerTools(server: McpServer, projectId: string, user: string): void;