UNPKG

@snapspecter/mcp-meta-mind

Version:

Meta Mind MCP Server - Advanced Model Context Protocol server for intelligent task management, workflow orchestration, and automatic archiving with hierarchical structures and agent specialization

676 lines 29.7 kB
import { TaskRepository } from "./taskRepository.js"; import { TaskStatus, TaskPriority, NotFoundError, InvalidOperationError, } from "./interfaces.js"; import * as fs from "node:fs/promises"; import * as path from "node:path"; import * as os from "node:os"; const DEFAULT_DATA_DIR = path.join(os.homedir(), ".meta_mind"); const COMPLETED_TASK_SUMMARIES_DIR_PATH = path.join(DEFAULT_DATA_DIR, "completed_task_summaries"); export class TaskManagerServer { taskRepository; isInitialized = false; constructor() { this.taskRepository = new TaskRepository(); } async init() { if (this.isInitialized) return; // Ensure directories exist for summary files await fs.mkdir(DEFAULT_DATA_DIR, { recursive: true }); await fs.mkdir(COMPLETED_TASK_SUMMARIES_DIR_PATH, { recursive: true }); this.isInitialized = true; console.log("Meta Mind MCP Server initialized with SQLite backend."); console.log(`Database path: ${this.taskRepository.constructor.name}`); console.log(`Task summaries directory: ${COMPLETED_TASK_SUMMARIES_DIR_PATH}`); } _assertInitialized() { if (!this.isInitialized) { throw new Error("TaskManagerServer is not initialized. Call init() first."); } } _getRequestEntryOrThrow(requestId) { const request = this.taskRepository.findRequestById(requestId); if (!request) { throw new NotFoundError(`Request '${requestId}' not found.`); } return request; } _getTaskOrThrow(requestEntry, taskId) { const task = requestEntry.tasks.find((t) => t.id === taskId); if (!task) { throw new NotFoundError(`Task '${taskId}' not found in request '${requestEntry.requestId}'.`); } return task; } _getTaskFromAnyRequestOrThrow(taskId) { const task = this.taskRepository.findTaskById(taskId); if (!task) { throw new NotFoundError(`Task '${taskId}' not found.`); } return task; } _areAllDescendantsStatus(task, targetStatus) { if (!task.subtaskIds || task.subtaskIds.length === 0) { return task.status === targetStatus; } for (const subtaskId of task.subtaskIds) { const subtask = this.taskRepository.findTaskById(subtaskId); if (!subtask || !this._areAllDescendantsStatus(subtask, targetStatus)) { return false; } } return task.status === targetStatus; } _isTaskTreeFullyTerminal(task) { const terminalStatuses = [TaskStatus.Done, TaskStatus.Failed]; if (!terminalStatuses.includes(task.status)) return false; if (!task.subtaskIds || task.subtaskIds.length === 0) { return true; } for (const subtaskId of task.subtaskIds) { const subtask = this.taskRepository.findTaskById(subtaskId); if (!subtask || !this._isTaskTreeFullyTerminal(subtask)) { return false; } } return true; } _collectTaskWithDescendants(task, collected = []) { collected.push(task); if (task.subtaskIds) { for (const subtaskId of task.subtaskIds) { const subtask = this.taskRepository.findTaskById(subtaskId); if (subtask) { this._collectTaskWithDescendants(subtask, collected); } } } return collected; } async _autoArchiveTaskTree(requestId, task) { if (!this._isTaskTreeFullyTerminal(task)) { return ""; } const request = this._getRequestEntryOrThrow(requestId); const allTasksToArchive = this._collectTaskWithDescendants(task); const taskIds = allTasksToArchive.map((t) => t.id); // Archive the task tree const archivedCount = this.taskRepository.archiveTaskTree(taskIds, requestId, request.originalRequest); if (archivedCount > 0) { // Check if request is now complete const remainingTasks = this.taskRepository.findTasksByRequestId(requestId); if (remainingTasks.length === 0) { this.taskRepository.updateRequestCompletion(requestId, true); } return ` Task tree '${task.id}' and ${archivedCount} descendant(s) auto-archived.`; } return ""; } async requestPlanning(params) { this._assertInitialized(); const { originalRequest, splitDetails, tasks } = params; // Create the request const requestId = this.taskRepository.createRequest(originalRequest, splitDetails || ""); // Create tasks const now = new Date().toISOString(); const createdTasks = []; for (const taskDef of tasks) { const taskId = this.taskRepository.getNextTaskId(); const task = { id: taskId, title: taskDef.title, description: taskDef.description, status: TaskStatus.Pending, priority: taskDef.priority || TaskPriority.Medium, type: taskDef.type, dependsOn: taskDef.dependsOn || [], parentId: undefined, subtaskIds: [], artifactsGenerated: taskDef.artifactsGenerated || [], environmentContext: taskDef.environmentContext, createdAt: now, updatedAt: now, }; this.taskRepository.createTask(task, requestId); createdTasks.push(task); } return { status: "planned", requestId, tasksCreated: createdTasks.length, message: `Request '${requestId}' planned with ${createdTasks.length} task(s).`, }; } async getNextTask(params) { this._assertInitialized(); const { requestId } = params; const request = this._getRequestEntryOrThrow(requestId); if (request.completed) { return { status: "request_completed", message: `Request '${requestId}' is already completed.`, }; } const tasks = this.taskRepository.findTasksByRequestId(requestId); // Find next actionable task (pending with no unmet dependencies) for (const task of tasks) { if (task.status !== TaskStatus.Pending) continue; // Check if all dependencies are met const hasUnmetDependencies = (task.dependsOn || []).some((depId) => { const depTask = this.taskRepository.findTaskById(depId); return !depTask || depTask.status !== TaskStatus.Done; }); if (!hasUnmetDependencies) { return { status: "next_task", task: { id: task.id, title: task.title, priority: task.priority, type: task.type, status: task.status, }, message: `Next task for '${requestId}': '${task.title}'.\n${this._formatTaskProgressTable(requestId)}`, }; } } return { status: "no_actionable_tasks", message: `No actionable tasks found for request '${requestId}'. All tasks are either completed, failed, or waiting for dependencies.`, }; } async markTaskDone(params) { this._assertInitialized(); const { requestId, taskId, completedDetails, artifactsGenerated } = params; const request = this._getRequestEntryOrThrow(requestId); const task = this._getTaskOrThrow(request, taskId); if (task.status === TaskStatus.Done) { return { status: "already_done", message: "Task already done." }; } if (task.status === TaskStatus.Failed) { throw new InvalidOperationError("Task failed. Cannot mark as done."); } // Update task in database this.taskRepository.updateTaskStatus(taskId, TaskStatus.Done, completedDetails || "Completed successfully."); // Update artifacts if provided if (artifactsGenerated) { this.taskRepository.updateTask(taskId, { artifactsGenerated }); } let message = `Task '${taskId}' marked done.`; // Handle parent completion logic const parentCompletionResult = await this._handleParentCompletion(requestId, task); message += parentCompletionResult.messageAugmentation; // Check if entire request is complete const remainingTasks = this.taskRepository.findTasksByRequestId(requestId); const allTasksTerminal = remainingTasks.every((t) => t.status === TaskStatus.Done || t.status === TaskStatus.Failed); if (allTasksTerminal) { this.taskRepository.updateRequestCompletion(requestId, true); message += " Request fully completed!"; } return { status: "done", message, taskProgress: this._formatTaskProgressTable(requestId), }; } async markTaskFailed(params) { this._assertInitialized(); const { requestId, taskId, reason, suggestedRetryStrategy } = params; const request = this._getRequestEntryOrThrow(requestId); const task = this._getTaskOrThrow(request, taskId); if (task.status === TaskStatus.Done) { throw new InvalidOperationError("Task is already done. Cannot mark as failed."); } if (task.status === TaskStatus.Failed) { return { status: "already_failed", message: "Task already failed." }; } // Update task failure in database this.taskRepository.updateTaskFailure(taskId, reason || "Task failed without specific reason.", suggestedRetryStrategy); let message = `Task '${taskId}' marked failed.`; if (reason) message += ` Reason: ${reason}`; if (suggestedRetryStrategy) message += ` Retry strategy: ${suggestedRetryStrategy}`; return { status: "failed", message, taskProgress: this._formatTaskProgressTable(requestId), }; } async updateTask(params) { this._assertInitialized(); const { requestId, taskId, ...updates } = params; const request = this._getRequestEntryOrThrow(requestId); const task = this._getTaskOrThrow(request, taskId); // Convert status update to use our status method if needed if (updates.status) { this.taskRepository.updateTaskStatus(taskId, updates.status); delete updates.status; } // Update other fields const changedCount = this.taskRepository.updateTask(taskId, updates); if (changedCount === 0) { return { status: "no_changes", message: "No changes were made to the task.", }; } return { status: "updated", message: `Task '${taskId}' updated successfully.`, }; } async deleteTask(params) { this._assertInitialized(); const { requestId, taskId } = params; const request = this._getRequestEntryOrThrow(requestId); const task = this._getTaskOrThrow(request, taskId); // Collect all tasks to delete (task + descendants) const allTasksToDelete = this._collectTaskWithDescendants(task); const taskIds = allTasksToDelete.map((t) => t.id); // Remove from parent's subtaskIds if this task has a parent if (task.parentId) { this.taskRepository.removeSubtask(task.parentId, taskId); } // Delete all tasks let deletedCount = 0; for (const id of taskIds) { deletedCount += this.taskRepository.deleteTask(id); } return { status: "deleted", message: `Task '${taskId}' and ${deletedCount - 1} descendant(s) deleted.`, }; } async addSubtask(params) { this._assertInitialized(); const { requestId, parentTaskId, subtaskTitle, subtaskDescription, priority, type, dependsOn, artifactsGenerated, environmentContext, } = params; const request = this._getRequestEntryOrThrow(requestId); const parentTask = this._getTaskOrThrow(request, parentTaskId); const subtaskId = this.taskRepository.getNextTaskId(); const now = new Date().toISOString(); const subtask = { id: subtaskId, title: subtaskTitle, description: subtaskDescription, status: TaskStatus.Pending, priority: priority || TaskPriority.Medium, type, dependsOn: dependsOn || [], parentId: parentTaskId, subtaskIds: [], artifactsGenerated: artifactsGenerated || [], environmentContext, createdAt: now, updatedAt: now, }; // Create subtask and update parent this.taskRepository.createTask(subtask, requestId); this.taskRepository.addSubtask(parentTaskId, subtaskId); return { status: "subtask_added", subtaskId, message: `Subtask '${subtaskId}' added to parent '${parentTaskId}'.`, }; } async removeSubtask(params) { this._assertInitialized(); const { requestId, subtaskId, parentTaskId } = params; const request = this._getRequestEntryOrThrow(requestId); const subtask = this._getTaskOrThrow(request, subtaskId); const actualParentId = parentTaskId || subtask.parentId; if (!actualParentId) { throw new InvalidOperationError("Cannot remove subtask: no parent specified and task has no parent."); } // Remove from parent and delete task tree this.taskRepository.removeSubtask(actualParentId, subtaskId); const allTasksToDelete = this._collectTaskWithDescendants(subtask); let deletedCount = 0; for (const task of allTasksToDelete) { deletedCount += this.taskRepository.deleteTask(task.id); } return { status: "subtask_removed", message: `Subtask '${subtaskId}' and ${deletedCount - 1} descendant(s) removed.`, }; } async archiveTaskTree(params) { this._assertInitialized(); const { requestId, taskId } = params; const request = this._getRequestEntryOrThrow(requestId); const task = this._getTaskOrThrow(request, taskId); if (!this._areAllDescendantsStatus(task, TaskStatus.Done)) { throw new InvalidOperationError("Cannot archive: task tree is not fully completed."); } const allTasksToArchive = this._collectTaskWithDescendants(task); const taskIds = allTasksToArchive.map((t) => t.id); const archivedCount = this.taskRepository.archiveTaskTree(taskIds, requestId, request.originalRequest); // Check if request is now complete const remainingTasks = this.taskRepository.findTasksByRequestId(requestId); if (remainingTasks.length === 0) { this.taskRepository.updateRequestCompletion(requestId, true); } return { status: "archived", archivedCount, message: `Task tree '${taskId}' with ${archivedCount} task(s) archived successfully.`, }; } async logTaskCompletionSummary(params) { this._assertInitialized(); const { requestId, taskId, summaryMarkdownContent, artifactsGenerated } = params; const request = this._getRequestEntryOrThrow(requestId); const task = this._getTaskOrThrow(request, taskId); const summaryFileName = `${taskId}_completion_summary.md`; const summaryFilePath = path.join(COMPLETED_TASK_SUMMARIES_DIR_PATH, summaryFileName); await fs.writeFile(summaryFilePath, summaryMarkdownContent, "utf-8"); // Update task with summary file path and artifacts const updates = { summaryFilePath }; if (artifactsGenerated) { updates.artifactsGenerated = artifactsGenerated; } this.taskRepository.updateTask(taskId, updates); return { status: "summary_logged", summaryFilePath, message: `Completion summary for task '${taskId}' saved to ${summaryFilePath}.`, }; } async openTaskDetails(params) { this._assertInitialized(); const { taskId } = params; const task = this._getTaskFromAnyRequestOrThrow(taskId); return { status: "task_details", task, }; } async listRequests(params) { this._assertInitialized(); const requests = this.taskRepository.findAllRequests(); return { status: "requests_listed", requests: this._formatRequestsList(requests), count: requests.length, }; } async addTasksToRequest(params) { this._assertInitialized(); const { requestId, tasks } = params; const request = this._getRequestEntryOrThrow(requestId); const now = new Date().toISOString(); const createdTasks = []; for (const taskDef of tasks) { const taskId = this.taskRepository.getNextTaskId(); const task = { id: taskId, title: taskDef.title, description: taskDef.description, status: TaskStatus.Pending, priority: taskDef.priority || TaskPriority.Medium, type: taskDef.type, dependsOn: taskDef.dependsOn || [], parentId: undefined, subtaskIds: [], artifactsGenerated: taskDef.artifactsGenerated || [], environmentContext: taskDef.environmentContext, createdAt: now, updatedAt: now, }; this.taskRepository.createTask(task, requestId); createdTasks.push(task); } return { status: "tasks_added", tasksCreated: createdTasks.length, message: `${createdTasks.length} task(s) added to request '${requestId}'.`, }; } async addDependency(params) { this._assertInitialized(); const { requestId, taskId, dependsOnTaskId } = params; const request = this._getRequestEntryOrThrow(requestId); this._getTaskOrThrow(request, taskId); this._getTaskOrThrow(request, dependsOnTaskId); const updated = this.taskRepository.addDependency(taskId, dependsOnTaskId); if (updated === 0) { return { status: "dependency_exists", message: "Dependency already exists or task not found.", }; } return { status: "dependency_added", message: `Task '${taskId}' now depends on '${dependsOnTaskId}'.`, }; } async removeDependency(params) { this._assertInitialized(); const { requestId, taskId, dependsOnTaskId } = params; const request = this._getRequestEntryOrThrow(requestId); this._getTaskOrThrow(request, taskId); const updated = this.taskRepository.removeDependency(taskId, dependsOnTaskId); if (updated === 0) { return { status: "dependency_not_found", message: "Dependency not found or task not found.", }; } return { status: "dependency_removed", message: `Dependency from '${taskId}' to '${dependsOnTaskId}' removed.`, }; } async validateDependencies(params) { this._assertInitialized(); const { requestId } = params; const request = this._getRequestEntryOrThrow(requestId); const tasks = this.taskRepository.findTasksByRequestId(requestId); // Build task map for quick lookup const taskMap = new Map(); tasks.forEach((task) => taskMap.set(task.id, task)); // Check for cycles using DFS function detectCycle(taskId, visited, recStack) { if (recStack.has(taskId)) return true; if (visited.has(taskId)) return false; visited.add(taskId); recStack.add(taskId); const task = taskMap.get(taskId); if (task?.dependsOn) { for (const depId of task.dependsOn) { if (detectCycle(depId, visited, recStack)) return true; } } recStack.delete(taskId); return false; } const visited = new Set(); const issues = []; for (const task of tasks) { if (detectCycle(task.id, visited, new Set())) { issues.push(`Circular dependency detected involving task '${task.id}'.`); } // Check for missing dependencies if (task.dependsOn) { for (const depId of task.dependsOn) { if (!taskMap.has(depId)) { issues.push(`Task '${task.id}' depends on non-existent task '${depId}'.`); } } } } return { status: issues.length === 0 ? "valid" : "invalid", issues, message: issues.length === 0 ? "All dependencies are valid." : `Found ${issues.length} dependency issue(s).`, }; } async splitTask(params) { this._assertInitialized(); const { requestId, taskIdToSplit, newSubtaskDefinitions } = params; const request = this._getRequestEntryOrThrow(requestId); const taskToSplit = this._getTaskOrThrow(request, taskIdToSplit); if (taskToSplit.status === TaskStatus.Done) { throw new InvalidOperationError("Cannot split a completed task."); } const now = new Date().toISOString(); const createdSubtasks = []; // Create subtasks for (const subtaskDef of newSubtaskDefinitions) { const subtaskId = this.taskRepository.getNextTaskId(); const subtask = { id: subtaskId, title: subtaskDef.title, description: subtaskDef.description, status: TaskStatus.Pending, priority: subtaskDef.priority || taskToSplit.priority, type: subtaskDef.type || taskToSplit.type, dependsOn: subtaskDef.dependsOn || [], parentId: taskIdToSplit, subtaskIds: [], artifactsGenerated: subtaskDef.artifactsGenerated || [], environmentContext: subtaskDef.environmentContext || taskToSplit.environmentContext, createdAt: now, updatedAt: now, }; this.taskRepository.createTask(subtask, requestId); this.taskRepository.addSubtask(taskIdToSplit, subtaskId); createdSubtasks.push(subtask); } return { status: "task_split", originalTaskId: taskIdToSplit, createdSubtasks: createdSubtasks.map((t) => ({ id: t.id, title: t.title, })), message: `Task '${taskIdToSplit}' split into ${createdSubtasks.length} subtask(s).`, }; } async mergeTasks(params) { this._assertInitialized(); const { requestId, primaryTaskId, taskIdsToMerge, newTitle, newDescription, newPriority, newType, newEnvironmentContext, newArtifactsGenerated, } = params; const request = this._getRequestEntryOrThrow(requestId); const primaryTask = this._getTaskOrThrow(request, primaryTaskId); // Validate all tasks to merge exist const tasksToMerge = []; for (const taskId of taskIdsToMerge) { const task = this._getTaskOrThrow(request, taskId); tasksToMerge.push(task); } // Collect merged dependencies and artifacts const mergedDependsOn = new Set(primaryTask.dependsOn || []); const mergedArtifacts = new Set(primaryTask.artifactsGenerated || []); for (const task of tasksToMerge) { (task.dependsOn || []).forEach((dep) => mergedDependsOn.add(dep)); (task.artifactsGenerated || []).forEach((art) => mergedArtifacts.add(art)); } // Remove self-references from dependencies const allTaskIds = [primaryTaskId, ...taskIdsToMerge]; allTaskIds.forEach((id) => mergedDependsOn.delete(id)); // Create merged task const now = new Date().toISOString(); const mergedTask = { id: this.taskRepository.getNextTaskId(), title: newTitle || primaryTask.title, description: newDescription || primaryTask.description, status: TaskStatus.Pending, priority: newPriority || primaryTask.priority, type: newType || primaryTask.type, dependsOn: Array.from(mergedDependsOn), parentId: primaryTask.parentId, subtaskIds: [], artifactsGenerated: newArtifactsGenerated || Array.from(mergedArtifacts), environmentContext: newEnvironmentContext || primaryTask.environmentContext, createdAt: now, updatedAt: now, }; // Use transaction to merge tasks this.taskRepository.mergeTasks(allTaskIds, mergedTask, requestId); // Update parent if primary task had one if (primaryTask.parentId) { this.taskRepository.removeSubtask(primaryTask.parentId, primaryTaskId); this.taskRepository.addSubtask(primaryTask.parentId, mergedTask.id); } return { status: "tasks_merged", mergedTaskId: mergedTask.id, originalTaskIds: allTaskIds, message: `Tasks [${allTaskIds.join(", ")}] merged into new task '${mergedTask.id}'.`, }; } async _handleParentCompletion(requestId, completedTask) { let messageAugmentation = ""; let treeCompletionStatus = "none"; if (!completedTask.parentId) { // Check for auto-archiving of root task const archiveMessage = await this._autoArchiveTaskTree(requestId, completedTask); messageAugmentation += archiveMessage; if (archiveMessage) treeCompletionStatus = "archived"; return { messageAugmentation, treeCompletionStatus }; } const parent = this.taskRepository.findTaskById(completedTask.parentId); if (!parent) return { messageAugmentation, treeCompletionStatus }; // Check if all siblings are done const allSiblingsDone = (parent.subtaskIds || []).every((siblingId) => { const sibling = this.taskRepository.findTaskById(siblingId); return sibling?.status === TaskStatus.Done; }); if (allSiblingsDone) { // Mark parent as done this.taskRepository.updateTaskStatus(parent.id, TaskStatus.Done, "All subtasks completed."); messageAugmentation += ` Parent task '${parent.id}' auto-completed.`; // Recursively check parent's parent const grandParentResult = await this._handleParentCompletion(requestId, parent); messageAugmentation += grandParentResult.messageAugmentation; if (grandParentResult.treeCompletionStatus !== "none") { treeCompletionStatus = grandParentResult.treeCompletionStatus; } } return { messageAugmentation, treeCompletionStatus }; } _formatTaskProgressTable(requestId) { const tasks = this.taskRepository.findTasksByRequestId(requestId); if (tasks.length === 0) return "No tasks found."; const lines = ["Task Progress:"]; lines.push("ID | Title | Status | Priority | Type"); lines.push("---|-------|--------|----------|-----"); for (const task of tasks) { const title = task.title.length > 30 ? task.title.substring(0, 27) + "..." : task.title; const status = task.status; const priority = task.priority; const type = task.type || "N/A"; lines.push(`${task.id} | ${title} | ${status} | ${priority} | ${type}`); } return lines.join("\n"); } _formatRequestsList(requests) { if (requests.length === 0) return "No requests found."; const lines = ["Active Requests:"]; lines.push("ID | Original Request | Tasks | Completed | Created"); lines.push("---|------------------|-------|-----------|--------"); for (const req of requests) { const originalRequest = req.originalRequest.length > 40 ? req.originalRequest.substring(0, 37) + "..." : req.originalRequest; const taskCount = req.tasks.length; const completed = req.completed ? "Yes" : "No"; const created = new Date(req.createdAt).toLocaleDateString(); lines.push(`${req.requestId} | ${originalRequest} | ${taskCount} | ${completed} | ${created}`); } return lines.join("\n"); } } //# sourceMappingURL=taskManagerServer.js.map