@holder-mcp/local-knowledge-base
Version:
Holder公司本地知识库MCP客户端,提供项目文档检索、模块信息查询和架构信息获取等工具
542 lines (537 loc) • 19.9 kB
JavaScript
;
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