Skip to main content
Glama

Task Orchestration

index.ts14.1 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, Request, } from '@modelcontextprotocol/sdk/types.js'; import { storage } from './storage'; import { Goal, Task, TaskResponse, AddTasksInput, HierarchicalTaskResponse, TaskInput } from './types'; export class SoftwarePlanningServer { private server: Server; private currentGoal: Goal | null = null; constructor() { this.server = new Server( { name: 'software-planning-tool', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.server.onerror = (error: Error) => console.error('[MCP Error]', error); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create_goal', description: 'Create a new goal', inputSchema: { type: 'object', properties: { description: { type: 'string', description: 'The software development goal description (string)', }, repoName: { type: 'string', description: 'Please give the name of the project that you are currently working on (string)', }, }, required: ['description'], }, }, { name: 'add_tasks', description: 'Add multiple tasks to a goal. Tasks can be provided in a hierarchical structure. For tasks that are children of *existing* tasks, use the `parentId` field. The operation is transactional: either all tasks in the batch succeed, or the entire operation fails.', inputSchema: { type: 'object', properties: { goalId: { type: 'number', description: 'ID of the goal to add tasks to (number)', }, tasks: { type: 'array', description: 'An array of task objects to be added. Each task can define nested subtasks.', items: { $ref: '#/definitions/TaskInput' } } }, required: ['goalId', 'tasks'], definitions: { TaskInput: { type: 'object', properties: { title: { type: 'string', description: 'Title of the task (string)' }, description: { type: 'string', description: 'Detailed description of the task (string)' }, parentId: { type: ['string', 'null'], description: 'Optional parent task ID for tasks that are children of *existing* tasks. Do not use for new subtasks defined hierarchically within this batch.' }, subtasks: { type: 'array', description: 'An array of nested subtask objects to be created under this task.', items: { $ref: '#/definitions/TaskInput' } } }, required: ['title', 'description'] } } } }, { name: 'remove_tasks', description: 'Soft-delete multiple tasks from a goal. Tasks are marked as deleted but remain in the system. Task IDs use a dot-notation (e.g., "1", "1.1", "1.1.1"). Responses will return simplified task objects without `createdAt`, `updatedAt`, or `parentId`. Soft-deleted tasks are excluded by default from `get_tasks` results unless `includeDeletedTasks` is set to true.', inputSchema: { type: 'object', properties: { goalId: { type: 'number', description: 'ID of the goal to remove tasks from (number)', }, taskIds: { type: 'array', items: { type: 'string', }, description: 'IDs of the tasks to remove (array of strings). Example: ["1", "1.1"].', }, deleteChildren: { type: 'boolean', description: 'Whether to delete child tasks along with the parent (boolean). Defaults to false. If false, attempting to delete a parent task with existing subtasks will throw an error.', default: false, }, }, required: ['goalId', 'taskIds'], }, }, { name: 'get_tasks', description: 'Get tasks for a goal. Task IDs use a dot-notation (e.g., "1", "1.1", "1.1.1"). When `includeSubtasks` is specified, responses will return hierarchical task objects. Otherwise, simplified task objects without `createdAt`, `updatedAt`, or `parentId` will be returned.', inputSchema: { type: 'object', properties: { goalId: { type: 'number', description: 'ID of the goal to get tasks for (number)', }, taskIds: { type: 'array', items: { type: 'string', }, description: 'Optional: IDs of tasks to fetch (array of strings). If null or empty, all tasks for the goal will be fetched.', }, includeSubtasks: { type: 'string', description: 'Level of subtasks to include: "none" (only top-level tasks), "first-level" (top-level tasks and their direct children), or "recursive" (all nested subtasks). Defaults to "none".', enum: ['none', 'first-level', 'recursive'], default: 'none', }, includeDeletedTasks: { type: 'boolean', description: 'Whether to include soft-deleted tasks in the results (boolean). Defaults to false.', default: false, }, }, required: ['goalId'], }, }, { name: 'complete_task_status', description: 'Update the completion status of tasks. Task IDs use a dot-notation (e.g., "1", "1.1", "1.1.1"). Responses will return simplified task objects without `createdAt`, `updatedAt`, or `parentId`.', inputSchema: { type: 'object', properties: { goalId: { type: 'number', description: 'ID of the goal containing the tasks (number)', }, taskIds: { type: 'array', items: { type: 'string', }, description: 'IDs of the tasks to update (array of strings). Example: ["1.1", "1.2"].', }, completeChildren: { type: 'boolean', description: 'Whether to complete all child tasks recursively (boolean). Defaults to false. If false, a task can only be completed if all its subtasks are already complete.', default: false, }, }, required: ['goalId', 'taskIds'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request: Request) => { if (!request.params) { throw new McpError(ErrorCode.InvalidParams, 'Missing request parameters'); } switch (request.params.name) { case 'create_goal': { const { description, repoName } = request.params.arguments as { description: string; repoName: string }; const goal = await storage.createGoal(description, repoName); this.currentGoal = goal; await storage.createPlan(goal.id); return { content: [ { type: 'text', text: JSON.stringify({ goalId: goal.id }), }, ], }; } case 'add_tasks': { const { goalId, tasks: taskInputs } = request.params.arguments as AddTasksInput; const createdTasks: HierarchicalTaskResponse[] = []; // Helper function to recursively process tasks const processTaskInputRecursively = async ( taskInput: TaskInput, currentGoalId: number, parentTaskId: string | null ): Promise<HierarchicalTaskResponse> => { // Validate parentId if it refers to an existing task if (taskInput.parentId !== undefined && taskInput.parentId !== null) { // Use getTasks to check for existing parent, passing the parentId as an array const existingTasks = await storage.getTasks(currentGoalId, [taskInput.parentId]); if (!existingTasks || existingTasks.length === 0) { throw new McpError(ErrorCode.InvalidParams, `Parent task with ID "${taskInput.parentId}" not found for goal ${currentGoalId}.`); } } // Add the current task const newTask = await storage.addTask(currentGoalId, { title: taskInput.title, description: taskInput.description, parentId: parentTaskId, // Use the parentTaskId from recursion, not taskInput.parentId deleted: false, }); const hierarchicalTaskResponse: HierarchicalTaskResponse = { id: newTask.id, goalId: newTask.goalId, title: newTask.title, description: newTask.description, isComplete: newTask.isComplete, deleted: newTask.deleted, }; // Recursively add subtasks if (taskInput.subtasks && taskInput.subtasks.length > 0) { hierarchicalTaskResponse.subtasks = []; for (const subtaskInput of taskInput.subtasks) { const subtaskResult = await processTaskInputRecursively( subtaskInput, currentGoalId, newTask.id // New task's ID becomes the parent for its subtasks ); hierarchicalTaskResponse.subtasks.push(subtaskResult); } } return hierarchicalTaskResponse; }; // Process top-level tasks for (const taskInput of taskInputs) { const createdTask = await processTaskInputRecursively(taskInput, goalId, taskInput.parentId || null); createdTasks.push(createdTask); } return { content: [ { type: 'text', text: JSON.stringify(createdTasks, null, 2), }, ], }; } case 'remove_tasks': { const { goalId, taskIds, deleteChildren } = request.params.arguments as { goalId: number; taskIds: string[]; deleteChildren?: boolean }; const results = await storage.removeTasks(goalId, taskIds, deleteChildren); const textContent = JSON.stringify(results, null, 2); return { content: [ { type: 'text', text: textContent, }, ], }; } case 'get_tasks': { const { goalId, taskIds, includeSubtasks = 'none', includeDeletedTasks = false } = request.params.arguments as { goalId: number; taskIds?: string[]; includeSubtasks?: 'none' | 'first-level' | 'recursive'; includeDeletedTasks?: boolean; }; // If taskIds are provided, fetch specific tasks. Otherwise, fetch all tasks for the goal. const fetchedTasks: TaskResponse[] = await storage.getTasks(goalId, taskIds && taskIds.length > 0 ? taskIds : undefined, includeSubtasks, includeDeletedTasks); // Map Task objects to TaskResponse objects to match the schema description const taskResponses: TaskResponse[] = fetchedTasks.map(task => ({ id: task.id, goalId: task.goalId, title: task.title, description: task.description, isComplete: task.isComplete, deleted: task.deleted, })); const textContent = JSON.stringify(taskResponses, null, 2); return { content: [ { type: 'text', text: textContent, }, ], }; } case 'complete_task_status': { const { goalId, taskIds, completeChildren } = request.params.arguments as { goalId: number; taskIds: string[]; completeChildren?: boolean; }; const results = await storage.completeTasksStatus(goalId, taskIds, completeChildren); const textContent = JSON.stringify(results, null, 2); return { content: [ { type: 'text', text: textContent, } as { type: 'text'; text: string }, ], }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); } }); } async start() { await storage.initialize(); const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('Software Planning MCP server running on stdio'); } } const server = new SoftwarePlanningServer(); server.start().catch((error) => { console.error('Failed to start server:', 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/hrishirc/task-orchestrator'

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