MCP Memory Server
by ebailey78
- src
import fs from 'fs/promises';
import path from 'path';
import { Memory, Metadata, MemoryType } from './types.js';
// Default memory directory
const DEFAULT_MEMORY_DIR = './memory';
export class MemoryStorage {
private memoryDir: string;
constructor(memoryDir: string = DEFAULT_MEMORY_DIR) {
this.memoryDir = memoryDir;
}
/**
* Initialize the memory storage directory structure
*/
async initialize(): Promise<void> {
// Create main memory directory if it doesn't exist
await fs.mkdir(this.memoryDir, { recursive: true });
// Create subdirectories for each memory type
await fs.mkdir(path.join(this.memoryDir, 'entities'), { recursive: true });
await fs.mkdir(path.join(this.memoryDir, 'concepts'), { recursive: true });
await fs.mkdir(path.join(this.memoryDir, 'sessions'), { recursive: true });
// Create or load metadata
await this.getOrCreateMetadata();
}
/**
* Get or create metadata file
*/
async getOrCreateMetadata(): Promise<Metadata> {
const metadataPath = path.join(this.memoryDir, 'metadata.json');
try {
const data = await fs.readFile(metadataPath, 'utf-8');
return JSON.parse(data) as Metadata;
} catch (error) {
// Create new metadata if file doesn't exist
const metadata: Metadata = {
lastUpdated: new Date().toISOString(),
memoryCount: 0,
indexVersion: 1
};
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
return metadata;
}
}
/**
* Update metadata
*/
async updateMetadata(updates: Partial<Metadata>): Promise<Metadata> {
const metadata = await this.getOrCreateMetadata();
const updatedMetadata = { ...metadata, ...updates, lastUpdated: new Date().toISOString() };
await fs.writeFile(
path.join(this.memoryDir, 'metadata.json'),
JSON.stringify(updatedMetadata, null, 2)
);
return updatedMetadata;
}
/**
* Get the directory path for a memory type
*/
private getTypeDirectory(type: MemoryType): string {
const typeDirs: Record<MemoryType, string> = {
entity: 'entities',
concept: 'concepts',
session: 'sessions'
};
return path.join(this.memoryDir, typeDirs[type]);
}
/**
* Get the file path for a memory
*/
private getMemoryPath(id: string, type: MemoryType): string {
return path.join(this.getTypeDirectory(type), `${id}.md`);
}
/**
* Convert a memory object to markdown format
*/
private memoryToMarkdown(memory: Memory): string {
const frontmatter = {
id: memory.id,
title: memory.title,
type: memory.type,
tags: memory.tags,
created: memory.created,
updated: memory.updated,
related: memory.related,
importance: memory.importance
};
return `---
${Object.entries(frontmatter)
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
.join('\n')}
---
# ${memory.title}
${memory.content}`;
}
/**
* Parse markdown to a memory object
*/
private markdownToMemory(markdown: string, id: string): Memory {
// Extract frontmatter
const frontmatterMatch = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!frontmatterMatch) {
throw new Error(`Invalid memory format for ${id}`);
}
const [, frontmatterStr, content] = frontmatterMatch;
// Parse frontmatter
const frontmatter: Record<string, any> = {};
frontmatterStr.split('\n').forEach(line => {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length) {
try {
frontmatter[key.trim()] = JSON.parse(valueParts.join(':').trim());
} catch {
frontmatter[key.trim()] = valueParts.join(':').trim();
}
}
});
return {
id: frontmatter.id || id,
title: frontmatter.title || '',
type: frontmatter.type || 'concept',
tags: frontmatter.tags || [],
created: frontmatter.created || new Date().toISOString(),
updated: frontmatter.updated || new Date().toISOString(),
related: frontmatter.related || [],
importance: frontmatter.importance || 0.5,
content: content.trim()
};
}
/**
* Save a memory to storage
*/
async saveMemory(memory: Memory): Promise<void> {
const filePath = this.getMemoryPath(memory.id, memory.type);
const markdown = this.memoryToMarkdown(memory);
await fs.writeFile(filePath, markdown);
// Update metadata
const metadata = await this.getOrCreateMetadata();
await this.updateMetadata({
memoryCount: metadata.memoryCount + 1,
indexVersion: metadata.indexVersion + 1
});
}
/**
* Get a memory by ID and type
*/
async getMemory(id: string, type: MemoryType): Promise<Memory | null> {
try {
const filePath = this.getMemoryPath(id, type);
const markdown = await fs.readFile(filePath, 'utf-8');
return this.markdownToMemory(markdown, id);
} catch (error) {
return null;
}
}
/**
* Update an existing memory
*/
async updateMemory(memory: Memory): Promise<void> {
const filePath = this.getMemoryPath(memory.id, memory.type);
try {
// Check if file exists
await fs.access(filePath);
// Update the memory
const markdown = this.memoryToMarkdown({
...memory,
updated: new Date().toISOString()
});
await fs.writeFile(filePath, markdown);
// Update metadata
await this.updateMetadata({
indexVersion: (await this.getOrCreateMetadata()).indexVersion + 1
});
} catch (error) {
throw new Error(`Memory ${memory.id} not found`);
}
}
/**
* Delete a memory
*/
async deleteMemory(id: string, type: MemoryType): Promise<void> {
const filePath = this.getMemoryPath(id, type);
try {
await fs.unlink(filePath);
// Update metadata
const metadata = await this.getOrCreateMetadata();
await this.updateMetadata({
memoryCount: Math.max(0, metadata.memoryCount - 1),
indexVersion: metadata.indexVersion + 1
});
} catch (error) {
// Ignore if file doesn't exist
}
}
/**
* List all memories
*/
async listMemories(): Promise<Memory[]> {
const memories: Memory[] = [];
// Process each type directory
for (const type of ['entity', 'concept', 'session'] as MemoryType[]) {
const typeDir = this.getTypeDirectory(type);
try {
const files = await fs.readdir(typeDir);
for (const file of files) {
if (file.endsWith('.md')) {
const id = path.basename(file, '.md');
const filePath = path.join(typeDir, file);
const content = await fs.readFile(filePath, 'utf-8');
try {
const memory = this.markdownToMemory(content, id);
memories.push(memory);
} catch (error) {
console.error(`Error parsing memory ${id}:`, error);
}
}
}
} catch (error) {
console.error(`Error reading directory ${typeDir}:`, error);
}
}
return memories;
}
}