MCP Memory Server
by ebailey78
- src
import lunr from 'lunr';
import fs from 'fs/promises';
import path from 'path';
import { Memory, SearchResult, MemoryType } from './types.js';
import { MemoryStorage } from './storage.js';
export class MemorySearch {
private indexPath: string;
private storage: MemoryStorage;
private index: lunr.Index | null = null;
private memoryMap: Map<string, Memory> = new Map();
constructor(storage: MemoryStorage, indexPath?: string) {
this.storage = storage;
// Use the memory directory from storage for the index path
this.indexPath = indexPath || path.join(storage['memoryDir'], 'index.json');
}
/**
* Initialize the search index
*/
async initialize(): Promise<void> {
try {
// Try to load existing index
await this.loadIndex();
} catch (error) {
// If index doesn't exist, build a new one
await this.rebuildIndex();
}
}
/**
* Load the search index from disk
*/
private async loadIndex(): Promise<void> {
try {
const indexData = await fs.readFile(this.indexPath, 'utf-8');
const { index, memories } = JSON.parse(indexData);
this.index = lunr.Index.load(index);
this.memoryMap = new Map(Object.entries(memories));
} catch (error) {
throw new Error('Failed to load search index');
}
}
/**
* Save the search index to disk
*/
private async saveIndex(): Promise<void> {
if (!this.index) {
throw new Error('Index not initialized');
}
const indexData = {
index: this.index.toJSON(),
memories: Object.fromEntries(this.memoryMap.entries())
};
// Ensure the directory exists
const indexDir = path.dirname(this.indexPath);
await fs.mkdir(indexDir, { recursive: true });
await fs.writeFile(this.indexPath, JSON.stringify(indexData));
}
/**
* Rebuild the search index from all memories
*/
async rebuildIndex(): Promise<void> {
// Get all memories from storage
const memories = await this.storage.listMemories();
// Build memory map for quick lookups
this.memoryMap = new Map();
for (const memory of memories) {
this.memoryMap.set(memory.id, memory);
}
// Build Lunr index
this.index = lunr(function() {
this.ref('id');
this.field('title', { boost: 10 });
this.field('content');
this.field('tags', { boost: 5 });
this.field('type');
// Add each memory to the index
for (const memory of memories) {
this.add({
id: memory.id,
title: memory.title,
content: memory.content,
tags: memory.tags.join(' '),
type: memory.type
});
}
});
// Save the index to disk
await this.saveIndex();
}
/**
* Add a memory to the index
*/
async addToIndex(memory: Memory): Promise<void> {
if (!this.index) {
await this.initialize();
}
// Update memory map
this.memoryMap.set(memory.id, memory);
// Rebuild index (for simplicity in this POC)
// In a production system, we would use lunr.Index.prototype.update
await this.rebuildIndex();
}
/**
* Remove a memory from the index
*/
async removeFromIndex(id: string): Promise<void> {
if (!this.index) {
await this.initialize();
}
// Remove from memory map
this.memoryMap.delete(id);
// Rebuild index (for simplicity in this POC)
await this.rebuildIndex();
}
/**
* Update a memory in the index
*/
async updateIndex(memory: Memory): Promise<void> {
await this.addToIndex(memory);
}
/**
* Extract a preview snippet from memory content
*/
private extractPreview(content: string, query: string, maxLength: number = 150): string {
// Simple preview extraction - in a real system, this would be more sophisticated
const lowerContent = content.toLowerCase();
const lowerQuery = query.toLowerCase();
// Try to find the query in the content
const index = lowerContent.indexOf(lowerQuery);
if (index >= 0) {
// Calculate start and end positions for the preview
const start = Math.max(0, index - 50);
const end = Math.min(content.length, index + query.length + 100);
// Extract the preview
let preview = content.substring(start, end);
// Add ellipsis if needed
if (start > 0) preview = '...' + preview;
if (end < content.length) preview = preview + '...';
return preview;
}
// If query not found, return the beginning of the content
return content.length > maxLength
? content.substring(0, maxLength) + '...'
: content;
}
/**
* Search memories
*/
async search(
query: string,
options: {
types?: MemoryType[],
tags?: string[],
limit?: number
} = {}
): Promise<SearchResult[]> {
if (!this.index) {
await this.initialize();
}
const { types, tags, limit = 10 } = options;
// Build Lunr query
let lunrQuery = query;
// Add type filters if specified
if (types && types.length > 0) {
lunrQuery += ' ' + types.map(type => `+type:${type}`).join(' ');
}
// Add tag filters if specified
if (tags && tags.length > 0) {
lunrQuery += ' ' + tags.map(tag => `+tags:${tag}`).join(' ');
}
try {
// Execute search
const results = this.index!.search(lunrQuery);
// Map results to SearchResult objects
return results
.slice(0, limit)
.map(result => {
const memory = this.memoryMap.get(result.ref);
if (!memory) {
throw new Error(`Memory ${result.ref} not found in memory map`);
}
return {
id: memory.id,
title: memory.title,
type: memory.type,
tags: memory.tags,
score: result.score,
preview: this.extractPreview(memory.content, query),
created: memory.created,
updated: memory.updated
};
});
} catch (error) {
console.error('Search error:', error);
return [];
}
}
/**
* List memories with optional filtering
*/
async list(
options: {
types?: MemoryType[],
tags?: string[],
limit?: number
} = {}
): Promise<SearchResult[]> {
const { types, tags, limit } = options;
// Get all memories from the map
let memories = Array.from(this.memoryMap.values());
// Filter by type if specified
if (types && types.length > 0) {
memories = memories.filter(memory => types.includes(memory.type));
}
// Filter by tags if specified
if (tags && tags.length > 0) {
memories = memories.filter(memory =>
tags.some(tag => memory.tags.includes(tag))
);
}
// Sort by updated date (newest first)
memories.sort((a, b) =>
new Date(b.updated).getTime() - new Date(a.updated).getTime()
);
// Apply limit if specified
if (limit) {
memories = memories.slice(0, limit);
}
// Convert to SearchResult format
return memories.map(memory => ({
id: memory.id,
title: memory.title,
type: memory.type,
tags: memory.tags,
score: 1.0, // Default score for listing
preview: memory.content.substring(0, 150) + (memory.content.length > 150 ? '...' : ''),
created: memory.created,
updated: memory.updated
}));
}
}