UNPKG

@holder-mcp/local-knowledge-base

Version:

Holder公司本地知识库MCP客户端,提供项目文档检索、模块信息查询和架构信息获取等工具

542 lines (537 loc) 19.9 kB
#!/usr/bin/env node "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MCPKnowledgeBaseServer = void 0; const http = require('http'); const fs = require('fs'); const path = require('path'); const tools_1 = require("./tools"); const document_service_1 = require("./document-service"); // 注意:工具定义已移至 tools.ts 文件中,这里不再重复定义 // 解析命令行参数 function parseArgs(argv) { const args = { serverUrl: 'http://localhost:8888', configFile: './mcp-config.json', projectName: '', documentPaths: [], watchEnabled: true, debounceMs: 30000 }; for (let i = 2; i < argv.length; i++) { switch (argv[i]) { case '--server-url': args.serverUrl = argv[i + 1]; i++; break; case '--config': args.configFile = argv[i + 1]; i++; break; case '--project': args.projectName = argv[i + 1]; i++; break; case '--default-project': args.defaultProject = argv[i + 1]; i++; break; case '--docs': args.documentPaths = argv[i + 1].split(',').map((p) => p.trim()); i++; break; case '--no-watch': args.watchEnabled = false; break; case '--debounce': args.debounceMs = parseInt(argv[i + 1]); i++; break; case '--cwd': args.workingDirectory = argv[i + 1]; i++; break; case '--help': console.log(` Holder本地知识库MCP客户端 使用方法: holder-mcp-kb [选项] 选项: --server-url <url> 服务器地址 (默认: http://localhost:8888) --config <file> 配置文件路径 (默认: ./mcp-config.json) --project <name> 项目名称 (用于单项目模式) --default-project <name> 默认项目名称 (用于知识库查询工具) --docs <paths> 文档路径列表,逗号分隔 (用于单项目模式) --no-watch 禁用文件监听 --debounce <ms> 防抖动延迟时间毫秒 (默认: 30000) --cwd <directory> 指定工作目录 (默认: 自动检测) --help 显示帮助信息 配置文件示例 (mcp-config.json): { "serverUrl": "http://localhost:8888", "documentWatch": { "enabled": true, "debounceMs": 30000, "projects": [ { "name": "my-project", "documentPaths": ["./docs", "./src", "./README.md"], "enabled": true } ] } } 单项目模式示例: holder-mcp-kb --project my-project --docs "./docs,./src,./README.md" 多项目模式示例: holder-mcp-kb --config ./projects-config.json `); process.exit(0); } } return args; } // 加载配置文件 function loadConfig(configFile, args) { let config = { serverUrl: args.serverUrl, timeout: 30000 }; // 尝试加载配置文件 if (fs.existsSync(configFile)) { try { const configData = JSON.parse(fs.readFileSync(configFile, 'utf-8')); config = { ...config, ...configData }; console.log(`✅ 加载配置文件: ${configFile}`); } catch (error) { console.warn(`⚠️ 加载配置文件失败: ${error.message}`); } } // 命令行参数覆盖配置文件 if (args.serverUrl !== 'http://localhost:8888') { config.serverUrl = args.serverUrl; } // 设置默认项目 if (args.defaultProject) { config.defaultProject = args.defaultProject; } // 设置默认文档路径(从命令行参数获取) if (args.documentPaths && args.documentPaths.length > 0) { config.defaultDocumentPaths = args.documentPaths; } // 设置工作目录 if (args.workingDirectory) { config.workingDirectory = args.workingDirectory; } // 单项目模式:通过命令行参数设置 if (args.projectName && args.documentPaths.length > 0) { config.documentWatch = { enabled: args.watchEnabled, debounceMs: args.debounceMs, projects: [{ name: args.projectName, documentPaths: args.documentPaths, enabled: true }] }; } return config; } // MCP知识库服务器 - 集成文档监听功能 class MCPKnowledgeBaseServer { constructor(config) { this.isWatchingStarted = false; this.config = config; this.serverUrl = config.serverUrl; this.documentService = new document_service_1.DocumentService(); } // 调用后端工具(通过HTTP API) async callBackendTool(toolName, args) { return new Promise((resolve, reject) => { let endpoint = ''; let queryParams = new URLSearchParams(); // 验证必需的项目名称参数 let targetProject = args.projectName; if (!targetProject && this.config.defaultProject) { targetProject = this.config.defaultProject; console.log(`使用默认项目: ${targetProject}`); } if (!targetProject) { reject(new Error(`工具 ${toolName} 需要 projectName 参数或配置默认项目`)); return; } const projectName = encodeURIComponent(targetProject); // 根据工具名称构建正确的端点(使用新的多项目API) switch (toolName) { case 'queryKnowledgeBase': endpoint = `/api/v1/projects/${projectName}/knowledge-base/query`; queryParams.append('query', args.query || ''); if (args.topK) queryParams.append('topK', args.topK); break; case 'getModuleInfo': endpoint = `/api/v1/projects/${projectName}/knowledge-base/module/${encodeURIComponent(args.moduleName || '')}`; break; case 'getArchitectureInfo': endpoint = `/api/v1/projects/${projectName}/knowledge-base/architecture`; break; case 'getKnowledgeBaseStats': endpoint = `/api/v1/projects/${projectName}/knowledge-base/stats`; break; case 'searchCodeDocumentation': endpoint = `/api/v1/projects/${projectName}/knowledge-base/search-code`; queryParams.append('codeQuery', args.codeQuery || ''); break; default: reject(new Error(`未知工具: ${toolName}`)); return; } const url = require('url'); const serverUrl = new url.URL(this.serverUrl); const options = { hostname: serverUrl.hostname, port: serverUrl.port || 8888, path: `${endpoint}?${queryParams.toString()}`, method: 'GET', headers: { 'Content-Type': 'application/json' } }; const req = http.request(options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { try { if (res.statusCode === 200) { const result = JSON.parse(responseData); resolve(result); } else { reject(new Error(`HTTP ${res.statusCode}: ${responseData}`)); } } catch (error) { reject(new Error(`解析响应失败: ${error.message}`)); } }); }); req.on('error', (error) => { reject(new Error(`请求失败: ${error.message}`)); }); req.setTimeout(30000, () => { req.destroy(); reject(new Error('请求超时')); }); req.end(); }); } // 处理新的文档相关工具 async handleDocumentTool(toolName, args) { switch (toolName) { case 'rebuildProjectIndex': return await this.handleRebuildProjectIndex(args); case 'uploadProjectDocuments': return await this.handleUploadProjectDocuments(args); default: throw new Error(`未知的文档工具: ${toolName}`); } } // 处理重建项目索引 async handleRebuildProjectIndex(args) { const { projectName } = args; let { documentPaths } = args; if (!projectName) { throw new Error('缺少必需参数: projectName'); } // 如果Agent没有传入documentPaths,使用配置中的默认路径 if (!documentPaths || !Array.isArray(documentPaths) || documentPaths.length === 0) { documentPaths = this.config.defaultDocumentPaths; if (!documentPaths || documentPaths.length === 0) { throw new Error('缺少必需参数: documentPaths,且未配置默认文档路径'); } console.log(`📁 使用配置中的默认文档路径: ${documentPaths.join(', ')}`); } else { console.log(`📁 使用Agent传入的文档路径: ${documentPaths.join(', ')}`); } try { // 1. 首先调用服务端重建索引API(清空现有索引) await this.callRebuildIndexAPI(projectName); // 2. 然后上传新文档 const uploadResult = await this.documentService.uploadProjectDocuments(projectName, documentPaths, this.serverUrl, this.config.workingDirectory); return `🔄 项目索引重建完成\n\n${uploadResult}`; } catch (error) { throw new Error(`重建项目索引失败: ${error.message}`); } } // 调用服务端重建索引API async callRebuildIndexAPI(projectName) { return new Promise((resolve, reject) => { const url = require('url'); const serverUrl = new url.URL(this.serverUrl); const options = { hostname: serverUrl.hostname, port: serverUrl.port || 8888, path: `/api/v1/projects/${encodeURIComponent(projectName)}/knowledge-base/rebuild-index`, method: 'POST', headers: { 'Content-Type': 'application/json' } }; const req = http.request(options, (res) => { let responseData = ''; res.on('data', (chunk) => { responseData += chunk; }); res.on('end', () => { if (res.statusCode === 200) { resolve(); } else { reject(new Error(`重建索引API调用失败: HTTP ${res.statusCode}: ${responseData}`)); } }); }); req.on('error', (error) => { reject(new Error(`重建索引API请求失败: ${error.message}`)); }); req.setTimeout(30000, () => { req.destroy(); reject(new Error('重建索引API请求超时')); }); req.end(); }); } // 处理上传项目文档 async handleUploadProjectDocuments(args) { const { projectName } = args; let { documentPaths } = args; if (!projectName) { throw new Error('缺少必需参数: projectName'); } // 如果Agent没有传入documentPaths,使用配置中的默认路径 if (!documentPaths || !Array.isArray(documentPaths) || documentPaths.length === 0) { documentPaths = this.config.defaultDocumentPaths; if (!documentPaths || documentPaths.length === 0) { throw new Error('缺少必需参数: documentPaths,且未配置默认文档路径'); } console.log(`📁 使用配置中的默认文档路径: ${documentPaths.join(', ')}`); } else { console.log(`📁 使用Agent传入的文档路径: ${documentPaths.join(', ')}`); } return await this.documentService.uploadProjectDocuments(projectName, documentPaths, this.serverUrl, this.config.workingDirectory); } // 启动文档监听(如果配置启用) async startDocumentWatching() { if (this.isWatchingStarted) { return; // 避免重复启动 } const watchConfig = this.config.documentWatch; if (!watchConfig || !watchConfig.enabled || !watchConfig.projects || watchConfig.projects.length === 0) { console.log('📝 文档监听未启用或无项目配置'); return; } console.log('🔄 启动文档监听...'); for (const project of watchConfig.projects) { if (!project.enabled) { console.log(`⏭️ 跳过已禁用的项目: ${project.name}`); continue; } try { const result = await this.documentService.startWatching(project.name, project.documentPaths, watchConfig.debounceMs, this.serverUrl); console.log(result); } catch (error) { console.error(`❌ 启动项目 ${project.name} 监听失败: ${error.message}`); } } this.isWatchingStarted = true; } // 停止文档监听 async stopDocumentWatching() { if (!this.isWatchingStarted) { return; } const watchConfig = this.config.documentWatch; if (watchConfig && watchConfig.projects) { for (const project of watchConfig.projects) { this.documentService.stopWatching(project.name); } } this.isWatchingStarted = false; console.log('🛑 文档监听已停止'); } // 模拟MCP协议方法 async initialize() { return { protocolVersion: '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'Holder-Knowledge-Base-MCP', version: '1.0.0' } }; } async listTools() { return { tools: tools_1.KNOWLEDGE_BASE_TOOLS }; } async callTool(name, args) { try { let result; // 新的文档相关工具 if (['rebuildProjectIndex', 'uploadProjectDocuments'].includes(name)) { result = await this.handleDocumentTool(name, args); } else { // 原有的知识库查询工具 result = await this.callBackendTool(name, args); } return { content: [ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) } ] }; } catch (error) { return { content: [ { type: 'text', text: `调用工具 ${name} 时发生错误: ${error.message}` } ], isError: true }; } } // 启动服务 async start() { console.log('📝 MCP客户端日志: [MCP] Holder知识库客户端启动'); console.log('📝 MCP客户端日志: [MCP] 服务器地址:', this.serverUrl); // 启动文档监听 await this.startDocumentWatching(); console.log('✅ MCP服务启动完成'); } // 关闭服务 async close() { console.log('📝 MCP客户端日志: [MCP] 正在关闭...'); // 停止文档监听 await this.stopDocumentWatching(); console.log('📝 MCP客户端日志: [MCP] 连接已关闭'); } } exports.MCPKnowledgeBaseServer = MCPKnowledgeBaseServer; // 处理来自stdin的MCP消息 async function handleMessage(message, server) { try { const request = JSON.parse(message); let response; switch (request.method) { case 'initialize': response = { jsonrpc: '2.0', id: request.id, result: await server.initialize() }; break; case 'tools/list': response = { jsonrpc: '2.0', id: request.id, result: await server.listTools() }; break; case 'tools/call': try { const toolResult = await server.callTool(request.params.name, request.params.arguments); response = { jsonrpc: '2.0', id: request.id, result: toolResult }; } catch (error) { response = { jsonrpc: '2.0', id: request.id, error: { code: -32603, message: error.message } }; } break; default: response = { jsonrpc: '2.0', id: request.id, error: { code: -32601, message: `方法未找到: ${request.method}` } }; } console.log(JSON.stringify(response)); } catch (error) { console.error('处理消息时发生错误:', error.message); } } // 主函数 async function main() { const args = parseArgs(process.argv); const config = loadConfig(args.configFile, args); const server = new MCPKnowledgeBaseServer(config); // 如果是作为模块使用,导出server if (require.main !== module) { return server; } await server.start(); // 处理stdin输入 process.stdin.setEncoding('utf8'); let buffer = ''; process.stdin.on('data', (chunk) => { buffer += chunk; // 处理完整的行 const lines = buffer.split('\n'); buffer = lines.pop() || ''; // 保留最后一行(可能不完整) for (const line of lines) { if (line.trim()) { handleMessage(line.trim(), server).catch(console.error); } } }); process.stdin.on('end', () => { if (buffer.trim()) { handleMessage(buffer.trim(), server).catch(console.error); } }); // 处理退出信号 process.on('SIGINT', () => { console.error('\n[MCP] 正在关闭...'); server.close().then(() => { process.exit(0); }); }); process.on('SIGTERM', () => { server.close().then(() => { process.exit(0); }); }); } if (require.main === module) { main().catch(console.error); } //# sourceMappingURL=index.js.map