Skip to main content
Glama

Memory MCP Server

by cbuntingde
index.ts20.2 kB
#!/usr/bin/env node /** * @copyright 2025 Chris Bunting <cbuntingde@gmail.com> * @license MIT * * Memory MCP Server - Implements three types of memory for vertical agents: * - Short-term memory: retains details within a session * - Long-term memory: stores demographics, contact details, and preferences * - Episodic memory: connects past experiences with present conversations */ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js"; import * as fs from 'fs'; import * as path from 'path'; // Memory type definitions interface ShortTermMemory { sessionId: string; data: Record<string, any>; timestamp: number; expiresAt: number; } interface LongTermMemory { userId: string; demographics?: { age?: number; location?: string; occupation?: string; }; contact?: { email?: string; phone?: string; preferredContact?: string; }; preferences?: { seating?: string; temperature?: string; communicationStyle?: string; interests?: string[]; }; lastUpdated: number; } interface EpisodicMemory { id: string; userId: string; sessionId?: string; event: string; context: string; outcome?: string; sentiment?: 'positive' | 'negative' | 'neutral'; timestamp: number; tags?: string[]; } // Memory storage class MemoryStore { private shortTermMemory: Map<string, ShortTermMemory> = new Map(); private longTermMemory: Map<string, LongTermMemory> = new Map(); private episodicMemory: Map<string, EpisodicMemory> = new Map(); private dataDir: string; constructor() { this.dataDir = path.join(process.cwd(), 'memory-data'); this.ensureDataDirectory(); this.loadPersistedData(); } private ensureDataDirectory(): void { if (!fs.existsSync(this.dataDir)) { fs.mkdirSync(this.dataDir, { recursive: true }); } } private loadPersistedData(): void { try { // Load long-term memory const longTermPath = path.join(this.dataDir, 'long-term.json'); if (fs.existsSync(longTermPath)) { const data = JSON.parse(fs.readFileSync(longTermPath, 'utf8')); Object.entries(data).forEach(([userId, memory]: [string, any]) => { this.longTermMemory.set(userId, memory); }); } // Load episodic memory const episodicPath = path.join(this.dataDir, 'episodic.json'); if (fs.existsSync(episodicPath)) { const data = JSON.parse(fs.readFileSync(episodicPath, 'utf8')); Object.entries(data).forEach(([id, memory]: [string, any]) => { this.episodicMemory.set(id, memory); }); } } catch (error) { console.error('Error loading persisted data:', error); } } private persistLongTermMemory(): void { try { const data = Object.fromEntries(this.longTermMemory); fs.writeFileSync( path.join(this.dataDir, 'long-term.json'), JSON.stringify(data, null, 2) ); } catch (error) { console.error('Error persisting long-term memory:', error); } } private persistEpisodicMemory(): void { try { const data = Object.fromEntries(this.episodicMemory); fs.writeFileSync( path.join(this.dataDir, 'episodic.json'), JSON.stringify(data, null, 2) ); } catch (error) { console.error('Error persisting episodic memory:', error); } } // Short-term memory operations setShortTermMemory(sessionId: string, key: string, value: any, ttlMinutes: number = 30): void { const expiresAt = Date.now() + (ttlMinutes * 60 * 1000); const existing = this.shortTermMemory.get(sessionId); if (existing && existing.expiresAt > Date.now()) { existing.data[key] = value; existing.expiresAt = expiresAt; } else { this.shortTermMemory.set(sessionId, { sessionId, data: { [key]: value }, timestamp: Date.now(), expiresAt }); } } getShortTermMemory(sessionId: string, key?: string): any { const memory = this.shortTermMemory.get(sessionId); if (!memory || memory.expiresAt <= Date.now()) { this.shortTermMemory.delete(sessionId); return null; } return key ? memory.data[key] : memory.data; } clearExpiredShortTermMemory(): void { const now = Date.now(); for (const [sessionId, memory] of this.shortTermMemory.entries()) { if (memory.expiresAt <= now) { this.shortTermMemory.delete(sessionId); } } } // Long-term memory operations setLongTermMemory(userId: string, data: Partial<LongTermMemory>): void { const existing = this.longTermMemory.get(userId) || { userId, lastUpdated: Date.now() }; const updated = { ...existing, ...data, lastUpdated: Date.now() }; this.longTermMemory.set(userId, updated); this.persistLongTermMemory(); } getLongTermMemory(userId: string): LongTermMemory | null { return this.longTermMemory.get(userId) || null; } // Episodic memory operations addEpisodicMemory(memory: Omit<EpisodicMemory, 'id' | 'timestamp'>): string { const id = `episodic_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const episodicMemory: EpisodicMemory = { ...memory, id, timestamp: Date.now() }; this.episodicMemory.set(id, episodicMemory); this.persistEpisodicMemory(); return id; } getEpisodicMemory(userId: string, limit?: number): EpisodicMemory[] { const memories = Array.from(this.episodicMemory.values()) .filter(memory => memory.userId === userId) .sort((a, b) => b.timestamp - a.timestamp); return limit ? memories.slice(0, limit) : memories; } searchEpisodicMemory(userId: string, query: string, limit?: number): EpisodicMemory[] { const searchTerm = query.toLowerCase(); const memories = Array.from(this.episodicMemory.values()) .filter(memory => memory.userId === userId && (memory.event.toLowerCase().includes(searchTerm) || memory.context.toLowerCase().includes(searchTerm) || memory.outcome?.toLowerCase().includes(searchTerm) || memory.tags?.some(tag => tag.toLowerCase().includes(searchTerm))) ) .sort((a, b) => b.timestamp - a.timestamp); return limit ? memories.slice(0, limit) : memories; } } // Initialize memory store const memoryStore = new MemoryStore(); // Create MCP server const server = new Server( { name: "memory-server", version: "0.1.0", }, { capabilities: { resources: {}, tools: {}, prompts: {}, }, } ); // Resource handlers server.setRequestHandler(ListResourcesRequestSchema, async () => { memoryStore.clearExpiredShortTermMemory(); const resources = []; // Add long-term memory resources for (const [userId] of memoryStore['longTermMemory'].entries()) { resources.push({ uri: `memory://long-term/${userId}`, mimeType: "application/json", name: `Long-term memory for ${userId}`, description: "Demographics, contact details, and preferences" }); } // Add episodic memory resources for (const [userId] of memoryStore['longTermMemory'].entries()) { resources.push({ uri: `memory://episodic/${userId}`, mimeType: "application/json", name: `Episodic memory for ${userId}`, description: "Past experiences and events" }); } return { resources }; }); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); const [type, userId] = url.pathname.replace(/^\//, '').split('/'); if (type === 'long-term') { const memory = memoryStore.getLongTermMemory(userId); if (!memory) { throw new McpError(ErrorCode.InvalidRequest, `Long-term memory not found for user: ${userId}`); } return { contents: [{ uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(memory, null, 2) }] }; } else if (type === 'episodic') { const memories = memoryStore.getEpisodicMemory(userId); return { contents: [{ uri: request.params.uri, mimeType: "application/json", text: JSON.stringify(memories, null, 2) }] }; } throw new McpError(ErrorCode.InvalidRequest, `Invalid memory resource type: ${type}`); }); // Tool handlers server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "set_short_term_memory", description: "Store data in short-term memory for a session", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session identifier" }, key: { type: "string", description: "Memory key" }, value: { description: "Memory value (any JSON-serializable data)" }, ttlMinutes: { type: "number", description: "Time to live in minutes (default: 30)", default: 30 } }, required: ["sessionId", "key", "value"] } }, { name: "get_short_term_memory", description: "Retrieve data from short-term memory", inputSchema: { type: "object", properties: { sessionId: { type: "string", description: "Session identifier" }, key: { type: "string", description: "Memory key (optional, returns all if not provided)" } }, required: ["sessionId"] } }, { name: "set_long_term_memory", description: "Store user demographics, contact details, or preferences", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User identifier" }, demographics: { type: "object", description: "User demographics (age, location, occupation, etc.)" }, contact: { type: "object", description: "Contact information" }, preferences: { type: "object", description: "User preferences and settings" } }, required: ["userId"] } }, { name: "get_long_term_memory", description: "Retrieve long-term memory for a user", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User identifier" } }, required: ["userId"] } }, { name: "add_episodic_memory", description: "Add a new episodic memory (past experience or event)", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User identifier" }, sessionId: { type: "string", description: "Optional session identifier" }, event: { type: "string", description: "Description of the event" }, context: { type: "string", description: "Context surrounding the event" }, outcome: { type: "string", description: "Outcome or resolution of the event" }, sentiment: { type: "string", enum: ["positive", "negative", "neutral"], description: "Sentiment of the experience" }, tags: { type: "array", items: { type: "string" }, description: "Tags for categorizing the memory" } }, required: ["userId", "event", "context"] } }, { name: "get_episodic_memory", description: "Retrieve episodic memories for a user", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User identifier" }, limit: { type: "number", description: "Maximum number of memories to return" } }, required: ["userId"] } }, { name: "search_episodic_memory", description: "Search episodic memories by content", inputSchema: { type: "object", properties: { userId: { type: "string", description: "User identifier" }, query: { type: "string", description: "Search query" }, limit: { type: "number", description: "Maximum number of results" } }, required: ["userId", "query"] } } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case "set_short_term_memory": { const { sessionId, key, value, ttlMinutes = 30 } = request.params.arguments as any; memoryStore.setShortTermMemory(sessionId, key, value, ttlMinutes); return { content: [{ type: "text", text: `Stored short-term memory for session ${sessionId}: ${key}` }] }; } case "get_short_term_memory": { const { sessionId, key } = request.params.arguments as any; const memory = memoryStore.getShortTermMemory(sessionId, key); return { content: [{ type: "text", text: JSON.stringify(memory, null, 2) }] }; } case "set_long_term_memory": { const { userId, demographics, contact, preferences } = request.params.arguments as any; memoryStore.setLongTermMemory(userId, { demographics, contact, preferences }); return { content: [{ type: "text", text: `Updated long-term memory for user ${userId}` }] }; } case "get_long_term_memory": { const { userId } = request.params.arguments as any; const memory = memoryStore.getLongTermMemory(userId); return { content: [{ type: "text", text: JSON.stringify(memory, null, 2) }] }; } case "add_episodic_memory": { const memoryData = request.params.arguments as any; const id = memoryStore.addEpisodicMemory(memoryData); return { content: [{ type: "text", text: `Added episodic memory with ID: ${id}` }] }; } case "get_episodic_memory": { const { userId, limit } = request.params.arguments as any; const memories = memoryStore.getEpisodicMemory(userId, limit); return { content: [{ type: "text", text: JSON.stringify(memories, null, 2) }] }; } case "search_episodic_memory": { const { userId, query, limit } = request.params.arguments as any; const memories = memoryStore.searchEpisodicMemory(userId, query, limit); return { content: [{ type: "text", text: JSON.stringify(memories, null, 2) }] }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } }); // Prompt handlers server.setRequestHandler(ListPromptsRequestSchema, async () => { return { prompts: [ { name: "memory_summary", description: "Generate a comprehensive memory summary for a user" }, { name: "personalization_insights", description: "Get personalization insights based on user memories" } ] }; }); server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === "memory_summary") { const userId = args?.userId as string; if (!userId) { throw new McpError(ErrorCode.InvalidParams, "userId is required for memory summary"); } const longTerm = memoryStore.getLongTermMemory(userId); const episodic = memoryStore.getEpisodicMemory(userId, 10); return { messages: [ { role: "user", content: { type: "text", text: `Please provide a comprehensive memory summary for user ${userId}. Include:` } }, { role: "user", content: { type: "text", text: "1. Key demographic and preference information" } }, { role: "user", content: { type: "text", text: "2. Recent experiences and patterns" } }, { role: "user", content: { type: "text", text: "3. Insights for personalization and proactive support" } }, { role: "user", content: { type: "resource", resource: { uri: `memory://long-term/${userId}`, mimeType: "application/json", text: JSON.stringify(longTerm, null, 2) } } }, { role: "user", content: { type: "resource", resource: { uri: `memory://episodic/${userId}`, mimeType: "application/json", text: JSON.stringify(episodic, null, 2) } } } ] }; } else if (name === "personalization_insights") { const userId = args?.userId as string; if (!userId) { throw new McpError(ErrorCode.InvalidParams, "userId is required for personalization insights"); } const longTerm = memoryStore.getLongTermMemory(userId); const episodic = memoryStore.getEpisodicMemory(userId, 20); return { messages: [ { role: "user", content: { type: "text", text: `Based on the user's memory data, provide personalization insights for:` } }, { role: "user", content: { type: "text", text: "1. Preferred communication style and approach" } }, { role: "user", content: { type: "text", text: "2. Anticipated needs based on past experiences" } }, { role: "user", content: { type: "text", text: "3. Opportunities for proactive personalization" } }, { role: "user", content: { type: "resource", resource: { uri: `memory://long-term/${userId}`, mimeType: "application/json", text: JSON.stringify(longTerm, null, 2) } } }, { role: "user", content: { type: "resource", resource: { uri: `memory://episodic/${userId}`, mimeType: "application/json", text: JSON.stringify(episodic, null, 2) } } } ] }; } throw new McpError(ErrorCode.MethodNotFound, `Unknown prompt: ${name}`); }); // Start the server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Memory MCP Server running on stdio"); } main().catch((error) => { console.error("Server error:", 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/cbuntingde/memory-mcp-server'

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