From 1679075b6b06beba9fecfbd926d75c55bf431c88 Mon Sep 17 00:00:00 2001 From: Eyal Toledano Date: Mon, 31 Mar 2025 13:01:31 -0400 Subject: [PATCH] Implement remove-subtask MCP command for removing subtasks from parent tasks --- .changeset/two-bats-smoke.md | 1 + .../core/direct-functions/remove-subtask.js | 85 +++++++++++++++++++ mcp-server/src/core/task-master-core.js | 7 +- mcp-server/src/tools/index.js | 2 + mcp-server/src/tools/remove-subtask.js | 50 +++++++++++ tasks/task_023.txt | 2 +- tasks/tasks.json | 2 +- 7 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 mcp-server/src/core/direct-functions/remove-subtask.js create mode 100644 mcp-server/src/tools/remove-subtask.js diff --git a/.changeset/two-bats-smoke.md b/.changeset/two-bats-smoke.md index c02325c9..bfb2526b 100644 --- a/.changeset/two-bats-smoke.md +++ b/.changeset/two-bats-smoke.md @@ -12,4 +12,5 @@ - Implement expand-task MCP command for breaking down tasks into subtasks - Implement add-task MCP command for creating new tasks using AI assistance - Implement add-subtask MCP command for adding subtasks to existing tasks +- Implement remove-subtask MCP command for removing subtasks from parent tasks - Document MCP server naming conventions in architecture.mdc and mcp.mdc files (file names use kebab-case, direct functions use camelCase with Direct suffix, tool registration functions use camelCase with Tool suffix, and MCP tool names use snake_case) diff --git a/mcp-server/src/core/direct-functions/remove-subtask.js b/mcp-server/src/core/direct-functions/remove-subtask.js new file mode 100644 index 00000000..2e9e47b9 --- /dev/null +++ b/mcp-server/src/core/direct-functions/remove-subtask.js @@ -0,0 +1,85 @@ +/** + * Direct function wrapper for removeSubtask + */ + +import { removeSubtask } from '../../../../scripts/modules/task-manager.js'; +import { findTasksJsonPath } from '../utils/path-utils.js'; + +/** + * Remove a subtask from its parent task + * @param {Object} args - Function arguments + * @param {string} args.id - Subtask ID in format "parentId.subtaskId" (required) + * @param {boolean} [args.convert] - Whether to convert the subtask to a standalone task + * @param {string} [args.file] - Path to the tasks file + * @param {boolean} [args.skipGenerate] - Skip regenerating task files + * @param {string} [args.projectRoot] - Project root directory + * @param {Object} log - Logger object + * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} + */ +export async function removeSubtaskDirect(args, log) { + try { + log.info(`Removing subtask with args: ${JSON.stringify(args)}`); + + if (!args.id) { + return { + success: false, + error: { + code: 'INPUT_VALIDATION_ERROR', + message: 'Subtask ID is required and must be in format "parentId.subtaskId"' + } + }; + } + + // Validate subtask ID format + if (!args.id.includes('.')) { + return { + success: false, + error: { + code: 'INPUT_VALIDATION_ERROR', + message: `Invalid subtask ID format: ${args.id}. Expected format: "parentId.subtaskId"` + } + }; + } + + // Find the tasks.json path + const tasksPath = findTasksJsonPath(args.file, args.projectRoot); + + // Convert convertToTask to a boolean + const convertToTask = args.convert === true; + + // Determine if we should generate files + const generateFiles = !args.skipGenerate; + + log.info(`Removing subtask ${args.id} (convertToTask: ${convertToTask}, generateFiles: ${generateFiles})`); + + const result = await removeSubtask(tasksPath, args.id, convertToTask, generateFiles); + + if (convertToTask && result) { + // Return info about the converted task + return { + success: true, + data: { + message: `Subtask ${args.id} successfully converted to task #${result.id}`, + task: result + } + }; + } else { + // Return simple success message for deletion + return { + success: true, + data: { + message: `Subtask ${args.id} successfully removed` + } + }; + } + } catch (error) { + log.error(`Error in removeSubtaskDirect: ${error.message}`); + return { + success: false, + error: { + code: 'CORE_FUNCTION_ERROR', + message: error.message + } + }; + } +} \ No newline at end of file diff --git a/mcp-server/src/core/task-master-core.js b/mcp-server/src/core/task-master-core.js index a8eb6d06..b0320656 100644 --- a/mcp-server/src/core/task-master-core.js +++ b/mcp-server/src/core/task-master-core.js @@ -18,6 +18,7 @@ import { nextTaskDirect } from './direct-functions/next-task.js'; import { expandTaskDirect } from './direct-functions/expand-task.js'; import { addTaskDirect } from './direct-functions/add-task.js'; import { addSubtaskDirect } from './direct-functions/add-subtask.js'; +import { removeSubtaskDirect } from './direct-functions/remove-subtask.js'; // Re-export utility functions export { findTasksJsonPath } from './utils/path-utils.js'; @@ -36,7 +37,8 @@ export const directFunctions = new Map([ ['nextTaskDirect', nextTaskDirect], ['expandTaskDirect', expandTaskDirect], ['addTaskDirect', addTaskDirect], - ['addSubtaskDirect', addSubtaskDirect] + ['addSubtaskDirect', addSubtaskDirect], + ['removeSubtaskDirect', removeSubtaskDirect] ]); // Re-export all direct function implementations @@ -53,5 +55,6 @@ export { nextTaskDirect, expandTaskDirect, addTaskDirect, - addSubtaskDirect + addSubtaskDirect, + removeSubtaskDirect }; \ No newline at end of file diff --git a/mcp-server/src/tools/index.js b/mcp-server/src/tools/index.js index d461c191..e4ebd615 100644 --- a/mcp-server/src/tools/index.js +++ b/mcp-server/src/tools/index.js @@ -16,6 +16,7 @@ import { registerNextTaskTool } from "./next-task.js"; import { registerExpandTaskTool } from "./expand-task.js"; import { registerAddTaskTool } from "./add-task.js"; import { registerAddSubtaskTool } from "./add-subtask.js"; +import { registerRemoveSubtaskTool } from "./remove-subtask.js"; /** * Register all Task Master tools with the MCP server @@ -34,6 +35,7 @@ export function registerTaskMasterTools(server) { registerExpandTaskTool(server); registerAddTaskTool(server); registerAddSubtaskTool(server); + registerRemoveSubtaskTool(server); logger.info("Registered all Task Master tools with MCP server"); } diff --git a/mcp-server/src/tools/remove-subtask.js b/mcp-server/src/tools/remove-subtask.js new file mode 100644 index 00000000..786de1fe --- /dev/null +++ b/mcp-server/src/tools/remove-subtask.js @@ -0,0 +1,50 @@ +/** + * tools/remove-subtask.js + * Tool for removing subtasks from parent tasks + */ + +import { z } from "zod"; +import { + handleApiResult, + createErrorResponse +} from "./utils.js"; +import { removeSubtaskDirect } from "../core/task-master-core.js"; + +/** + * Register the removeSubtask tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerRemoveSubtaskTool(server) { + server.addTool({ + name: "remove_subtask", + description: "Remove a subtask from its parent task", + parameters: z.object({ + id: z.string().describe("Subtask ID to remove in format 'parentId.subtaskId' (required)"), + convert: z.boolean().optional().describe("Convert the subtask to a standalone task instead of deleting it"), + file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"), + skipGenerate: z.boolean().optional().describe("Skip regenerating task files"), + projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") + }), + execute: async (args, { log }) => { + try { + log.info(`Removing subtask with args: ${JSON.stringify(args)}`); + + // Call the direct function wrapper + const result = await removeSubtaskDirect(args, log); + + // Log result + if (result.success) { + log.info(`Subtask removed successfully: ${result.data.message}`); + } else { + log.error(`Failed to remove subtask: ${result.error.message}`); + } + + // Use handleApiResult to format the response + return handleApiResult(result, log, 'Error removing subtask'); + } catch (error) { + log.error(`Error in removeSubtask tool: ${error.message}`); + return createErrorResponse(error.message); + } + }, + }); +} \ No newline at end of file diff --git a/tasks/task_023.txt b/tasks/task_023.txt index eeb3eb0f..8dcb861c 100644 --- a/tasks/task_023.txt +++ b/tasks/task_023.txt @@ -670,7 +670,7 @@ Following MCP implementation standards: - Unit test for addSubtaskDirect.js - Integration test for MCP tool -## 27. Implement remove-subtask MCP command [pending] +## 27. Implement remove-subtask MCP command [done] ### Dependencies: None ### Description: Create direct function wrapper and MCP tool for removing subtasks from tasks. ### Details: diff --git a/tasks/tasks.json b/tasks/tasks.json index ff95f30e..3d81d42f 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -1591,7 +1591,7 @@ "title": "Implement remove-subtask MCP command", "description": "Create direct function wrapper and MCP tool for removing subtasks from tasks.", "details": "Following MCP implementation standards:\n\n1. Create removeSubtaskDirect.js in mcp-server/src/core/direct-functions/:\n - Import removeSubtask from task-manager.js\n - Handle file paths using findTasksJsonPath utility\n - Process arguments: parentTaskId, subtaskId\n - Validate inputs and handle errors with try/catch\n - Return standardized { success, data/error } object\n\n2. Export from task-master-core.js:\n - Import the function from its file\n - Add to directFunctions map\n\n3. Create remove-subtask.js MCP tool in mcp-server/src/tools/:\n - Import z from zod for parameter schema\n - Import executeMCPToolAction from ./utils.js\n - Import removeSubtaskDirect from task-master-core.js\n - Define parameters matching CLI options using zod schema\n - Implement registerRemoveSubtaskTool(server) with server.addTool\n - Use executeMCPToolAction in execute method\n\n4. Register in tools/index.js with tool name 'remove_subtask'\n\n5. Add to .cursor/mcp.json with appropriate schema\n\n6. Write tests following testing guidelines:\n - Unit test for removeSubtaskDirect.js\n - Integration test for MCP tool", - "status": "pending", + "status": "done", "dependencies": [], "parentTaskId": 23 },