Skip to main content
Glama

Knowledge Retrieval Server

by cskwork
index.ts14 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, TextContent } from '@modelcontextprotocol/sdk/types.js'; import { loadConfig } from './config/config.js'; import { DocumentLoader } from './services/DocumentLoader.js'; import { DocumentRepository } from './services/DocumentRepository.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; // 전역 변수 let repository: DocumentRepository | null = null; let config: any = null; let isInitialized = false; let isInitializing = false; console.error(`[DEBUG] Module loaded at ${new Date().toISOString()}`); // PID 파일 경로 const PID_FILE = path.join(os.tmpdir(), 'mcp-knowledge-retrieval.pid'); /** * 프로세스 잠금 확인 및 설정 */ function checkAndSetProcessLock(): void { try { if (fs.existsSync(PID_FILE)) { const existingPid = fs.readFileSync(PID_FILE, 'utf8').trim(); // 기존 프로세스가 실행 중인지 확인 try { process.kill(parseInt(existingPid), 0); // 시그널 0으로 프로세스 존재 확인 console.error(`[ERROR] MCP server already running with PID ${existingPid}`); console.error(`[ERROR] Kill existing process first: kill ${existingPid}`); process.exit(1); } catch (e) { // 프로세스가 존재하지 않으면 PID 파일 제거 console.error(`[DEBUG] Stale PID file found, removing...`); fs.unlinkSync(PID_FILE); } } // 현재 프로세스 PID 저장 fs.writeFileSync(PID_FILE, process.pid.toString()); console.error(`[DEBUG] Process lock acquired, PID: ${process.pid}`); // 프로세스 종료 시 PID 파일 정리 process.on('exit', () => { try { if (fs.existsSync(PID_FILE)) { fs.unlinkSync(PID_FILE); } } catch (e) { // 무시 } }); process.on('SIGINT', () => { console.error(`[DEBUG] Received SIGINT, cleaning up...`); process.exit(0); }); process.on('SIGTERM', () => { console.error(`[DEBUG] Received SIGTERM, cleaning up...`); process.exit(0); }); } catch (error) { console.error(`[ERROR] Failed to set process lock: ${error}`); process.exit(1); } } /** * MCP 서버 초기화 */ async function initializeServer() { console.error(`[DEBUG] initializeServer() started, isInitialized=${isInitialized}, isInitializing=${isInitializing}`); if (isInitialized && repository?.isInitialized()) { console.error(`[DEBUG] Already initialized, skipping. Repository has ${repository.getStatistics().totalDocuments} documents`); return; } if (isInitializing) { console.error(`[DEBUG] Already initializing, waiting...`); // 초기화 완료까지 대기 while (isInitializing && !isInitialized) { await new Promise(resolve => setTimeout(resolve, 100)); } return; } try { isInitializing = true; // 설정 로드 console.error(`[DEBUG] Loading config...`); config = await loadConfig(); console.error(`[DEBUG] Config loaded`); console.error(`[DEBUG] config.documentSource.basePath=${config.documentSource.basePath}`); console.error(`[DEBUG] config.documentSource.domains length=${config.documentSource.domains.length}`); config.documentSource.domains.forEach((d: {name: string; path: string; category?: string})=> console.error(`[DEBUG] domain-> name:${d.name}, path:${d.path}`) ); // 문서 로드 console.error(`[DEBUG] Creating DocumentLoader...`); const loader = new DocumentLoader(config.documentSource); console.error(`[DEBUG] Loading documents...`); const documents = await loader.loadAllDocuments(); console.error(`[DEBUG] Documents loaded: ${documents.length} documents`); // 저장소 생성 및 초기화 (기존 repository가 있으면 재사용하지 않고 새로 생성) console.error(`[DEBUG] Creating and initializing DocumentRepository...`); if (!repository) { repository = new DocumentRepository(); } // repository가 이미 초기화되어 있다면 재초기화하지 않음 if (!repository.isInitialized()) { await repository.initialize(documents); // 비동기 초기화 대기 } else { console.error(`[DEBUG] Repository already initialized, skipping re-initialization`); } const stats = repository.getStatistics(); console.error(`[DEBUG] Repository fully initialized`); console.error(`[DEBUG] Repository instance: ${repository ? 'EXISTS' : 'NULL'}`); console.error(`[DEBUG] Repository stats: ${JSON.stringify(stats)}`); isInitialized = true; console.error(`[DEBUG] initializeServer() completed successfully, isInitialized=${isInitialized}`); } catch (error) { console.error(`[DEBUG] Error in initializeServer(): ${error}`); isInitialized = false; throw error; } finally { isInitializing = false; } } /** * 서버가 준비될 때까지 대기 */ async function ensureServerReady() { // 이미 초기화된 경우 기존 repository 사용 if (isInitialized && repository?.isInitialized()) { console.error(`[DEBUG] ensureServerReady: Already initialized, using existing repository with ${repository.getStatistics().totalDocuments} documents`); return; } if (!isInitialized && !isInitializing) { console.error(`[DEBUG] ensureServerReady: Starting initialization...`); await initializeServer(); } else if (isInitializing) { console.error(`[DEBUG] ensureServerReady: Waiting for initialization to complete...`); while (isInitializing) { await new Promise(resolve => setTimeout(resolve, 100)); } } // 초기화 후 상태 검증 if (!repository || !repository.isInitialized()) { throw new Error('Repository initialization failed or corrupted'); } } /** * MCP 서버 생성 및 시작 */ async function main() { console.error(`[DEBUG] Starting main() function at ${new Date().toISOString()}`); // 프로세스 중복 실행 방지 checkAndSetProcessLock(); // 서버 초기화 (문서 로딩 및 인덱싱 완료까지 대기) console.error(`[DEBUG] About to call initializeServer()`); await initializeServer(); console.error(`[DEBUG] initializeServer() completed, repository exists: ${!!repository}`); console.error(`[DEBUG] Repository initialized: ${repository?.isInitialized()}`); // MCP 서버 생성 const server = new Server( { name: config.serverName, version: config.serverVersion, }, { capabilities: { tools: {}, }, } ); // 도구 목록 조회 핸들러 server.setRequestHandler(ListToolsRequestSchema, async () => { // This is a dummy handler, tools are managed by the client if (!config) { try { await ensureServerReady(); } catch(e) { console.error("Failed to initialize server for ListTools", e); } } const tools: Tool[] = [ { name: 'search-documents', description: 'Search documents using BM25 algorithm. Takes keyword arrays and returns relevant document chunks.', inputSchema: { type: 'object', properties: { keywords: { type: 'array', items: { type: 'string' }, description: 'Array of keywords to search for (e.g., ["payment", "API", "authentication"])' }, domain: { type: 'string', description: 'Domain to search in (optional, e.g., "company", "customer")' }, topN: { type: 'number', description: 'Maximum number of results to return (default: 10)', default: 10 } }, required: ['keywords'] } }, { name: 'get-document-by-id', description: 'Retrieve full document by ID.', inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Document ID to retrieve' } }, required: ['id'] } }, { name: 'list-domains', description: 'List all available domains and their document counts.', inputSchema: { type: 'object', properties: {} } }, { name: 'get-chunk-with-context', description: 'Get specific chunk with surrounding context.', inputSchema: { type: 'object', properties: { documentId: { type: 'number', description: 'Document ID' }, chunkId: { type: 'number', description: 'Chunk ID' }, windowSize: { type: 'number', description: 'Context window size (default: 1)', default: 1 } }, required: ['documentId', 'chunkId'] } } ]; return { tools }; }); // 도구 실행 핸들러 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; console.error(`[DEBUG] Tool called: ${name}`); console.error(`[DEBUG] Repository state: ${repository ? 'EXISTS' : 'NULL'}`); // 도구 호출 전에 반드시 서버 준비 상태 확인 try { await ensureServerReady(); } catch (error) { console.error('[FATAL] Server initialization failed during tool call:', error); throw new Error('Server is not initialized and failed to recover. Please check logs and restart.'); } // 상태 로깅 if (repository) { console.error(`[DEBUG] Repository instance ID: ${repository.getInstanceId()}`); console.error(`[DEBUG] Repository documents count: ${repository.getStatistics().totalDocuments}`); } // 이 시점에서 repository는 null이 아님을 보장 if (!repository || !repository.isInitialized()) { throw new Error('Repository is not properly initialized'); } switch (name) { case 'search-documents': { const { keywords, domain, topN } = args as { keywords: string[]; domain?: string; topN?: number; }; const results = await repository.searchDocuments(keywords, { domain, topN, contextWindow: config.chunk.contextWindowSize }); const content: TextContent[] = [{ type: 'text', text: results }]; return { content }; } case 'get-document-by-id': { const { id } = args as { id: number }; const document = repository.getDocumentById(id); if (!document) { const content: TextContent[] = [{ type: 'text', text: `Document with ID ${id} not found.` }]; return { content }; } const content: TextContent[] = [{ type: 'text', text: `# ${document.title}\n\n${document.content}` }]; return { content }; } case 'list-domains': { console.error(`[DEBUG] list-domains called`); const domains = repository.listDomains(); console.error(`[DEBUG] domains found: ${JSON.stringify(domains)}`); if (domains.length === 0) { const content: TextContent[] = [{ type: 'text', text: `## Available Domains\n\nNo domains found. Check if documents are loaded properly.` }]; return { content }; } const domainList = domains .map(d => `- ${d.name}: ${d.documentCount} documents`) .join('\n'); const content: TextContent[] = [{ type: 'text', text: `## Available Domains\n\n${domainList}` }]; return { content }; } case 'get-chunk-with-context': { const { documentId, chunkId, windowSize } = args as { documentId: number; chunkId: number; windowSize?: number; }; const document = repository.getDocumentById(documentId); if (!document) { const content: TextContent[] = [{ type: 'text', text: `Document with ID ${documentId} not found.` }]; return { content }; } const chunks = document.getChunkWithWindow(chunkId, windowSize || 1); if (chunks.length === 0) { const content: TextContent[] = [{ type: 'text', text: `Chunk with ID ${chunkId} not found.` }]; return { content }; } const content = chunks.map(chunk => chunk.text).join('\n\n---\n\n'); const textContent: TextContent[] = [{ type: 'text', text: content }]; return { content: textContent }; } default: throw new Error(`Unknown tool: ${name}`); } }); // STDIO 전송 설정 const transport = new StdioServerTransport(); await server.connect(transport); // MCP server started successfully (silent for protocol) } // 에러 핸들링 process.on('uncaughtException', (error) => { console.error('Uncaught exception:', error); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { console.error('Unhandled rejection at:', promise, 'reason:', reason); process.exit(1); }); // 서버 시작 main().catch((error) => { console.error('Failed to start server:', error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cskwork/keyword-rag-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server