Skip to main content
Glama
index.ts14.7 kB
// RAGServer implementation with MCP tools import { randomUUID } from 'node:crypto' import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' import { DocumentChunker } from '../chunker/index.js' import { Embedder } from '../embedder/index.js' import { DocumentParser } from '../parser/index.js' import { type VectorChunk, VectorStore } from '../vectordb/index.js' // ============================================ // Type Definitions // ============================================ /** * RAGServer configuration */ export interface RAGServerConfig { /** LanceDB database path */ dbPath: string /** Transformers.js model path */ modelName: string /** Model cache directory */ cacheDir: string /** Document base directory */ baseDir: string /** Maximum file size (100MB) */ maxFileSize: number /** Chunk size */ chunkSize: number /** Chunk overlap */ chunkOverlap: number } /** * query_documents tool input */ export interface QueryDocumentsInput { /** Natural language query */ query: string /** Number of results to retrieve (default 5) */ limit?: number } /** * ingest_file tool input */ export interface IngestFileInput { /** File path */ filePath: string } /** * delete_file tool input */ export interface DeleteFileInput { /** File path */ filePath: string } /** * ingest_file tool output */ export interface IngestResult { /** File path */ filePath: string /** Chunk count */ chunkCount: number /** Timestamp */ timestamp: string } /** * query_documents tool output */ export interface QueryResult { /** File path */ filePath: string /** Chunk index */ chunkIndex: number /** Text */ text: string /** Similarity score */ score: number } // ============================================ // RAGServer Class // ============================================ /** * RAG server compliant with MCP Protocol * * Responsibilities: * - MCP tool integration (4 tools) * - Tool handler implementation * - Error handling * - Initialization (LanceDB, Transformers.js) */ export class RAGServer { private readonly server: Server private readonly vectorStore: VectorStore private readonly embedder: Embedder private readonly chunker: DocumentChunker private readonly parser: DocumentParser constructor(config: RAGServerConfig) { this.server = new Server( { name: 'rag-mcp-server', version: '1.0.0' }, { capabilities: { tools: {} } } ) // Component initialization this.vectorStore = new VectorStore({ dbPath: config.dbPath, tableName: 'chunks' }) this.embedder = new Embedder({ modelPath: config.modelName, batchSize: 8, cacheDir: config.cacheDir, }) this.chunker = new DocumentChunker({ chunkSize: config.chunkSize, chunkOverlap: config.chunkOverlap, }) this.parser = new DocumentParser({ baseDir: config.baseDir, maxFileSize: config.maxFileSize, }) this.setupHandlers() } /** * Set up MCP handlers */ private setupHandlers(): void { // Tool list this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'query_documents', description: 'Search through previously ingested documents (PDF, DOCX, TXT, MD) using semantic search. Returns relevant passages from documents in the BASE_DIR. Documents must be ingested first using ingest_file.', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language search query (e.g., "transformer architecture", "API documentation")', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 5, max recommended: 20)', }, }, required: ['query'], }, }, { name: 'ingest_file', description: 'Ingest a document file (PDF, DOCX, TXT, MD) into the vector database for semantic search. File path must be an absolute path. Supports re-ingestion to update existing documents.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Absolute path to the file to ingest. Example: "/Users/user/documents/manual.pdf"', }, }, required: ['filePath'], }, }, { name: 'delete_file', description: 'Delete a previously ingested file from the vector database. Removes all chunks and embeddings associated with the specified file. File path must be an absolute path. This operation is idempotent - deleting a non-existent file completes without error.', inputSchema: { type: 'object', properties: { filePath: { type: 'string', description: 'Absolute path to the file to delete from the database. Example: "/Users/user/documents/manual.pdf"', }, }, required: ['filePath'], }, }, { name: 'list_files', description: 'List all ingested files in the vector database. Returns file paths and chunk counts for each document.', inputSchema: { type: 'object', properties: {} }, }, { name: 'status', description: 'Get system status including total documents, total chunks, database size, and configuration information.', inputSchema: { type: 'object', properties: {} }, }, ], })) // Tool invocation this.server.setRequestHandler( CallToolRequestSchema, async (request: { params: { name: string; arguments?: unknown } }) => { switch (request.params.name) { case 'query_documents': return await this.handleQueryDocuments( request.params.arguments as unknown as QueryDocumentsInput ) case 'ingest_file': return await this.handleIngestFile( request.params.arguments as unknown as IngestFileInput ) case 'delete_file': return await this.handleDeleteFile( request.params.arguments as unknown as DeleteFileInput ) case 'list_files': return await this.handleListFiles() case 'status': return await this.handleStatus() default: throw new Error(`Unknown tool: ${request.params.name}`) } } ) } /** * Initialization */ async initialize(): Promise<void> { await this.vectorStore.initialize() await this.chunker.initialize() console.error('RAGServer initialized') } /** * query_documents tool handler */ async handleQueryDocuments( args: QueryDocumentsInput ): Promise<{ content: [{ type: 'text'; text: string }] }> { try { // Generate query embedding const queryVector = await this.embedder.embed(args.query) // Vector search const searchResults = await this.vectorStore.search(queryVector, args.limit || 5) // Format results const results: QueryResult[] = searchResults.map((result) => ({ filePath: result.filePath, chunkIndex: result.chunkIndex, text: result.text, score: result.score, })) return { content: [ { type: 'text', text: JSON.stringify(results, null, 2), }, ], } } catch (error) { console.error('Failed to query documents:', error) throw error } } /** * ingest_file tool handler (re-ingestion support, transaction processing, rollback capability) */ async handleIngestFile( args: IngestFileInput ): Promise<{ content: [{ type: 'text'; text: string }] }> { let backup: VectorChunk[] | null = null try { // Parse file const text = await this.parser.parseFile(args.filePath) // Split text into chunks const chunks = await this.chunker.chunkText(text) // Generate embeddings const embeddings = await this.embedder.embedBatch(chunks.map((chunk) => chunk.text)) // Create backup (if existing data exists) try { const existingFiles = await this.vectorStore.listFiles() const existingFile = existingFiles.find((file) => file.filePath === args.filePath) if (existingFile && existingFile.chunkCount > 0) { // Backup existing data (retrieve via search) const queryVector = embeddings[0] || [] if (queryVector.length === 384) { const allChunks = await this.vectorStore.search(queryVector, 20) // Retrieve max 20 items backup = allChunks .filter((chunk) => chunk.filePath === args.filePath) .map((chunk) => ({ id: randomUUID(), filePath: chunk.filePath, chunkIndex: chunk.chunkIndex, text: chunk.text, vector: queryVector, // Use dummy vector since actual vector cannot be retrieved metadata: chunk.metadata, timestamp: new Date().toISOString(), })) } console.error(`Backup created: ${backup?.length || 0} chunks for ${args.filePath}`) } } catch (error) { // Backup creation failure is warning only (for new files) console.warn('Failed to create backup (new file?):', error) } // Delete existing data await this.vectorStore.deleteChunks(args.filePath) console.error(`Deleted existing chunks for: ${args.filePath}`) // Create vector chunks const timestamp = new Date().toISOString() const vectorChunks: VectorChunk[] = chunks.map((chunk, index) => { const embedding = embeddings[index] if (!embedding) { throw new Error(`Missing embedding for chunk ${index}`) } return { id: randomUUID(), filePath: args.filePath, chunkIndex: chunk.index, text: chunk.text, vector: embedding, metadata: { fileName: args.filePath.split('/').pop() || args.filePath, fileSize: text.length, fileType: args.filePath.split('.').pop() || '', }, timestamp, } }) // Insert vectors (transaction processing) try { await this.vectorStore.insertChunks(vectorChunks) console.error(`Inserted ${vectorChunks.length} chunks for: ${args.filePath}`) // Delete backup on success backup = null } catch (insertError) { // Rollback on error if (backup && backup.length > 0) { console.error('Ingestion failed, rolling back...', insertError) try { await this.vectorStore.insertChunks(backup) console.error(`Rollback completed: ${backup.length} chunks restored`) } catch (rollbackError) { console.error('Rollback failed:', rollbackError) throw new Error( `Failed to ingest file and rollback failed: ${(insertError as Error).message}` ) } } throw insertError } // Result const result: IngestResult = { filePath: args.filePath, chunkCount: chunks.length, timestamp, } return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], } } catch (error) { // Error handling: suppress stack trace in production const errorMessage = process.env['NODE_ENV'] === 'production' ? (error as Error).message : (error as Error).stack || (error as Error).message console.error('Failed to ingest file:', errorMessage) throw new Error(`Failed to ingest file: ${errorMessage}`) } } /** * list_files tool handler (Phase 1: basic implementation) */ async handleListFiles(): Promise<{ content: [{ type: 'text'; text: string }] }> { try { const files = await this.vectorStore.listFiles() return { content: [ { type: 'text', text: JSON.stringify(files, null, 2), }, ], } } catch (error) { console.error('Failed to list files:', error) throw error } } /** * status tool handler (Phase 1: basic implementation) */ async handleStatus(): Promise<{ content: [{ type: 'text'; text: string }] }> { try { const status = await this.vectorStore.getStatus() return { content: [ { type: 'text', text: JSON.stringify(status, null, 2), }, ], } } catch (error) { console.error('Failed to get status:', error) throw error } } /** * delete_file tool handler */ async handleDeleteFile( args: DeleteFileInput ): Promise<{ content: [{ type: 'text'; text: string }] }> { try { // Validate and normalize file path (S-002 security requirement) this.parser.validateFilePath(args.filePath) // Delete chunks from vector database await this.vectorStore.deleteChunks(args.filePath) // Return success message const result = { filePath: args.filePath, deleted: true, timestamp: new Date().toISOString(), } return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], } } catch (error) { // Error handling: suppress stack trace in production const errorMessage = process.env['NODE_ENV'] === 'production' ? (error as Error).message : (error as Error).stack || (error as Error).message console.error('Failed to delete file:', errorMessage) throw new Error(`Failed to delete file: ${errorMessage}`) } } /** * Start the server */ async run(): Promise<void> { const transport = new StdioServerTransport() await this.server.connect(transport) console.error('RAGServer running on stdio transport') } }

Implementation Reference

Latest Blog Posts

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/shinpr/mcp-local-rag'

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