diff --git a/.cursor/rules/mcp.mdc b/.cursor/rules/mcp.mdc index 6f9ef633..b07bd7ac 100644 --- a/.cursor/rules/mcp.mdc +++ b/.cursor/rules/mcp.mdc @@ -97,3 +97,91 @@ Follow these steps to add MCP support for an existing Task Master command (see [ - `handleApiResult` uses `createContentResponse` or `createErrorResponse` internally. - `handleApiResult` also uses `processMCPResponseData` by default to filter potentially large fields (`details`, `testStrategy`) from task data. Provide a custom processor function to `handleApiResult` if different filtering is needed. - The final JSON response sent to the MCP client will include the `fromCache` boolean flag (obtained from the `*Direct` function's result) alongside the actual data (e.g., `{ "fromCache": true, "data": { ... } }` or `{ "fromCache": false, "data": { ... } }`). + +## Parameter Type Handling + +- **Prefer Direct Function Calls**: For optimal performance and error handling, MCP tools should utilize direct function wrappers defined in [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js). These wrappers call the underlying logic from the core modules (e.g., [`task-manager.js`](mdc:scripts/modules/task-manager.js)). +- **Standard Tool Execution Pattern**: + - The `execute` method within each MCP tool (in `mcp-server/src/tools/*.js`) should: + 1. Call the corresponding `*Direct` function wrapper (e.g., `listTasksDirect`) from [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js), passing necessary arguments and the logger. + 2. Receive the result object (typically `{ success, data/error, fromCache }`). + 3. Pass this result object to the `handleApiResult` utility (from [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js)) for standardized response formatting and error handling. + 4. Return the formatted response object provided by `handleApiResult`. +- **CLI Execution as Fallback**: The `executeTaskMasterCommand` utility in [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js) allows executing commands via the CLI (`task-master ...`). This should **only** be used as a fallback if a direct function wrapper is not yet implemented or if a specific command intrinsically requires CLI execution. +- **Centralized Utilities** (See also: [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc)): + - Use `findTasksJsonPath` (in [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js)) *within direct function wrappers* to locate the `tasks.json` file consistently. + - **Leverage MCP Utilities**: The file [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js) contains essential helpers for MCP tool implementation: + - `getProjectRoot`: Normalizes project paths. + - `handleApiResult`: Takes the raw result from a `*Direct` function and formats it into a standard MCP success or error response, automatically handling data processing via `processMCPResponseData`. This is called by the tool's `execute` method. + - `createContentResponse`/`createErrorResponse`: Used by `handleApiResult` to format successful/error MCP responses. + - `processMCPResponseData`: Filters/cleans data (e.g., removing `details`, `testStrategy`) before it's sent in the MCP response. Called by `handleApiResult`. + - `getCachedOrExecute`: **Used inside `*Direct` functions** in `task-master-core.js` to implement caching logic. + - `executeTaskMasterCommand`: Fallback for executing CLI commands. +- **Caching**: To improve performance for frequently called read operations (like `listTasks`, `showTask`, `nextTask`), a caching layer using `lru-cache` is implemented. + - **Caching logic resides *within* the direct function wrappers** in [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js) using the `getCachedOrExecute` utility from [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js). + - Generate unique cache keys based on function arguments that define a distinct call (e.g., file path, filters). + - The `getCachedOrExecute` utility handles checking the cache, executing the core logic function on a cache miss, storing the result, and returning the data along with a `fromCache` flag. + - Cache statistics can be monitored using the `cacheStats` MCP tool (implemented via `getCacheStatsDirect`). + - **Caching should generally be applied to read-only operations** that don't modify the `tasks.json` state. Commands like `set-status`, `add-task`, `update-task`, `parse-prd`, `add-dependency` should *not* be cached as they change the underlying data. + +**MCP Tool Implementation Checklist**: + +1. **Core Logic Verification**: + - [ ] Confirm the core function is properly exported from its module (e.g., `task-manager.js`) + - [ ] Identify all required parameters and their types + +2. **Direct Function Wrapper**: + - [ ] Create the `*Direct` function in `task-master-core.js` + - [ ] Handle all parameter validations and type conversions + - [ ] Implement path resolving for relative paths + - [ ] Add appropriate error handling with standardized error codes + - [ ] Add to `directFunctions` map + +3. **MCP Tool Implementation**: + - [ ] Create new file in `mcp-server/src/tools/` with kebab-case naming + - [ ] Define zod schema for all parameters + - [ ] Implement the `execute` method following the standard pattern + - [ ] Register tool in `mcp-server/src/tools/index.js` + +4. **Testing**: + - [ ] Write unit tests for the direct function wrapper + - [ ] Write integration tests for the MCP tool + +## Standard Error Codes + +- **Standard Error Codes**: Use consistent error codes across direct function wrappers + - `INPUT_VALIDATION_ERROR`: For missing or invalid required parameters + - `FILE_NOT_FOUND_ERROR`: For file system path issues + - `CORE_FUNCTION_ERROR`: For errors thrown by the core function + - `UNEXPECTED_ERROR`: For all other unexpected errors + +- **Error Object Structure**: + ```javascript + { + success: false, + error: { + code: 'ERROR_CODE', + message: 'Human-readable error message' + }, + fromCache: false + } + ``` + +- **MCP Tool Logging Pattern**: + - ✅ DO: Log the start of execution with arguments (sanitized if sensitive) + - ✅ DO: Log successful completion with result summary + - ✅ DO: Log all error conditions with appropriate log levels + - ✅ DO: Include the cache status in result logs + - ❌ DON'T: Log entire large data structures or sensitive information + +- The MCP server integrates with Task Master core functions through three layers: + 1. Tool Definitions (`mcp-server/src/tools/*.js`) - Define parameters and execute methods + 2. Direct Function Wrappers (`task-master-core.js`) - Handle validation, path resolution, and caching + 3. Core Logic Functions (various modules) - Implement actual functionality + +- This layered approach provides: + - Clear separation of concerns + - Consistent parameter validation + - Centralized error handling + - Performance optimization through caching (for read operations) + - Standardized response formatting diff --git a/mcp-server/src/core/task-master-core.js b/mcp-server/src/core/task-master-core.js index 1985fde3..f6642680 100644 --- a/mcp-server/src/core/task-master-core.js +++ b/mcp-server/src/core/task-master-core.js @@ -19,6 +19,7 @@ const __dirname = dirname(__filename); import { listTasks, parsePRD, + updateTasks, // We'll import more functions as we continue implementation } from '../../../scripts/modules/task-manager.js'; @@ -159,60 +160,187 @@ export async function getCacheStatsDirect(args, log) { } /** - * Direct function wrapper for parsePRD with error handling. - * - * @param {Object} args - Command arguments (input file path, output path, numTasks). + * Direct function wrapper for parsing PRD documents and generating tasks. + * + * @param {Object} args - Command arguments containing input, numTasks or tasks, and output options. * @param {Object} log - Logger object. - * @returns {Promise} - Result object { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }. + * @returns {Promise} - Result object with success status and data/error information. */ export async function parsePRDDirect(args, log) { try { - // Normalize paths based on projectRoot - const projectRoot = args.projectRoot || process.cwd(); + log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`); - // Get the input file path (PRD file) - const inputPath = args.input - ? path.resolve(projectRoot, args.input) - : path.resolve(projectRoot, 'sample-prd.txt'); - - log.info(`Using PRD file: ${inputPath}`); - - // Determine tasks output path - let tasksPath; - try { - // Try to find existing tasks.json first - tasksPath = findTasksJsonPath(args, log); - } catch (error) { - // If not found, use default path - tasksPath = path.resolve(projectRoot, 'tasks', 'tasks.json'); - log.info(`No existing tasks.json found, will create at: ${tasksPath}`); + // Check required parameters + if (!args.input) { + const errorMessage = 'No input file specified. Please provide an input PRD document path.'; + log.error(errorMessage); + return { + success: false, + error: { code: 'MISSING_INPUT_FILE', message: errorMessage }, + fromCache: false + }; } - // Get number of tasks to generate - const numTasks = args.numTasks ? parseInt(args.numTasks, 10) : undefined; + // Resolve input path (relative to project root if provided) + const projectRoot = args.projectRoot || process.cwd(); + const inputPath = path.isAbsolute(args.input) ? args.input : path.resolve(projectRoot, args.input); - log.info(`Parsing PRD file ${inputPath} to generate tasks in ${tasksPath}`); + // Determine output path + let outputPath; + if (args.output) { + outputPath = path.isAbsolute(args.output) ? args.output : path.resolve(projectRoot, args.output); + } else { + // Default to tasks/tasks.json in the project root + outputPath = path.resolve(projectRoot, 'tasks', 'tasks.json'); + } - // Call the core parsePRD function - await parsePRD(inputPath, tasksPath, numTasks); + // Verify input file exists + if (!fs.existsSync(inputPath)) { + const errorMessage = `Input file not found: ${inputPath}`; + log.error(errorMessage); + return { + success: false, + error: { code: 'INPUT_FILE_NOT_FOUND', message: errorMessage }, + fromCache: false + }; + } - return { - success: true, - data: { - message: `Successfully parsed PRD and generated tasks in ${tasksPath}`, - inputFile: inputPath, - outputFile: tasksPath - }, - fromCache: false // PRD parsing is never cached - }; + // Parse number of tasks - handle both string and number values + let numTasks = 10; // Default + if (args.numTasks) { + numTasks = typeof args.numTasks === 'string' ? parseInt(args.numTasks, 10) : args.numTasks; + if (isNaN(numTasks)) { + numTasks = 10; // Fallback to default if parsing fails + log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`); + } + } + + log.info(`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`); + + // Execute core parsePRD function (which is not async but we'll await it to maintain consistency) + await parsePRD(inputPath, outputPath, numTasks); + + // Since parsePRD doesn't return a value but writes to a file, we'll read the result + // to return it to the caller + if (fs.existsSync(outputPath)) { + const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8')); + log.info(`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`); + + return { + success: true, + data: { + message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`, + taskCount: tasksData.tasks?.length || 0, + outputPath + }, + fromCache: false // This operation always modifies state and should never be cached + }; + } else { + const errorMessage = `Tasks file was not created at ${outputPath}`; + log.error(errorMessage); + return { + success: false, + error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage }, + fromCache: false + }; + } } catch (error) { log.error(`Error parsing PRD: ${error.message}`); return { success: false, - error: { - code: 'PARSE_PRD_ERROR', - message: error.message - }, + error: { code: 'PARSE_PRD_ERROR', message: error.message || 'Unknown error parsing PRD' }, + fromCache: false + }; + } +} + +/** + * Direct function wrapper for updating tasks based on new context/prompt. + * + * @param {Object} args - Command arguments containing fromId, prompt, useResearch and file path options. + * @param {Object} log - Logger object. + * @returns {Promise} - Result object with success status and data/error information. + */ +export async function updateTasksDirect(args, log) { + try { + log.info(`Updating tasks with args: ${JSON.stringify(args)}`); + + // Check required parameters + if (!args.from) { + const errorMessage = 'No from ID specified. Please provide a task ID to start updating from.'; + log.error(errorMessage); + return { + success: false, + error: { code: 'MISSING_FROM_ID', message: errorMessage }, + fromCache: false + }; + } + + if (!args.prompt) { + const errorMessage = 'No prompt specified. Please provide a prompt with new context for task updates.'; + log.error(errorMessage); + return { + success: false, + error: { code: 'MISSING_PROMPT', message: errorMessage }, + fromCache: false + }; + } + + // Parse fromId - handle both string and number values + let fromId; + if (typeof args.from === 'string') { + fromId = parseInt(args.from, 10); + if (isNaN(fromId)) { + const errorMessage = `Invalid from ID: ${args.from}. Task ID must be a positive integer.`; + log.error(errorMessage); + return { + success: false, + error: { code: 'INVALID_FROM_ID', message: errorMessage }, + fromCache: false + }; + } + } else { + fromId = args.from; + } + + // Get tasks file path + let tasksPath; + try { + tasksPath = findTasksJsonPath(args, log); + } catch (error) { + log.error(`Error finding tasks file: ${error.message}`); + return { + success: false, + error: { code: 'TASKS_FILE_ERROR', message: error.message }, + fromCache: false + }; + } + + // Get research flag + const useResearch = args.research === true; + + log.info(`Updating tasks from ID ${fromId} with prompt "${args.prompt}" and research: ${useResearch}`); + + // Execute core updateTasks function + await updateTasks(tasksPath, fromId, args.prompt, useResearch); + + // Since updateTasks doesn't return a value but modifies the tasks file, + // we'll return a success message + return { + success: true, + data: { + message: `Successfully updated tasks from ID ${fromId} based on the prompt`, + fromId, + tasksPath, + useResearch + }, + fromCache: false // This operation always modifies state and should never be cached + }; + } catch (error) { + log.error(`Error updating tasks: ${error.message}`); + return { + success: false, + error: { code: 'UPDATE_TASKS_ERROR', message: error.message || 'Unknown error updating tasks' }, fromCache: false }; } @@ -225,5 +353,6 @@ export const directFunctions = { list: listTasksDirect, cacheStats: getCacheStatsDirect, parsePRD: parsePRDDirect, + update: updateTasksDirect, // Add more functions as we implement them }; \ No newline at end of file diff --git a/mcp-server/src/tools/index.js b/mcp-server/src/tools/index.js index 7176fb40..cb62c305 100644 --- a/mcp-server/src/tools/index.js +++ b/mcp-server/src/tools/index.js @@ -11,6 +11,7 @@ import { registerExpandTaskTool } from "./expandTask.js"; import { registerNextTaskTool } from "./nextTask.js"; import { registerAddTaskTool } from "./addTask.js"; import { registerParsePRDTool } from "./parsePRD.js"; +import { registerUpdateTool } from "./update.js"; /** * Register all Task Master tools with the MCP server @@ -24,6 +25,7 @@ export function registerTaskMasterTools(server) { registerNextTaskTool(server); registerAddTaskTool(server); registerParsePRDTool(server); + registerUpdateTool(server); } export default { diff --git a/mcp-server/src/tools/parsePRD.js b/mcp-server/src/tools/parsePRD.js index 1118484d..c9718bba 100644 --- a/mcp-server/src/tools/parsePRD.js +++ b/mcp-server/src/tools/parsePRD.js @@ -1,10 +1,13 @@ /** * tools/parsePRD.js - * Tool to parse PRD documents and generate Task Master tasks + * Tool to parse PRD document and generate tasks */ import { z } from "zod"; -import { executeMCPToolAction } from "./utils.js"; +import { + handleApiResult, + createErrorResponse +} from "./utils.js"; import { parsePRDDirect } from "../core/task-master-core.js"; /** @@ -14,28 +17,34 @@ import { parsePRDDirect } from "../core/task-master-core.js"; export function registerParsePRDTool(server) { server.addTool({ name: "parsePRD", - description: "Parse a PRD document and generate Task Master tasks", + description: "Parse PRD document and generate tasks", parameters: z.object({ - input: z - .string() - .optional() - .describe("Path to the PRD text file (default: sample-prd.txt)"), - numTasks: z - .number() - .optional() - .describe("Number of tasks to generate"), + input: z.string().describe("Path to the PRD document file"), + numTasks: z.union([z.number(), z.string()]).optional().describe("Number of tasks to generate (default: 10)"), + output: z.string().optional().describe("Output path for tasks.json file (default: tasks/tasks.json)"), projectRoot: z .string() .optional() - .describe("Root directory of the project (default: current working directory)") + .describe( + "Root directory of the project (default: current working directory)" + ), }), execute: async (args, { log }) => { - return executeMCPToolAction({ - actionFn: parsePRDDirect, - args, - log, - actionName: "Parse PRD and generate tasks" - }); + try { + log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`); + + // Call the direct function wrapper + const result = await parsePRDDirect(args, log); + + // Log result + log.info(`${result.success ? `Successfully generated ${result.data?.taskCount || 0} tasks` : 'Failed to parse PRD'}`); + + // Use handleApiResult to format the response + return handleApiResult(result, log, 'Error parsing PRD document'); + } catch (error) { + log.error(`Error in parsePRD tool: ${error.message}`); + return createErrorResponse(error.message); + } }, }); } \ No newline at end of file diff --git a/mcp-server/src/tools/update.js b/mcp-server/src/tools/update.js new file mode 100644 index 00000000..aee07f72 --- /dev/null +++ b/mcp-server/src/tools/update.js @@ -0,0 +1,51 @@ +/** + * tools/update.js + * Tool to update tasks based on new context/prompt + */ + +import { z } from "zod"; +import { + handleApiResult, + createErrorResponse +} from "./utils.js"; +import { updateTasksDirect } from "../core/task-master-core.js"; + +/** + * Register the update tool with the MCP server + * @param {Object} server - FastMCP server instance + */ +export function registerUpdateTool(server) { + server.addTool({ + name: "update", + description: "Update tasks with ID >= specified ID based on the provided prompt", + parameters: z.object({ + from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating"), + prompt: z.string().describe("Explanation of changes or new context"), + research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"), + file: z.string().optional().describe("Path to the tasks file"), + projectRoot: z + .string() + .optional() + .describe( + "Root directory of the project (default: current working directory)" + ), + }), + execute: async (args, { log }) => { + try { + log.info(`Updating tasks with args: ${JSON.stringify(args)}`); + + // Call the direct function wrapper + const result = await updateTasksDirect(args, log); + + // Log result + log.info(`${result.success ? `Successfully updated tasks from ID ${args.from}` : 'Failed to update tasks'}`); + + // Use handleApiResult to format the response + return handleApiResult(result, log, 'Error updating tasks'); + } catch (error) { + log.error(`Error in update 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 a596956a..6c1749bf 100644 --- a/tasks/task_023.txt +++ b/tasks/task_023.txt @@ -346,13 +346,13 @@ function listTasks(tasksPath, statusFilter, withSubtasks = false, outputFormat = ### Details: 1. Research and implement SSE protocol for the MCP server\n2. Create dedicated SSE endpoints for event streaming\n3. Implement event emitter pattern for internal event management\n4. Add support for different event types (task status, logs, errors)\n5. Implement client connection management with proper keep-alive handling\n6. Add filtering capabilities to allow subscribing to specific event types\n7. Create in-memory event buffer for clients reconnecting\n8. Document SSE endpoint usage and client implementation examples\n9. Add robust error handling for dropped connections\n10. Implement rate limiting and backpressure mechanisms\n11. Add authentication for SSE connections -## 16. Implement parse-prd MCP command [in-progress] +## 16. Implement parse-prd MCP command [done] ### Dependencies: None ### Description: Create direct function wrapper and MCP tool for parsing PRD documents to generate tasks. ### Details: Following MCP implementation standards:\n\n1. Create parsePRDDirect function in task-master-core.js:\n - Import parsePRD from task-manager.js\n - Handle file paths using findTasksJsonPath utility\n - Process arguments: input file, output path, numTasks\n - Validate inputs and handle errors with try/catch\n - Return standardized { success, data/error } object\n - Add to directFunctions map\n\n2. Create parse-prd.js MCP tool in mcp-server/src/tools/:\n - Import z from zod for parameter schema\n - Import executeMCPToolAction from ./utils.js\n - Import parsePRDDirect from task-master-core.js\n - Define parameters matching CLI options using zod schema\n - Implement registerParsePRDTool(server) with server.addTool\n - Use executeMCPToolAction in execute method\n\n3. Register in tools/index.js\n\n4. Add to .cursor/mcp.json with appropriate schema\n\n5. Write tests following testing guidelines:\n - Unit test for parsePRDDirect\n - Integration test for MCP tool -## 17. Implement update MCP command [pending] +## 17. Implement update MCP command [done] ### Dependencies: None ### Description: Create direct function wrapper and MCP tool for updating multiple tasks based on prompt. ### Details: diff --git a/tasks/tasks.json b/tasks/tasks.json index 1c6e0d6f..811c81f4 100644 --- a/tasks/tasks.json +++ b/tasks/tasks.json @@ -1492,7 +1492,7 @@ "title": "Implement parse-prd MCP command", "description": "Create direct function wrapper and MCP tool for parsing PRD documents to generate tasks.", "details": "Following MCP implementation standards:\\n\\n1. Create parsePRDDirect function in task-master-core.js:\\n - Import parsePRD from task-manager.js\\n - Handle file paths using findTasksJsonPath utility\\n - Process arguments: input file, output path, numTasks\\n - Validate inputs and handle errors with try/catch\\n - Return standardized { success, data/error } object\\n - Add to directFunctions map\\n\\n2. Create parse-prd.js MCP tool in mcp-server/src/tools/:\\n - Import z from zod for parameter schema\\n - Import executeMCPToolAction from ./utils.js\\n - Import parsePRDDirect from task-master-core.js\\n - Define parameters matching CLI options using zod schema\\n - Implement registerParsePRDTool(server) with server.addTool\\n - Use executeMCPToolAction in execute method\\n\\n3. Register in tools/index.js\\n\\n4. Add to .cursor/mcp.json with appropriate schema\\n\\n5. Write tests following testing guidelines:\\n - Unit test for parsePRDDirect\\n - Integration test for MCP tool", - "status": "in-progress", + "status": "done", "dependencies": [], "parentTaskId": 23 }, @@ -1501,7 +1501,7 @@ "title": "Implement update MCP command", "description": "Create direct function wrapper and MCP tool for updating multiple tasks based on prompt.", "details": "Following MCP implementation standards:\\n\\n1. Create updateTasksDirect function in task-master-core.js:\\n - Import updateTasks from task-manager.js\\n - Handle file paths using findTasksJsonPath utility\\n - Process arguments: fromId, prompt, useResearch\\n - Validate inputs and handle errors with try/catch\\n - Return standardized { success, data/error } object\\n - Add to directFunctions map\\n\\n2. Create update.js MCP tool in mcp-server/src/tools/:\\n - Import z from zod for parameter schema\\n - Import executeMCPToolAction from ./utils.js\\n - Import updateTasksDirect from task-master-core.js\\n - Define parameters matching CLI options using zod schema\\n - Implement registerUpdateTool(server) with server.addTool\\n - Use executeMCPToolAction in execute method\\n\\n3. Register in tools/index.js\\n\\n4. Add to .cursor/mcp.json with appropriate schema\\n\\n5. Write tests following testing guidelines:\\n - Unit test for updateTasksDirect\\n - Integration test for MCP tool", - "status": "pending", + "status": "done", "dependencies": [], "parentTaskId": 23 },