UNPKG

mcp-memory-taskmanager

Version:

Intelligent MCP Memory System with Task Manager - Domain-specific knowledge organization and autonomous task management for AI assistants

525 lines 24.5 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import * as fs from 'fs-extra'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); class MCPMemoryTaskManager { config; server; constructor() { this.config = { memoryPath: process.env.MEMORY_FILE_PATH || path.join(process.cwd(), 'memory', 'memory.json'), tasksPath: process.env.TASK_MANAGER_FILE_PATH || path.join(process.cwd(), 'tasks', 'tasks.json'), memoriesBasePath: process.env.MEMORIES_BASE_PATH || path.join(process.cwd(), 'memories'), enableBackup: process.env.ENABLE_BACKUP === 'true', maxMemorySize: parseInt(process.env.MAX_MEMORY_SIZE || '10000') }; this.server = new Server({ name: 'mcp-memory-taskmanager', version: '1.0.0', }, { capabilities: { tools: {}, }, }); this.setupToolHandlers(); this.ensureDirectories(); } async ensureDirectories() { const memoryDir = path.dirname(this.config.memoryPath); const tasksDir = path.dirname(this.config.tasksPath); await fs.ensureDir(memoryDir); await fs.ensureDir(tasksDir); await fs.ensureDir(this.config.memoriesBasePath); // Initialize files if they don't exist if (!await fs.pathExists(this.config.memoryPath)) { await fs.outputFile(this.config.memoryPath, JSON.stringify({ memories: [], domains: {} }, null, 2)); } if (!await fs.pathExists(this.config.tasksPath)) { await fs.outputFile(this.config.tasksPath, JSON.stringify({ tasks: [], requests: {} }, null, 2)); } // Ensure domain-specific memory directories exist const domains = ['frontend', 'backend', 'ui_design', 'ux_design', 'devops', 'architecture', 'mobile', 'database', 'testing', 'security']; for (const domain of domains) { const domainDir = path.join(this.config.memoriesBasePath, domain); await fs.ensureDir(domainDir); const domainMemoryPath = path.join(domainDir, 'memory.json'); if (!await fs.pathExists(domainMemoryPath)) { const defaultDomainMemory = { domain, description: `Specialized memory for ${domain} development`, entries: [], metadata: { created_at: new Date().toISOString(), last_updated: new Date().toISOString(), total_entries: 0, technologies: [], patterns: [] }, stats: { success_rate: 0.0, most_used_technologies: [], recent_learnings: [], knowledge_gaps: [] } }; await fs.outputFile(domainMemoryPath, JSON.stringify(defaultDomainMemory, null, 2)); } } } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'store_memory', description: 'Store information in the memory system with domain classification', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'The information to store' }, domain: { type: 'string', description: 'Domain classification. Specialized domains: frontend, backend, ui_design, ux_design, devops, architecture, mobile, database, testing, security. Other domains will use general memory.' }, technology: { type: 'string', description: 'Specific technology or framework (e.g., React, Node.js, Docker, etc.)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags for categorization' }, importance: { type: 'number', description: 'Importance level (1-10)', minimum: 1, maximum: 10 } }, required: ['content', 'domain'] } }, { name: 'search_memory', description: 'Search stored memories by content, domain, or tags', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, domain: { type: 'string', description: 'Filter by domain' }, tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags' }, limit: { type: 'number', description: 'Maximum number of results', default: 10 } }, required: ['query'] } }, { name: 'request_planning', description: 'Register a new user request and plan its associated tasks', inputSchema: { type: 'object', properties: { originalRequest: { type: 'string', description: 'The original user request' }, splitDetails: { type: 'string', description: 'Details about how the request was split' }, tasks: { type: 'array', items: { type: 'object', properties: { title: { type: 'string' }, description: { type: 'string' } }, required: ['title', 'description'] } } }, required: ['originalRequest', 'tasks'] } }, { name: 'get_next_task', description: 'Get the next pending task for a request', inputSchema: { type: 'object', properties: { requestId: { type: 'string', description: 'The request ID' } }, required: ['requestId'] } }, { name: 'mark_task_done', description: 'Mark a task as completed', inputSchema: { type: 'object', properties: { requestId: { type: 'string', description: 'The request ID' }, taskId: { type: 'string', description: 'The task ID' }, completedDetails: { type: 'string', description: 'Details about task completion' } }, required: ['requestId', 'taskId'] } }, { name: 'approve_task_completion', description: 'Approve a completed task', inputSchema: { type: 'object', properties: { requestId: { type: 'string', description: 'The request ID' }, taskId: { type: 'string', description: 'The task ID' } }, required: ['requestId', 'taskId'] } }, { name: 'approve_request_completion', description: 'Approve the completion of an entire request', inputSchema: { type: 'object', properties: { requestId: { type: 'string', description: 'The request ID' } }, required: ['requestId'] } } ] }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'store_memory': return await this.storeMemory(args); case 'search_memory': return await this.searchMemory(args); case 'request_planning': return await this.requestPlanning(args); case 'get_next_task': return await this.getNextTask(args); case 'mark_task_done': return await this.markTaskDone(args); case 'approve_task_completion': return await this.approveTaskCompletion(args); case 'approve_request_completion': return await this.approveRequestCompletion(args); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { throw new McpError(ErrorCode.InternalError, `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`); } }); } async getDomainMemoryPath(domain) { return path.join(this.config.memoriesBasePath, domain, 'memory.json'); } async loadDomainMemory(domain) { const domainPath = await this.getDomainMemoryPath(domain); if (await fs.pathExists(domainPath)) { return await fs.readJson(domainPath); } throw new Error(`Domain memory not found: ${domain}`); } async saveDomainMemory(domain, memoryData) { const domainPath = await this.getDomainMemoryPath(domain); memoryData.metadata.last_updated = new Date().toISOString(); memoryData.metadata.total_entries = memoryData.entries.length; await fs.writeJson(domainPath, memoryData, { spaces: 2 }); } async storeMemory(args) { try { const { content, domain = 'general', tags = [], importance = 5, technology = '' } = args; // Store in domain-specific memory if domain is recognized const recognizedDomains = ['frontend', 'backend', 'ui_design', 'ux_design', 'devops', 'architecture', 'mobile', 'database', 'testing', 'security']; if (recognizedDomains.includes(domain)) { const domainMemory = await this.loadDomainMemory(domain); const newEntry = { id: Date.now().toString(), timestamp: new Date().toISOString(), content, technology, tags, importance, context: { source: 'mcp-memory-taskmanager' } }; domainMemory.entries.push(newEntry); // Update metadata if (technology && !domainMemory.metadata.technologies.includes(technology)) { domainMemory.metadata.technologies.push(technology); } await this.saveDomainMemory(domain, domainMemory); return { content: [{ type: 'text', text: `Memory stored successfully in ${domain} domain with ID: ${newEntry.id}${technology ? ` (Technology: ${technology})` : ''}` }] }; } else { // Fallback to general memory system const memoryData = await fs.readJson(this.config.memoryPath); const newMemory = { id: Date.now().toString(), timestamp: new Date().toISOString(), content, domain, tags, importance }; memoryData.memories.push(newMemory); if (!memoryData.domains[domain]) { memoryData.domains[domain] = []; } memoryData.domains[domain].push(newMemory.id); await fs.writeJson(this.config.memoryPath, memoryData, { spaces: 2 }); return { content: [{ type: 'text', text: `Memory stored successfully with ID: ${newMemory.id} in domain: ${domain}` }] }; } } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to store memory: ${error}`); } } async searchMemory(args) { try { const { query, domain, limit = 10 } = args; let allResults = []; const recognizedDomains = ['frontend', 'backend', 'ui_design', 'ux_design', 'devops', 'architecture', 'mobile', 'database', 'testing', 'security']; // Search in domain-specific memories if (domain && recognizedDomains.includes(domain)) { try { const domainMemory = await this.loadDomainMemory(domain); const domainResults = domainMemory.entries.filter((entry) => { return entry.content.toLowerCase().includes(query.toLowerCase()) || entry.tags.some((tag) => tag.toLowerCase().includes(query.toLowerCase())) || (entry.technology && entry.technology.toLowerCase().includes(query.toLowerCase())); }).map(entry => ({ ...entry, source: `${domain} domain` })); allResults.push(...domainResults); } catch (error) { console.warn(`Could not search in ${domain} domain:`, error); } } else if (!domain) { // Search in all domain-specific memories for (const domainName of recognizedDomains) { try { const domainMemory = await this.loadDomainMemory(domainName); const domainResults = domainMemory.entries.filter((entry) => { return entry.content.toLowerCase().includes(query.toLowerCase()) || entry.tags.some((tag) => tag.toLowerCase().includes(query.toLowerCase())) || (entry.technology && entry.technology.toLowerCase().includes(query.toLowerCase())); }).map(entry => ({ ...entry, source: `${domainName} domain` })); allResults.push(...domainResults); } catch (error) { console.warn(`Could not search in ${domainName} domain:`, error); } } } // Also search in general memory system try { const data = await fs.readJson(this.config.memoryPath); const generalResults = data.memories.filter((memory) => { const matchesQuery = memory.content.toLowerCase().includes(query.toLowerCase()) || memory.tags.some((tag) => tag.toLowerCase().includes(query.toLowerCase())); const matchesDomain = !domain || memory.domain === domain; return matchesQuery && matchesDomain; }).map((memory) => ({ ...memory, source: 'general memory' })); allResults.push(...generalResults); } catch (error) { console.warn('Could not search in general memory:', error); } // Sort by importance and recency allResults.sort((a, b) => { const importanceDiff = b.importance - a.importance; if (importanceDiff !== 0) return importanceDiff; return new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime(); }); const results = allResults.slice(0, limit); return { content: [{ type: 'text', text: `Found ${results.length} memories:\n\n${results.map((memory) => `ID: ${memory.id}\nSource: ${memory.source}\nDomain: ${memory.domain || 'N/A'}\nTechnology: ${memory.technology || 'N/A'}\nContent: ${memory.content}\nTags: ${memory.tags.join(', ')}\nImportance: ${memory.importance}\nTimestamp: ${memory.timestamp}\n`).join('\n')}` }] }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to search memory: ${error}`); } } async requestPlanning(args) { const data = await fs.readJson(this.config.tasksPath); const requestId = Date.now().toString(); const request = { id: requestId, originalRequest: args.originalRequest, splitDetails: args.splitDetails, tasks: args.tasks.map((task, index) => ({ id: `${requestId}_${index}`, title: task.title, description: task.description, status: 'pending', createdAt: new Date().toISOString() })), status: 'active', createdAt: new Date().toISOString() }; data.requests[requestId] = request; await fs.outputFile(this.config.tasksPath, JSON.stringify(data, null, 2)); return { content: [{ type: 'text', text: `Request planned successfully with ID: ${requestId}\n\nTasks created:\n${request.tasks.map((t) => `- ${t.title}: ${t.description}`).join('\n')}` }] }; } async getNextTask(args) { const data = await fs.readJson(this.config.tasksPath); const request = data.requests[args.requestId]; if (!request) { throw new Error(`Request ${args.requestId} not found`); } const nextTask = request.tasks.find((task) => task.status === 'pending'); if (!nextTask) { return { content: [{ type: 'text', text: 'All tasks completed. Request awaits approval for completion.' }] }; } return { content: [{ type: 'text', text: JSON.stringify(nextTask, null, 2) }] }; } async markTaskDone(args) { const data = await fs.readJson(this.config.tasksPath); const request = data.requests[args.requestId]; if (!request) { throw new Error(`Request ${args.requestId} not found`); } const task = request.tasks.find((t) => t.id === args.taskId); if (!task) { throw new Error(`Task ${args.taskId} not found`); } task.status = 'done'; task.completedAt = new Date().toISOString(); task.completedDetails = args.completedDetails; await fs.outputFile(this.config.tasksPath, JSON.stringify(data, null, 2)); return { content: [{ type: 'text', text: `Task ${args.taskId} marked as done. Awaiting approval.` }] }; } async approveTaskCompletion(args) { const data = await fs.readJson(this.config.tasksPath); const request = data.requests[args.requestId]; if (!request) { throw new Error(`Request ${args.requestId} not found`); } const task = request.tasks.find((t) => t.id === args.taskId); if (!task) { throw new Error(`Task ${args.taskId} not found`); } task.status = 'approved'; task.approvedAt = new Date().toISOString(); await fs.outputFile(this.config.tasksPath, JSON.stringify(data, null, 2)); return { content: [{ type: 'text', text: `Task ${args.taskId} approved. You can proceed to the next task.` }] }; } async approveRequestCompletion(args) { const data = await fs.readJson(this.config.tasksPath); const request = data.requests[args.requestId]; if (!request) { throw new Error(`Request ${args.requestId} not found`); } request.status = 'completed'; request.completedAt = new Date().toISOString(); await fs.outputFile(this.config.tasksPath, JSON.stringify(data, null, 2)); return { content: [{ type: 'text', text: `Request ${args.requestId} fully completed and approved!` }] }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('MCP Memory TaskManager server running on stdio'); } } // Run the server const server = new MCPMemoryTaskManager(); server.run().catch(console.error); //# sourceMappingURL=index.js.map