diff --git a/.changeset/two-bats-smoke.md b/.changeset/two-bats-smoke.md index da0ae1b6..79ce7ab2 100644 --- a/.changeset/two-bats-smoke.md +++ b/.changeset/two-bats-smoke.md @@ -10,8 +10,8 @@ - **Refactor project root handling for MCP Server:** - **Prioritize Session Roots**: MCP tools now extract the project root path directly from `session.roots[0].uri` provided by the client (e.g., Cursor). - - **New Utility `getProjectRootFromSession`**: Added to `mcp-server/src/tools/utils.js` to encapsulate session root extraction and decoding. - - **Simplify `findTasksJsonPath`**: The core path finding utility in `mcp-server/src/core/utils/path-utils.js` now prioritizes the `projectRoot` passed in `args` (originating from the session). Removed checks for `TASK_MASTER_PROJECT_ROOT` env var and package directory fallback. + - **New Utility `getProjectRootFromSession`**: Added to `mcp-server/src/tools/utils.js` to encapsulate session root extraction and decoding. **Further refined for more reliable detection, especially in integrated environments, including deriving root from script path and avoiding fallback to '/'.** + - **Simplify `findTasksJsonPath`**: The core path finding utility in `mcp-server/src/core/utils/path-utils.js` now prioritizes the `projectRoot` passed in `args` (originating from the session). Removed checks for `TASK_MASTER_PROJECT_ROOT` env var (we do not use this anymore) and package directory fallback. **Enhanced error handling to include detailed debug information (paths searched, CWD, server dir, etc.) and clearer potential solutions when `tasks.json` is not found.** - **Retain CLI Fallbacks**: Kept `lastFoundProjectRoot` cache check and CWD search in `findTasksJsonPath` for compatibility with direct CLI usage. - Updated all MCP tools to use the new project root handling: @@ -22,7 +22,10 @@ - Add comprehensive PROJECT_MARKERS array for detecting common project files (used in CLI fallback logic). - Improved error messages with specific troubleshooting guidance. -- Enhanced logging to indicate the source of project root selection. +- **Enhanced logging:** + - Indicate the source of project root selection more clearly. + - **Add verbose logging in `get-task.js` to trace session object content and resolved project root path, aiding debugging.** + - DRY refactoring by centralizing path utilities in `core/utils/path-utils.js` and session handling in `tools/utils.js`. - Keep caching of `lastFoundProjectRoot` for CLI performance. @@ -59,6 +62,7 @@ - Update tool descriptions to better reflect their actual behavior and capabilities. - Add cross-references between related tools and commands. - Include troubleshooting guidance in tool descriptions. + - **Add default values for `DEFAULT_SUBTASKS` and `DEFAULT_PRIORITY` to the example `.cursor/mcp.json` configuration.** - 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). - Update MCP tool naming to follow more intuitive conventions that better align with natural language requests in client chat applications. diff --git a/.cursor/mcp.json b/.cursor/mcp.json index 6b838029..6dd8186d 100644 --- a/.cursor/mcp.json +++ b/.cursor/mcp.json @@ -4,7 +4,17 @@ "command": "node", "args": [ "./mcp-server/server.js" - ] + ], + "env": { + "ANTHROPIC_API_KEY": "%ANTHROPIC_API_KEY%", + "PERPLEXITY_API_KEY": "%PERPLEXITY_API_KEY%", + "MODEL": "%MODEL%", + "PERPLEXITY_MODEL": "%PERPLEXITY_MODEL%", + "MAX_TOKENS": "%MAX_TOKENS%", + "TEMPERATURE": "%TEMPERATURE%", + "DEFAULT_SUBTASKS": 5, + "DEFAULT_PRIORITY": "medium" + } } } } \ No newline at end of file diff --git a/mcp-server/src/core/utils/path-utils.js b/mcp-server/src/core/utils/path-utils.js index 4114a3aa..9cfc39c2 100644 --- a/mcp-server/src/core/utils/path-utils.js +++ b/mcp-server/src/core/utils/path-utils.js @@ -91,8 +91,23 @@ export function findTasksJsonPath(args, log) { if (args.projectRoot) { const projectRoot = args.projectRoot; log.info(`Using explicitly provided project root: ${projectRoot}`); - // This will throw if tasks.json isn't found within this root - return findTasksJsonInDirectory(projectRoot, args.file, log); + try { + // This will throw if tasks.json isn't found within this root + return findTasksJsonInDirectory(projectRoot, args.file, log); + } catch (error) { + // Include debug info in error + const debugInfo = { + projectRoot, + currentDir: process.cwd(), + serverDir: path.dirname(process.argv[1]), + possibleProjectRoot: path.resolve(path.dirname(process.argv[1]), '../..'), + lastFoundProjectRoot, + searchedPaths: error.message + }; + + error.message = `Tasks file not found in any of the expected locations relative to project root "${projectRoot}" (from session).\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`; + throw error; + } } // --- Fallback logic primarily for CLI or when projectRoot isn't passed --- @@ -120,7 +135,7 @@ export function findTasksJsonPath(args, log) { return findTasksJsonWithParentSearch(startDir, args.file, log); } catch (error) { // If all attempts fail, augment and throw the original error from CWD search - error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)`; + error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)\n\nCurrent working directory: ${startDir}\nLast known project root: ${lastFoundProjectRoot}\nProject root from args: ${args.projectRoot}`; throw error; } } diff --git a/mcp-server/src/tools/add-dependency.js b/mcp-server/src/tools/add-dependency.js index 05b9bdba..75f62d6b 100644 --- a/mcp-server/src/tools/add-dependency.js +++ b/mcp-server/src/tools/add-dependency.js @@ -43,7 +43,7 @@ export function registerAddDependencyTool(server) { const result = await addDependencyDirect({ projectRoot: rootFolder, ...args - }, log); + }, log, { reportProgress, mcpLog: log, session}); reportProgress({ progress: 100 }); diff --git a/mcp-server/src/tools/add-subtask.js b/mcp-server/src/tools/add-subtask.js index b3abc761..e4855076 100644 --- a/mcp-server/src/tools/add-subtask.js +++ b/mcp-server/src/tools/add-subtask.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { addSubtaskDirect } from "../core/task-master-core.js"; @@ -30,21 +31,28 @@ export function registerAddSubtaskTool(server) { 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 }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Adding subtask with args: ${JSON.stringify(args)}`); - // Call the direct function wrapper - const result = await addSubtaskDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await addSubtaskDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); - // Log result if (result.success) { log.info(`Subtask added successfully: ${result.data.message}`); } else { log.error(`Failed to add subtask: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error adding subtask'); } catch (error) { log.error(`Error in addSubtask tool: ${error.message}`); diff --git a/mcp-server/src/tools/add-task.js b/mcp-server/src/tools/add-task.js index 43b55c06..7639e42f 100644 --- a/mcp-server/src/tools/add-task.js +++ b/mcp-server/src/tools/add-task.js @@ -43,7 +43,7 @@ export function registerAddTaskTool(server) { const result = await addTaskDirect({ projectRoot: rootFolder, // Pass the resolved root ...args - }, log); + }, log, { reportProgress, mcpLog: log, session}); return handleApiResult(result, log); } catch (error) { diff --git a/mcp-server/src/tools/analyze.js b/mcp-server/src/tools/analyze.js index 2fc35581..eaee89eb 100644 --- a/mcp-server/src/tools/analyze.js +++ b/mcp-server/src/tools/analyze.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { analyzeTaskComplexityDirect } from "../core/task-master-core.js"; @@ -26,14 +27,25 @@ export function registerAnalyzeTool(server) { research: z.boolean().optional().describe("Use Perplexity AI for research-backed complexity analysis"), projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await analyzeTaskComplexityDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await analyzeTaskComplexityDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Task complexity analysis complete: ${result.data.message}`); log.info(`Report summary: ${JSON.stringify(result.data.reportSummary)}`); @@ -41,7 +53,6 @@ export function registerAnalyzeTool(server) { log.error(`Failed to analyze task complexity: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error analyzing task complexity'); } catch (error) { log.error(`Error in analyze tool: ${error.message}`); diff --git a/mcp-server/src/tools/clear-subtasks.js b/mcp-server/src/tools/clear-subtasks.js index 60f52c2b..cf1a32ea 100644 --- a/mcp-server/src/tools/clear-subtasks.js +++ b/mcp-server/src/tools/clear-subtasks.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { clearSubtasksDirect } from "../core/task-master-core.js"; @@ -27,21 +28,31 @@ export function registerClearSubtasksTool(server) { message: "Either 'id' or 'all' parameter must be provided", path: ["id", "all"] }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await clearSubtasksDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await clearSubtasksDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Subtasks cleared successfully: ${result.data.message}`); } else { log.error(`Failed to clear subtasks: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error clearing subtasks'); } catch (error) { log.error(`Error in clearSubtasks tool: ${error.message}`); diff --git a/mcp-server/src/tools/complexity-report.js b/mcp-server/src/tools/complexity-report.js index 415ad713..0c07349e 100644 --- a/mcp-server/src/tools/complexity-report.js +++ b/mcp-server/src/tools/complexity-report.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { complexityReportDirect } from "../core/task-master-core.js"; @@ -22,21 +23,31 @@ export function registerComplexityReportTool(server) { file: z.string().optional().describe("Path to the report file (default: scripts/task-complexity-report.json)"), projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Getting complexity report with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await complexityReportDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await complexityReportDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Successfully retrieved complexity report${result.fromCache ? ' (from cache)' : ''}`); } else { log.error(`Failed to retrieve complexity report: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error retrieving complexity report'); } catch (error) { log.error(`Error in complexity-report tool: ${error.message}`); diff --git a/mcp-server/src/tools/expand-all.js b/mcp-server/src/tools/expand-all.js index ddd6fbff..8465f8c7 100644 --- a/mcp-server/src/tools/expand-all.js +++ b/mcp-server/src/tools/expand-all.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { expandAllTasksDirect } from "../core/task-master-core.js"; @@ -26,21 +27,31 @@ export function registerExpandAllTool(server) { file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"), projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await expandAllTasksDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await expandAllTasksDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`All tasks expanded successfully: ${result.data.message}`); } else { log.error(`Failed to expand tasks: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error expanding tasks'); } catch (error) { log.error(`Error in expandAll tool: ${error.message}`); diff --git a/mcp-server/src/tools/expand-task.js b/mcp-server/src/tools/expand-task.js index ecef0eee..97ba11e7 100644 --- a/mcp-server/src/tools/expand-task.js +++ b/mcp-server/src/tools/expand-task.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { expandTaskDirect } from "../core/task-master-core.js"; @@ -27,25 +28,36 @@ export function registerExpandTaskTool(server) { 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 }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Expanding task with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await expandTaskDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await expandTaskDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Successfully expanded task ID: ${args.id} with ${result.data.subtasksAdded} new subtasks${result.data.hasExistingSubtasks ? ' (appended to existing subtasks)' : ''}`); } else { log.error(`Failed to expand task: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error expanding task'); } catch (error) { log.error(`Error in expand-task tool: ${error.message}`); diff --git a/mcp-server/src/tools/fix-dependencies.js b/mcp-server/src/tools/fix-dependencies.js index 70340c67..0d999940 100644 --- a/mcp-server/src/tools/fix-dependencies.js +++ b/mcp-server/src/tools/fix-dependencies.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { fixDependenciesDirect } from "../core/task-master-core.js"; @@ -22,21 +23,31 @@ export function registerFixDependenciesTool(server) { 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 }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await fixDependenciesDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await fixDependenciesDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Successfully fixed dependencies: ${result.data.message}`); } else { log.error(`Failed to fix dependencies: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error fixing dependencies'); } catch (error) { log.error(`Error in fixDependencies tool: ${error.message}`); diff --git a/mcp-server/src/tools/generate.js b/mcp-server/src/tools/generate.js index e42f4ef4..510966e9 100644 --- a/mcp-server/src/tools/generate.js +++ b/mcp-server/src/tools/generate.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { generateTaskFilesDirect } from "../core/task-master-core.js"; @@ -23,21 +24,36 @@ export function registerGenerateTool(server) { output: z.string().optional().describe("Output directory (default: same directory as tasks file)"), projectRoot: z .string() + .optional() .describe( "Root directory of the project (default: current working directory)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Generating task files with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await generateTaskFilesDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result - log.info(`${result.success ? 'Successfully generated task files' : 'Failed to generate task files'}`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await generateTaskFilesDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); + + if (result.success) { + log.info(`Successfully generated task files: ${result.data.message}`); + } else { + log.error(`Failed to generate task files: ${result.error.message}`); + } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error generating task files'); } catch (error) { log.error(`Error in generate tool: ${error.message}`); diff --git a/mcp-server/src/tools/get-task.js b/mcp-server/src/tools/get-task.js index b619f002..b007be4b 100644 --- a/mcp-server/src/tools/get-task.js +++ b/mcp-server/src/tools/get-task.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { showTaskDirect } from "../core/task-master-core.js"; @@ -28,24 +29,43 @@ export function registerShowTaskTool(server) { "Root directory of the project (default: current working directory)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { + // Log the session right at the start of execute + log.info(`Session object received in execute: ${JSON.stringify(session)}`); // Use JSON.stringify for better visibility + try { log.info(`Getting task details for ID: ${args.id}`); + + log.info(`Session object received in execute: ${JSON.stringify(session)}`); // Use JSON.stringify for better visibility - // Call the direct function wrapper - const result = await showTaskDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } else if (!rootFolder) { + // Ensure we always have *some* root, even if session failed and args didn't provide one + rootFolder = process.cwd(); + log.warn(`Session and args failed to provide root, using CWD: ${rootFolder}`); + } + + log.info(`Attempting to use project root: ${rootFolder}`); // Log the final resolved root + + log.info(`Root folder: ${rootFolder}`); // Log the final resolved root + const result = await showTaskDirect({ + projectRoot: rootFolder, + ...args + }, log); - // Log result if (result.success) { log.info(`Successfully retrieved task details for ID: ${args.id}${result.fromCache ? ' (from cache)' : ''}`); } else { log.error(`Failed to get task: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error retrieving task details'); } catch (error) { - log.error(`Error in get-task tool: ${error.message}`); + log.error(`Error in get-task tool: ${error.message}\n${error.stack}`); // Add stack trace return createErrorResponse(`Failed to get task: ${error.message}`); } }, diff --git a/mcp-server/src/tools/get-tasks.js b/mcp-server/src/tools/get-tasks.js index 52fd5dbe..4e494b44 100644 --- a/mcp-server/src/tools/get-tasks.js +++ b/mcp-server/src/tools/get-tasks.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { createErrorResponse, - handleApiResult + handleApiResult, + getProjectRootFromSession } from "./utils.js"; import { listTasksDirect } from "../core/task-master-core.js"; @@ -16,31 +17,42 @@ import { listTasksDirect } from "../core/task-master-core.js"; */ export function registerListTasksTool(server) { server.addTool({ - name: "get-tasks", - description: "Get all tasks from Task Master", + name: "get_tasks", + description: "Get all tasks from Task Master, optionally filtering by status and including subtasks.", parameters: z.object({ - status: z.string().optional().describe("Filter tasks by status"), + status: z.string().optional().describe("Filter tasks by status (e.g., 'pending', 'done')"), withSubtasks: z .boolean() .optional() - .describe("Include subtasks in the response"), - file: z.string().optional().describe("Path to the tasks file"), + .describe("Include subtasks nested within their parent tasks in the response"), + file: z.string().optional().describe("Path to the tasks file (relative to project root or absolute)"), projectRoot: z .string() .optional() .describe( - "Root directory of the project (default: automatically detected)" + "Root directory of the project (default: automatically detected from session or CWD)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Getting tasks with filters: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call core function - args contains projectRoot which is handled internally - const result = await listTasksDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result and use handleApiResult utility - log.info(`Retrieved ${result.success ? (result.data?.tasks?.length || 0) : 0} tasks`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await listTasksDirect({ + projectRoot: rootFolder, + ...args + }, log); + + await reportProgress({ progress: 100 }); + + log.info(`Retrieved ${result.success ? (result.data?.tasks?.length || 0) : 0} tasks${result.fromCache ? ' (from cache)' : ''}`); return handleApiResult(result, log, 'Error getting tasks'); } catch (error) { log.error(`Error getting tasks: ${error.message}`); diff --git a/mcp-server/src/tools/index.js b/mcp-server/src/tools/index.js index 6c86de79..f04358a5 100644 --- a/mcp-server/src/tools/index.js +++ b/mcp-server/src/tools/index.js @@ -30,9 +30,7 @@ import { registerAddDependencyTool } from "./add-dependency.js"; * Register all Task Master tools with the MCP server * @param {Object} server - FastMCP server instance */ -export function registerTaskMasterTools(server) { - logger.info("Registering Task Master tools with MCP server"); - +export function registerTaskMasterTools(server) { try { // Register each tool registerListTasksTool(server); @@ -56,8 +54,6 @@ export function registerTaskMasterTools(server) { registerFixDependenciesTool(server); registerComplexityReportTool(server); registerAddDependencyTool(server); - - logger.info("Successfully registered all Task Master tools"); } catch (error) { logger.error(`Error registering Task Master tools: ${error.message}`); throw error; diff --git a/mcp-server/src/tools/next-task.js b/mcp-server/src/tools/next-task.js index c6b4b81d..10f1cd1b 100644 --- a/mcp-server/src/tools/next-task.js +++ b/mcp-server/src/tools/next-task.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { nextTaskDirect } from "../core/task-master-core.js"; @@ -22,18 +23,30 @@ export function registerNextTaskTool(server) { 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 }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Finding next task with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await nextTaskDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await nextTaskDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { if (result.data.nextTask) { log.info(`Successfully found next task ID: ${result.data.nextTask.id}${result.fromCache ? ' (from cache)' : ''}`); @@ -44,7 +57,6 @@ export function registerNextTaskTool(server) { log.error(`Failed to find next task: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error finding next task'); } catch (error) { log.error(`Error in next-task tool: ${error.message}`); diff --git a/mcp-server/src/tools/parse-prd.js b/mcp-server/src/tools/parse-prd.js index 2846072c..76f2b501 100644 --- a/mcp-server/src/tools/parse-prd.js +++ b/mcp-server/src/tools/parse-prd.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { parsePRDDirect } from "../core/task-master-core.js"; @@ -16,32 +17,47 @@ import { parsePRDDirect } from "../core/task-master-core.js"; */ export function registerParsePRDTool(server) { server.addTool({ - name: "parse_prd_document", - description: "Parse PRD document and generate tasks", + name: "parse_prd", + description: "Parse a Product Requirements Document (PRD) or text file to automatically generate initial tasks.", parameters: z.object({ - 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)"), + input: z.string().default("tasks/tasks.json").describe("Path to the PRD document file (relative to project root or absolute)"), + numTasks: z.union([z.number(), z.string()]).optional().describe("Approximate number of top-level tasks to generate (default: 10)"), + output: z.string().optional().describe("Output path for tasks.json file (relative to project root or absolute, default: tasks/tasks.json)"), + force: z.boolean().optional().describe("Allow overwriting an existing tasks.json file."), projectRoot: z .string() + .optional() .describe( - "Root directory of the project (default: current working directory)" + "Root directory of the project (default: automatically detected from session or CWD)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`); - // Call the direct function wrapper - const result = await parsePRDDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result - log.info(`${result.success ? `Successfully generated ${result.data?.taskCount || 0} tasks` : 'Failed to parse PRD'}`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await parsePRDDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); + + if (result.success) { + log.info(`Successfully generated ${result.data?.taskCount || 0} tasks from PRD at ${result.data?.outputPath}`); + } else { + log.error(`Failed to parse PRD: ${result.error?.message || 'Unknown error'}`); + } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error parsing PRD document'); } catch (error) { - log.error(`Error in parsePRD tool: ${error.message}`); + log.error(`Error in parse_prd tool: ${error.message}`); return createErrorResponse(error.message); } }, diff --git a/mcp-server/src/tools/remove-dependency.js b/mcp-server/src/tools/remove-dependency.js index 2cecf3d6..af182f5d 100644 --- a/mcp-server/src/tools/remove-dependency.js +++ b/mcp-server/src/tools/remove-dependency.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { removeDependencyDirect } from "../core/task-master-core.js"; @@ -24,26 +25,36 @@ export function registerRemoveDependencyTool(server) { file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"), projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await removeDependencyDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await removeDependencyDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Successfully removed dependency: ${result.data.message}`); } else { log.error(`Failed to remove dependency: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error removing dependency'); } catch (error) { log.error(`Error in removeDependency tool: ${error.message}`); return createErrorResponse(error.message); } - }, + } }); -} \ No newline at end of file +} \ No newline at end of file diff --git a/mcp-server/src/tools/remove-subtask.js b/mcp-server/src/tools/remove-subtask.js index 786de1fe..2ec67a1d 100644 --- a/mcp-server/src/tools/remove-subtask.js +++ b/mcp-server/src/tools/remove-subtask.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { removeSubtaskDirect } from "../core/task-master-core.js"; @@ -25,21 +26,31 @@ export function registerRemoveSubtaskTool(server) { 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 }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Removing subtask with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await removeSubtaskDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await removeSubtaskDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // 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}`); diff --git a/mcp-server/src/tools/set-task-status.js b/mcp-server/src/tools/set-task-status.js index 536cef49..01982ce9 100644 --- a/mcp-server/src/tools/set-task-status.js +++ b/mcp-server/src/tools/set-task-status.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { setTaskStatusDirect } from "../core/task-master-core.js"; @@ -17,14 +18,14 @@ import { setTaskStatusDirect } from "../core/task-master-core.js"; export function registerSetTaskStatusTool(server) { server.addTool({ name: "set_task_status", - description: "Set the status of a task", + description: "Set the status of one or more tasks or subtasks.", parameters: z.object({ id: z .string() - .describe("Task ID (can be comma-separated for multiple tasks)"), + .describe("Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated for multiple updates."), status: z .string() - .describe("New status (todo, in-progress, review, done)"), + .describe("New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'."), file: z.string().optional().describe("Path to the tasks file"), projectRoot: z .string() @@ -33,17 +34,31 @@ export function registerSetTaskStatusTool(server) { "Root directory of the project (default: automatically detected)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Setting status of task(s) ${args.id} to: ${args.status}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await setTaskStatusDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result - log.info(`${result.success ? `Successfully updated task ${args.id} status to "${args.status}"` : 'Failed to update task status'}`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await setTaskStatusDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); + + if (result.success) { + log.info(`Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}`); + } else { + log.error(`Failed to update task status: ${result.error?.message || 'Unknown error'}`); + } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error setting task status'); } catch (error) { log.error(`Error in setTaskStatus tool: ${error.message}`); diff --git a/mcp-server/src/tools/update-subtask.js b/mcp-server/src/tools/update-subtask.js index 8c50ced8..e5ac37f3 100644 --- a/mcp-server/src/tools/update-subtask.js +++ b/mcp-server/src/tools/update-subtask.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { updateSubtaskByIdDirect } from "../core/task-master-core.js"; @@ -16,7 +17,7 @@ import { updateSubtaskByIdDirect } from "../core/task-master-core.js"; */ export function registerUpdateSubtaskTool(server) { server.addTool({ - name: "update-subtask", + name: "update_subtask", description: "Appends additional information to a specific subtask without replacing existing content", parameters: z.object({ id: z.string().describe("ID of the subtask to update in format \"parentId.subtaskId\" (e.g., \"5.2\")"), @@ -30,20 +31,34 @@ export function registerUpdateSubtaskTool(server) { "Root directory of the project (default: current working directory)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Updating subtask with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await updateSubtaskByIdDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result - log.info(`${result.success ? `Successfully updated subtask with ID ${args.id}` : 'Failed to update subtask'}`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await updateSubtaskByIdDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); + + if (result.success) { + log.info(`Successfully updated subtask with ID ${args.id}`); + } else { + log.error(`Failed to update subtask: ${result.error?.message || 'Unknown error'}`); + } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error updating subtask'); } catch (error) { - log.error(`Error in update-subtask tool: ${error.message}`); + log.error(`Error in update_subtask tool: ${error.message}`); return createErrorResponse(error.message); } }, diff --git a/mcp-server/src/tools/update-task.js b/mcp-server/src/tools/update-task.js index 8cdd6974..5ff88561 100644 --- a/mcp-server/src/tools/update-task.js +++ b/mcp-server/src/tools/update-task.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { updateTaskByIdDirect } from "../core/task-master-core.js"; @@ -16,11 +17,11 @@ import { updateTaskByIdDirect } from "../core/task-master-core.js"; */ export function registerUpdateTaskTool(server) { server.addTool({ - name: "update-task", - description: "Updates a single task by ID with new information", + name: "update_task", + description: "Updates a single task by ID with new information or context provided in the prompt.", parameters: z.object({ - id: z.union([z.number(), z.string()]).describe("ID of the task to update"), - prompt: z.string().describe("New information or context to update the task"), + id: z.union([z.number(), z.string()]).describe("ID of the task or subtask (e.g., '15', '15.2') to update"), + prompt: z.string().describe("New information or context to incorporate into the task"), research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"), file: z.string().optional().describe("Path to the tasks file"), projectRoot: z @@ -30,20 +31,34 @@ export function registerUpdateTaskTool(server) { "Root directory of the project (default: current working directory)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Updating task with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await updateTaskByIdDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result - log.info(`${result.success ? `Successfully updated task with ID ${args.id}` : 'Failed to update task'}`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await updateTaskByIdDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); + + if (result.success) { + log.info(`Successfully updated task with ID ${args.id}`); + } else { + log.error(`Failed to update task: ${result.error?.message || 'Unknown error'}`); + } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error updating task'); } catch (error) { - log.error(`Error in update-task tool: ${error.message}`); + log.error(`Error in update_task tool: ${error.message}`); return createErrorResponse(error.message); } }, diff --git a/mcp-server/src/tools/update.js b/mcp-server/src/tools/update.js index aee07f72..bb06394b 100644 --- a/mcp-server/src/tools/update.js +++ b/mcp-server/src/tools/update.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { updateTasksDirect } from "../core/task-master-core.js"; @@ -17,10 +18,10 @@ import { updateTasksDirect } from "../core/task-master-core.js"; export function registerUpdateTool(server) { server.addTool({ name: "update", - description: "Update tasks with ID >= specified ID based on the provided prompt", + description: "Update multiple upcoming tasks (with ID >= 'from' ID) based on new context or changes provided in the 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"), + from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating (inclusive)"), + prompt: z.string().describe("Explanation of changes or new context to apply"), research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"), file: z.string().optional().describe("Path to the tasks file"), projectRoot: z @@ -30,17 +31,31 @@ export function registerUpdateTool(server) { "Root directory of the project (default: current working directory)" ), }), - execute: async (args, { log }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Updating tasks with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await updateTasksDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); - // Log result - log.info(`${result.success ? `Successfully updated tasks from ID ${args.from}` : 'Failed to update tasks'}`); + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await updateTasksDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); + + if (result.success) { + log.info(`Successfully updated tasks from ID ${args.from}: ${result.data.message}`); + } else { + log.error(`Failed to update tasks: ${result.error?.message || 'Unknown error'}`); + } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error updating tasks'); } catch (error) { log.error(`Error in update tool: ${error.message}`); diff --git a/mcp-server/src/tools/utils.js b/mcp-server/src/tools/utils.js index fbfd4940..168b507e 100644 --- a/mcp-server/src/tools/utils.js +++ b/mcp-server/src/tools/utils.js @@ -5,12 +5,11 @@ import { spawnSync } from "child_process"; import path from "path"; -import { contextManager } from '../core/context-manager.js'; // Import the singleton import fs from 'fs'; -import { decodeURIComponent } from 'querystring'; // Added for URI decoding +import { contextManager } from '../core/context-manager.js'; // Import the singleton // Import path utilities to ensure consistent path resolution -import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/utils/path-utils.js'; +import { lastFoundProjectRoot, PROJECT_MARKERS } from '../core/utils/path-utils.js'; /** * Get normalized project root path @@ -18,7 +17,7 @@ import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/u * @param {Object} log - Logger object * @returns {string} - Normalized absolute path to project root */ -export function getProjectRoot(projectRootRaw, log) { +function getProjectRoot(projectRootRaw, log) { // PRECEDENCE ORDER: // 1. Environment variable override // 2. Explicitly provided projectRoot in args @@ -74,28 +73,69 @@ export function getProjectRoot(projectRootRaw, log) { * @param {Object} log - Logger object. * @returns {string|null} - The absolute path to the project root, or null if not found. */ -export function getProjectRootFromSession(session, log) { - if (session && session.roots && session.roots.length > 0) { - const firstRoot = session.roots[0]; - if (firstRoot && firstRoot.uri) { - try { - const rootUri = firstRoot.uri; - const rootPath = rootUri.startsWith('file://') - ? decodeURIComponent(rootUri.slice(7)) // Remove 'file://' and decode - : rootUri; // Assume it's a path if no scheme - log.info(`Extracted project root from session: ${rootPath}`); - return rootPath; - } catch (e) { - log.error(`Error decoding project root URI from session: ${firstRoot.uri}`, e); - return null; - } - } else { - log.info('Session exists, but first root or its URI is missing.'); +function getProjectRootFromSession(session, log) { + try { + // If we have a session with roots array + if (session?.roots?.[0]?.uri) { + const rootUri = session.roots[0].uri; + const rootPath = rootUri.startsWith('file://') + ? decodeURIComponent(rootUri.slice(7)) + : rootUri; + return rootPath; } - } else { - log.info('No session or session roots found to extract project root.'); + + // If we have a session with roots.roots array (different structure) + if (session?.roots?.roots?.[0]?.uri) { + const rootUri = session.roots.roots[0].uri; + const rootPath = rootUri.startsWith('file://') + ? decodeURIComponent(rootUri.slice(7)) + : rootUri; + return rootPath; + } + + // Get the server's location and try to find project root -- this is a fallback necessary in Cursor IDE + const serverPath = process.argv[1]; // This should be the path to server.js, which is in mcp-server/ + if (serverPath && serverPath.includes('mcp-server')) { + // Find the mcp-server directory first + const mcpServerIndex = serverPath.indexOf('mcp-server'); + if (mcpServerIndex !== -1) { + // Get the path up to mcp-server, which should be the project root + const projectRoot = serverPath.substring(0, mcpServerIndex - 1); // -1 to remove trailing slash + + // Verify this looks like our project root by checking for key files/directories + if (fs.existsSync(path.join(projectRoot, '.cursor')) || + fs.existsSync(path.join(projectRoot, 'mcp-server')) || + fs.existsSync(path.join(projectRoot, 'package.json'))) { + return projectRoot; + } + } + } + + // If we get here, we'll try process.cwd() but only if it's not "/" + const cwd = process.cwd(); + if (cwd !== '/') { + return cwd; + } + + // Last resort: try to derive from the server path we found earlier + if (serverPath) { + const mcpServerIndex = serverPath.indexOf('mcp-server'); + return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : cwd; + } + + throw new Error('Could not determine project root'); + } catch (e) { + // If we have a server path, use it as a basis for project root + const serverPath = process.argv[1]; + if (serverPath && serverPath.includes('mcp-server')) { + const mcpServerIndex = serverPath.indexOf('mcp-server'); + return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : process.cwd(); + } + + // Only use cwd if it's not "/" + const cwd = process.cwd(); + return cwd !== '/' ? cwd : '/'; } - return null; } /** @@ -106,7 +146,7 @@ export function getProjectRootFromSession(session, log) { * @param {Function} processFunction - Optional function to process successful result data * @returns {Object} - Standardized MCP response object */ -export function handleApiResult(result, log, errorPrefix = 'API error', processFunction = processMCPResponseData) { +function handleApiResult(result, log, errorPrefix = 'API error', processFunction = processMCPResponseData) { if (!result.success) { const errorMsg = result.error?.message || `Unknown ${errorPrefix}`; // Include cache status in error logs @@ -138,7 +178,7 @@ export function handleApiResult(result, log, errorPrefix = 'API error', processF * @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally) * @returns {Object} - The result of the command execution */ -export function executeTaskMasterCommand( +function executeTaskMasterCommand( command, log, args = [], @@ -215,7 +255,7 @@ export function executeTaskMasterCommand( * @returns {Promise} - An object containing the result, indicating if it was from cache. * Format: { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean } */ -export async function getCachedOrExecute({ cacheKey, actionFn, log }) { +async function getCachedOrExecute({ cacheKey, actionFn, log }) { // Check cache first const cachedResult = contextManager.getCachedData(cacheKey); @@ -259,7 +299,7 @@ export async function getCachedOrExecute({ cacheKey, actionFn, log }) { * @param {string[]} fieldsToRemove - An array of field names to remove. * @returns {Object|Array} - The processed data with specified fields removed. */ -export function processMCPResponseData(taskOrData, fieldsToRemove = ['details', 'testStrategy']) { +function processMCPResponseData(taskOrData, fieldsToRemove = ['details', 'testStrategy']) { if (!taskOrData) { return taskOrData; } @@ -316,7 +356,7 @@ export function processMCPResponseData(taskOrData, fieldsToRemove = ['details', * @param {string|Object} content - Content to include in response * @returns {Object} - Content response object in FastMCP format */ -export function createContentResponse(content) { +function createContentResponse(content) { // FastMCP requires text type, so we format objects as JSON strings return { content: [ @@ -351,10 +391,11 @@ export function createErrorResponse(errorMessage) { // Ensure all functions are exported export { + getProjectRoot, + getProjectRootFromSession, handleApiResult, executeTaskMasterCommand, getCachedOrExecute, processMCPResponseData, createContentResponse, - createErrorResponse }; diff --git a/mcp-server/src/tools/validate-dependencies.js b/mcp-server/src/tools/validate-dependencies.js index 2b4460c0..e24f0feb 100644 --- a/mcp-server/src/tools/validate-dependencies.js +++ b/mcp-server/src/tools/validate-dependencies.js @@ -6,7 +6,8 @@ import { z } from "zod"; import { handleApiResult, - createErrorResponse + createErrorResponse, + getProjectRootFromSession } from "./utils.js"; import { validateDependenciesDirect } from "../core/task-master-core.js"; @@ -17,26 +18,36 @@ import { validateDependenciesDirect } from "../core/task-master-core.js"; export function registerValidateDependenciesTool(server) { server.addTool({ name: "validate_dependencies", - description: "Identify invalid dependencies in tasks without fixing them", + description: "Check tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.", parameters: z.object({ 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 }) => { + execute: async (args, { log, session, reportProgress }) => { try { log.info(`Validating dependencies with args: ${JSON.stringify(args)}`); + await reportProgress({ progress: 0 }); - // Call the direct function wrapper - const result = await validateDependenciesDirect(args, log); + let rootFolder = getProjectRootFromSession(session, log); + + if (!rootFolder && args.projectRoot) { + rootFolder = args.projectRoot; + log.info(`Using project root from args as fallback: ${rootFolder}`); + } + + const result = await validateDependenciesDirect({ + projectRoot: rootFolder, + ...args + }, log, { reportProgress, mcpLog: log, session}); + + await reportProgress({ progress: 100 }); - // Log result if (result.success) { log.info(`Successfully validated dependencies: ${result.data.message}`); } else { log.error(`Failed to validate dependencies: ${result.error.message}`); } - // Use handleApiResult to format the response return handleApiResult(result, log, 'Error validating dependencies'); } catch (error) { log.error(`Error in validateDependencies tool: ${error.message}`); @@ -44,4 +55,4 @@ export function registerValidateDependenciesTool(server) { } }, }); -} \ No newline at end of file +} \ No newline at end of file diff --git a/scripts/modules/ai-services.js b/scripts/modules/ai-services.js index 4850fb97..6e9c4c81 100644 --- a/scripts/modules/ai-services.js +++ b/scripts/modules/ai-services.js @@ -136,9 +136,13 @@ function handleClaudeError(error) { * @param {string} prdPath - Path to the PRD file * @param {number} numTasks - Number of tasks to generate * @param {number} retryCount - Retry count + * @param {Object} options - Options object containing: + * - reportProgress: Function to report progress to MCP server (optional) + * - mcpLog: MCP logger object (optional) + * - session: Session object from MCP server (optional) * @returns {Object} Claude's response */ -async function callClaude(prdContent, prdPath, numTasks, retryCount = 0) { +async function callClaude(prdContent, prdPath, numTasks, retryCount = 0, { reportProgress, mcpLog, session } = {}) { try { log('info', 'Calling Claude...'); @@ -190,7 +194,7 @@ Expected output format: Important: Your response must be valid JSON only, with no additional explanation or comments.`; // Use streaming request to handle large responses and show progress - return await handleStreamingRequest(prdContent, prdPath, numTasks, CONFIG.maxTokens, systemPrompt); + return await handleStreamingRequest(prdContent, prdPath, numTasks, CONFIG.maxTokens, systemPrompt, { reportProgress, mcpLog, session } = {}); } catch (error) { // Get user-friendly error message const userMessage = handleClaudeError(error); @@ -224,19 +228,24 @@ Important: Your response must be valid JSON only, with no additional explanation * @param {number} numTasks - Number of tasks to generate * @param {number} maxTokens - Maximum tokens * @param {string} systemPrompt - System prompt + * @param {Object} options - Options object containing: + * - reportProgress: Function to report progress to MCP server (optional) + * - mcpLog: MCP logger object (optional) + * - session: Session object from MCP server (optional) * @returns {Object} Claude's response */ -async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, systemPrompt) { +async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, systemPrompt, { reportProgress, mcpLog, session } = {}) { const loadingIndicator = startLoadingIndicator('Generating tasks from PRD...'); + if (reportProgress) { await reportProgress({ progress: 0 }); } let responseText = ''; let streamingInterval = null; try { // Use streaming for handling large responses const stream = await anthropic.messages.create({ - model: CONFIG.model, - max_tokens: maxTokens, - temperature: CONFIG.temperature, + model: session?.env?.ANTHROPIC_MODEL || CONFIG.model, + max_tokens: session?.env?.MAX_TOKENS || maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, system: systemPrompt, messages: [ { @@ -261,6 +270,12 @@ async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (responseText.length / maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${responseText.length / maxTokens * 100}%`); + } } if (streamingInterval) clearInterval(streamingInterval); @@ -355,9 +370,13 @@ function processClaudeResponse(textContent, numTasks, retryCount, prdContent, pr * @param {number} numSubtasks - Number of subtasks to generate * @param {number} nextSubtaskId - Next subtask ID * @param {string} additionalContext - Additional context + * @param {Object} options - Options object containing: + * - reportProgress: Function to report progress to MCP server (optional) + * - mcpLog: MCP logger object (optional) + * - session: Session object from MCP server (optional) * @returns {Array} Generated subtasks */ -async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '') { +async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '', { reportProgress, mcpLog, session } = {}) { try { log('info', `Generating ${numSubtasks} subtasks for task ${task.id}: ${task.title}`); @@ -418,12 +437,14 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use process.stdout.write(`Generating subtasks for task ${task.id}${'.'.repeat(dotCount)}`); dotCount = (dotCount + 1) % 4; }, 500); + + // TODO: MOVE THIS TO THE STREAM REQUEST FUNCTION (DRY) // Use streaming API call const stream = await anthropic.messages.create({ - model: CONFIG.model, - max_tokens: CONFIG.maxTokens, - temperature: CONFIG.temperature, + model: session?.env?.ANTHROPIC_MODEL || CONFIG.model, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, system: systemPrompt, messages: [ { @@ -439,6 +460,12 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`); + } } if (streamingInterval) clearInterval(streamingInterval); @@ -464,15 +491,19 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use * @param {number} numSubtasks - Number of subtasks to generate * @param {number} nextSubtaskId - Next subtask ID * @param {string} additionalContext - Additional context + * @param {Object} options - Options object containing: + * - reportProgress: Function to report progress to MCP server (optional) + * - mcpLog: MCP logger object (optional) + * - session: Session object from MCP server (optional) * @returns {Array} Generated subtasks */ -async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1, additionalContext = '') { +async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1, additionalContext = '', { reportProgress, mcpLog, session } = {}) { try { // First, perform research to get context log('info', `Researching context for task ${task.id}: ${task.title}`); const perplexityClient = getPerplexityClient(); - const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || 'sonar-pro'; + const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro'; const researchLoadingIndicator = startLoadingIndicator('Researching best practices with Perplexity AI...'); // Formulate research query based on task @@ -566,9 +597,9 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use // Use streaming API call const stream = await anthropic.messages.create({ - model: CONFIG.model, - max_tokens: CONFIG.maxTokens, - temperature: CONFIG.temperature, + model: session?.env?.ANTHROPIC_MODEL || CONFIG.model, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, system: systemPrompt, messages: [ { @@ -584,6 +615,12 @@ Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`); + } } if (streamingInterval) clearInterval(streamingInterval); diff --git a/scripts/modules/task-manager.js b/scripts/modules/task-manager.js index 9abb1f37..64a5d4f3 100644 --- a/scripts/modules/task-manager.js +++ b/scripts/modules/task-manager.js @@ -49,19 +49,19 @@ import { // Initialize Anthropic client const anthropic = new Anthropic({ - apiKey: process.env.ANTHROPIC_API_KEY, + apiKey: process.env.ANTHROPIC_API_KEY || session?.env?.ANTHROPIC_API_KEY, }); // Import perplexity if available let perplexity; try { - if (process.env.PERPLEXITY_API_KEY) { + if (process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY) { // Using the existing approach from ai-services.js const OpenAI = (await import('openai')).default; perplexity = new OpenAI({ - apiKey: process.env.PERPLEXITY_API_KEY, + apiKey: process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY, baseURL: 'https://api.perplexity.ai', }); @@ -77,8 +77,11 @@ try { * @param {string} prdPath - Path to the PRD file * @param {string} tasksPath - Path to the tasks.json file * @param {number} numTasks - Number of tasks to generate + * @param {function} reportProgress - Function to report progress to MCP server (optional) + * @param {Object} mcpLog - MCP logger object (optional) + * @param {Object} session - Session object from MCP server (optional) */ -async function parsePRD(prdPath, tasksPath, numTasks) { +async function parsePRD(prdPath, tasksPath, numTasks, { reportProgress, mcpLog, session } = {}) { try { log('info', `Parsing PRD file: ${prdPath}`); @@ -86,22 +89,20 @@ async function parsePRD(prdPath, tasksPath, numTasks) { const prdContent = fs.readFileSync(prdPath, 'utf8'); // Call Claude to generate tasks - const tasksData = await callClaude(prdContent, prdPath, numTasks); + const tasksData = await callClaude(prdContent, prdPath, numTasks, { reportProgress, mcpLog, session } = {}); // Create the directory if it doesn't exist const tasksDir = path.dirname(tasksPath); if (!fs.existsSync(tasksDir)) { fs.mkdirSync(tasksDir, { recursive: true }); } - // Write the tasks to the file writeJSON(tasksPath, tasksData); - log('success', `Successfully generated ${tasksData.tasks.length} tasks from PRD`); log('info', `Tasks saved to: ${tasksPath}`); // Generate individual task files - await generateTaskFiles(tasksPath, tasksDir); + await generateTaskFiles(tasksPath, tasksDir, { reportProgress, mcpLog, session } = {}); console.log(boxen( chalk.green(`Successfully generated ${tasksData.tasks.length} tasks from PRD`), @@ -132,13 +133,16 @@ async function parsePRD(prdPath, tasksPath, numTasks) { * @param {number} fromId - Task ID to start updating from * @param {string} prompt - Prompt with new context * @param {boolean} useResearch - Whether to use Perplexity AI for research + * @param {function} reportProgress - Function to report progress to MCP server (optional) + * @param {Object} mcpLog - MCP logger object (optional) + * @param {Object} session - Session object from MCP server (optional) */ -async function updateTasks(tasksPath, fromId, prompt, useResearch = false) { +async function updateTasks(tasksPath, fromId, prompt, useResearch = false, { reportProgress, mcpLog, session } = {}) { try { log('info', `Updating tasks from ID ${fromId} with prompt: "${prompt}"`); // Validate research flag - if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY)) { + if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY)) { log('warn', 'Perplexity AI is not available. Falling back to Claude AI.'); console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.')); useResearch = false; @@ -224,7 +228,7 @@ The changes described in the prompt should be applied to ALL tasks in the list.` log('info', 'Using Perplexity AI for research-backed task updates'); // Call Perplexity AI using format consistent with ai-services.js - const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro'; + const perplexityModel = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro'; const result = await perplexity.chat.completions.create({ model: perplexityModel, messages: [ @@ -245,8 +249,8 @@ IMPORTANT: In the tasks JSON above, any subtasks with "status": "done" or "statu Return only the updated tasks as a valid JSON array.` } ], - temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature), - max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens), + temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature), + max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens), }); const responseText = result.choices[0].message.content; @@ -278,9 +282,9 @@ Return only the updated tasks as a valid JSON array.` // Use streaming API call const stream = await anthropic.messages.create({ - model: CONFIG.model, - max_tokens: CONFIG.maxTokens, - temperature: CONFIG.temperature, + model: session?.env?.ANTHROPIC_MODEL || CONFIG.model, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, system: systemPrompt, messages: [ { @@ -304,6 +308,13 @@ Return only the updated tasks as a valid JSON array.` if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 }); + } + + if (mcpLog) { + mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`); + } } if (streamingInterval) clearInterval(streamingInterval); @@ -366,9 +377,12 @@ Return only the updated tasks as a valid JSON array.` * @param {number} taskId - Task ID to update * @param {string} prompt - Prompt with new context * @param {boolean} useResearch - Whether to use Perplexity AI for research + * @param {function} reportProgress - Function to report progress to MCP server (optional) + * @param {Object} mcpLog - MCP logger object (optional) + * @param {Object} session - Session object from MCP server (optional) * @returns {Object} - Updated task data or null if task wasn't updated */ -async function updateTaskById(tasksPath, taskId, prompt, useResearch = false) { +async function updateTaskById(tasksPath, taskId, prompt, useResearch = false, { reportProgress, mcpLog, session } = {}) { try { log('info', `Updating single task ${taskId} with prompt: "${prompt}"`); @@ -383,7 +397,7 @@ async function updateTaskById(tasksPath, taskId, prompt, useResearch = false) { } // Validate research flag - if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY)) { + if (useResearch && (!perplexity || !process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY)) { log('warn', 'Perplexity AI is not available. Falling back to Claude AI.'); console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.')); useResearch = false; @@ -484,13 +498,13 @@ The changes described in the prompt should be thoughtfully applied to make the t log('info', 'Using Perplexity AI for research-backed task update'); // Verify Perplexity API key exists - if (!process.env.PERPLEXITY_API_KEY) { + if (!process.env.PERPLEXITY_API_KEY || session?.env?.PERPLEXITY_API_KEY) { throw new Error('PERPLEXITY_API_KEY environment variable is missing but --research flag was used.'); } try { // Call Perplexity AI - const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro'; + const perplexityModel = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro'; const result = await perplexity.chat.completions.create({ model: perplexityModel, messages: [ @@ -511,8 +525,8 @@ IMPORTANT: In the task JSON above, any subtasks with "status": "done" or "status Return only the updated task as a valid JSON object.` } ], - temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature), - max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens), + temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature), + max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens), }); const responseText = result.choices[0].message.content; @@ -542,7 +556,7 @@ Return only the updated task as a valid JSON object.` try { // Verify Anthropic API key exists - if (!process.env.ANTHROPIC_API_KEY) { + if (!process.env.ANTHROPIC_API_KEY || session?.env?.ANTHROPIC_API_KEY) { throw new Error('ANTHROPIC_API_KEY environment variable is missing. Required for task updates.'); } @@ -557,9 +571,9 @@ Return only the updated task as a valid JSON object.` // Use streaming API call const stream = await anthropic.messages.create({ - model: CONFIG.model, - max_tokens: CONFIG.maxTokens, - temperature: CONFIG.temperature, + model: session?.env?.ANTHROPIC_MODEL || CONFIG.model, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, system: systemPrompt, messages: [ { @@ -583,6 +597,12 @@ Return only the updated task as a valid JSON object.` if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`); + } } if (streamingInterval) clearInterval(streamingInterval); @@ -2034,9 +2054,12 @@ function clearSubtasks(tasksPath, taskIds) { * @param {string} prompt - Description of the task to add * @param {Array} dependencies - Task dependencies * @param {string} priority - Task priority + * @param {function} reportProgress - Function to report progress to MCP server (optional) + * @param {Object} mcpLog - MCP logger object (optional) + * @param {Object} session - Session object from MCP server (optional) * @returns {number} The new task ID */ -async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium') { +async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium', { reportProgress, mcpLog, session } = {}) { displayBanner(); // Read the existing tasks @@ -2112,9 +2135,9 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium' try { // Call Claude with streaming enabled const stream = await anthropic.messages.create({ - max_tokens: CONFIG.maxTokens, - model: CONFIG.model, - temperature: CONFIG.temperature, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + model: session?.env?.ANTHROPIC_MODEL || CONFIG.model, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, messages: [{ role: "user", content: userPrompt }], system: systemPrompt, stream: true @@ -2133,6 +2156,13 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium' if (chunk.type === 'content_block_delta' && chunk.delta.text) { fullResponse += chunk.delta.text; } + + if (reportProgress) { + await reportProgress({ progress: (fullResponse.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${fullResponse.length / CONFIG.maxTokens * 100}%`); + } } if (streamingInterval) clearInterval(streamingInterval); @@ -2213,8 +2243,11 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium' /** * Analyzes task complexity and generates expansion recommendations * @param {Object} options Command options + * @param {function} reportProgress - Function to report progress to MCP server (optional) + * @param {Object} mcpLog - MCP logger object (optional) + * @param {Object} session - Session object from MCP server (optional) */ -async function analyzeTaskComplexity(options) { +async function analyzeTaskComplexity(options, { reportProgress, mcpLog, session } = {}) { const tasksPath = options.file || 'tasks/tasks.json'; const outputPath = options.output || 'scripts/task-complexity-report.json'; const modelOverride = options.model; @@ -2274,7 +2307,7 @@ Your response must be a clean JSON array only, following exactly this format: DO NOT include any text before or after the JSON array. No explanations, no markdown formatting.`; const result = await perplexity.chat.completions.create({ - model: process.env.PERPLEXITY_MODEL || 'sonar-pro', + model: process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro', messages: [ { role: "system", @@ -2285,8 +2318,8 @@ DO NOT include any text before or after the JSON array. No explanations, no mark content: researchPrompt } ], - temperature: CONFIG.temperature, - max_tokens: CONFIG.maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, }); // Extract the response text @@ -2315,9 +2348,9 @@ DO NOT include any text before or after the JSON array. No explanations, no mark async function useClaudeForComplexityAnalysis() { // Call the LLM API with streaming const stream = await anthropic.messages.create({ - max_tokens: CONFIG.maxTokens, - model: modelOverride || CONFIG.model, - temperature: CONFIG.temperature, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + model: modelOverride || CONFIG.model || session?.env?.ANTHROPIC_MODEL, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, messages: [{ role: "user", content: prompt }], system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.", stream: true @@ -2336,6 +2369,12 @@ DO NOT include any text before or after the JSON array. No explanations, no mark if (chunk.type === 'content_block_delta' && chunk.delta.text) { fullResponse += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (fullResponse.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${fullResponse.length / CONFIG.maxTokens * 100}%`); + } } clearInterval(streamingInterval); @@ -2530,7 +2569,7 @@ Your response must be a clean JSON array only, following exactly this format: DO NOT include any text before or after the JSON array. No explanations, no markdown formatting.`; const result = await perplexity.chat.completions.create({ - model: process.env.PERPLEXITY_MODEL || 'sonar-pro', + model: process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro', messages: [ { role: "system", @@ -2541,8 +2580,8 @@ DO NOT include any text before or after the JSON array. No explanations, no mark content: missingTasksResearchPrompt } ], - temperature: CONFIG.temperature, - max_tokens: CONFIG.maxTokens, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, }); // Extract the response @@ -2550,9 +2589,9 @@ DO NOT include any text before or after the JSON array. No explanations, no mark } else { // Use Claude const stream = await anthropic.messages.create({ - max_tokens: CONFIG.maxTokens, - model: modelOverride || CONFIG.model, - temperature: CONFIG.temperature, + max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens, + model: modelOverride || CONFIG.model || session?.env?.ANTHROPIC_MODEL, + temperature: session?.env?.TEMPERATURE || CONFIG.temperature, messages: [{ role: "user", content: missingTasksPrompt }], system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.", stream: true @@ -2563,6 +2602,12 @@ DO NOT include any text before or after the JSON array. No explanations, no mark if (chunk.type === 'content_block_delta' && chunk.delta.text) { missingAnalysisResponse += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (missingAnalysisResponse.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${missingAnalysisResponse.length / CONFIG.maxTokens * 100}%`); + } } } @@ -3063,9 +3108,12 @@ async function removeSubtask(tasksPath, subtaskId, convertToTask = false, genera * @param {string} subtaskId - ID of the subtask to update in format "parentId.subtaskId" * @param {string} prompt - Prompt for generating additional information * @param {boolean} useResearch - Whether to use Perplexity AI for research-backed updates + * @param {function} reportProgress - Function to report progress to MCP server (optional) + * @param {Object} mcpLog - MCP logger object (optional) + * @param {Object} session - Session object from MCP server (optional) * @returns {Object|null} - The updated subtask or null if update failed */ -async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false) { +async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false, { reportProgress, mcpLog, session } = {} ) { let loadingIndicator = null; try { log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`); @@ -3194,15 +3242,15 @@ Provide concrete examples, code snippets, or implementation details when relevan if (modelType === 'perplexity') { // Construct Perplexity payload - const perplexityModel = process.env.PERPLEXITY_MODEL || 'sonar-pro'; + const perplexityModel = process.env.PERPLEXITY_MODEL || session?.env?.PERPLEXITY_MODEL || 'sonar-pro'; const response = await client.chat.completions.create({ model: perplexityModel, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userMessageContent } ], - temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature), - max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens), + temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature), + max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens), }); additionalInformation = response.choices[0].message.content.trim(); } else { // Claude @@ -3234,6 +3282,12 @@ Provide concrete examples, code snippets, or implementation details when relevan if (chunk.type === 'content_block_delta' && chunk.delta.text) { responseText += chunk.delta.text; } + if (reportProgress) { + await reportProgress({ progress: (responseText.length / CONFIG.maxTokens) * 100 }); + } + if (mcpLog) { + mcpLog.info(`Progress: ${responseText.length / CONFIG.maxTokens * 100}%`); + } } } finally { if (streamingInterval) clearInterval(streamingInterval);