fix: Improve MCP server robustness and debugging

- Refactor  for more reliable project root detection, particularly when running within integrated environments like Cursor IDE. Includes deriving root from script path and avoiding fallback to '/'.
- Enhance error handling in :
    - Add detailed debug information (paths searched, CWD, etc.) to the error message when  is not found in the provided project root.
    - Improve clarity of error messages and potential solutions.
- Add verbose logging in  to trace session object content and the finally resolved project root path, aiding in debugging path-related issues.
- Add default values for  and  to the example  environment configuration.
This commit is contained in:
Eyal Toledano
2025-04-02 22:04:00 -04:00
parent 9869ebe045
commit 52adb5c2f6
28 changed files with 658 additions and 257 deletions

View File

@@ -10,8 +10,8 @@
- **Refactor project root handling for MCP Server:** - **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). - **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. - **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 and package directory fallback. - **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. - **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: - 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). - Add comprehensive PROJECT_MARKERS array for detecting common project files (used in CLI fallback logic).
- Improved error messages with specific troubleshooting guidance. - 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`. - 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. - Keep caching of `lastFoundProjectRoot` for CLI performance.
@@ -59,6 +62,7 @@
- Update tool descriptions to better reflect their actual behavior and capabilities. - Update tool descriptions to better reflect their actual behavior and capabilities.
- Add cross-references between related tools and commands. - Add cross-references between related tools and commands.
- Include troubleshooting guidance in tool descriptions. - 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). - 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. - Update MCP tool naming to follow more intuitive conventions that better align with natural language requests in client chat applications.

View File

@@ -4,7 +4,17 @@
"command": "node", "command": "node",
"args": [ "args": [
"./mcp-server/server.js" "./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"
}
} }
} }
} }

View File

@@ -91,8 +91,23 @@ export function findTasksJsonPath(args, log) {
if (args.projectRoot) { if (args.projectRoot) {
const projectRoot = args.projectRoot; const projectRoot = args.projectRoot;
log.info(`Using explicitly provided project root: ${projectRoot}`); log.info(`Using explicitly provided project root: ${projectRoot}`);
// This will throw if tasks.json isn't found within this root try {
return findTasksJsonInDirectory(projectRoot, args.file, log); // 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 --- // --- 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); return findTasksJsonWithParentSearch(startDir, args.file, log);
} catch (error) { } catch (error) {
// If all attempts fail, augment and throw the original error from CWD search // 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; throw error;
} }
} }

View File

@@ -43,7 +43,7 @@ export function registerAddDependencyTool(server) {
const result = await addDependencyDirect({ const result = await addDependencyDirect({
projectRoot: rootFolder, projectRoot: rootFolder,
...args ...args
}, log); }, log, { reportProgress, mcpLog: log, session});
reportProgress({ progress: 100 }); reportProgress({ progress: 100 });

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { addSubtaskDirect } from "../core/task-master-core.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"), skipGenerate: z.boolean().optional().describe("Skip regenerating task files"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") 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 { try {
log.info(`Adding subtask with args: ${JSON.stringify(args)}`); log.info(`Adding subtask with args: ${JSON.stringify(args)}`);
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await addSubtaskDirect(args, 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) { if (result.success) {
log.info(`Subtask added successfully: ${result.data.message}`); log.info(`Subtask added successfully: ${result.data.message}`);
} else { } else {
log.error(`Failed to add subtask: ${result.error.message}`); log.error(`Failed to add subtask: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error adding subtask'); return handleApiResult(result, log, 'Error adding subtask');
} catch (error) { } catch (error) {
log.error(`Error in addSubtask tool: ${error.message}`); log.error(`Error in addSubtask tool: ${error.message}`);

View File

@@ -43,7 +43,7 @@ export function registerAddTaskTool(server) {
const result = await addTaskDirect({ const result = await addTaskDirect({
projectRoot: rootFolder, // Pass the resolved root projectRoot: rootFolder, // Pass the resolved root
...args ...args
}, log); }, log, { reportProgress, mcpLog: log, session});
return handleApiResult(result, log); return handleApiResult(result, log);
} catch (error) { } catch (error) {

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { analyzeTaskComplexityDirect } from "../core/task-master-core.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"), 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)") 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 { try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`); log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await analyzeTaskComplexityDirect(args, 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) { if (result.success) {
log.info(`Task complexity analysis complete: ${result.data.message}`); log.info(`Task complexity analysis complete: ${result.data.message}`);
log.info(`Report summary: ${JSON.stringify(result.data.reportSummary)}`); 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}`); log.error(`Failed to analyze task complexity: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error analyzing task complexity'); return handleApiResult(result, log, 'Error analyzing task complexity');
} catch (error) { } catch (error) {
log.error(`Error in analyze tool: ${error.message}`); log.error(`Error in analyze tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { clearSubtasksDirect } from "../core/task-master-core.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", message: "Either 'id' or 'all' parameter must be provided",
path: ["id", "all"] path: ["id", "all"]
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`); log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await clearSubtasksDirect(args, 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) { if (result.success) {
log.info(`Subtasks cleared successfully: ${result.data.message}`); log.info(`Subtasks cleared successfully: ${result.data.message}`);
} else { } else {
log.error(`Failed to clear subtasks: ${result.error.message}`); log.error(`Failed to clear subtasks: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error clearing subtasks'); return handleApiResult(result, log, 'Error clearing subtasks');
} catch (error) { } catch (error) {
log.error(`Error in clearSubtasks tool: ${error.message}`); log.error(`Error in clearSubtasks tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { complexityReportDirect } from "../core/task-master-core.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)"), 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)") 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 { try {
log.info(`Getting complexity report with args: ${JSON.stringify(args)}`); log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await complexityReportDirect(args, 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) { if (result.success) {
log.info(`Successfully retrieved complexity report${result.fromCache ? ' (from cache)' : ''}`); log.info(`Successfully retrieved complexity report${result.fromCache ? ' (from cache)' : ''}`);
} else { } else {
log.error(`Failed to retrieve complexity report: ${result.error.message}`); log.error(`Failed to retrieve complexity report: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error retrieving complexity report'); return handleApiResult(result, log, 'Error retrieving complexity report');
} catch (error) { } catch (error) {
log.error(`Error in complexity-report tool: ${error.message}`); log.error(`Error in complexity-report tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { expandAllTasksDirect } from "../core/task-master-core.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)"), 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)") 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 { try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`); log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await expandAllTasksDirect(args, 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) { if (result.success) {
log.info(`All tasks expanded successfully: ${result.data.message}`); log.info(`All tasks expanded successfully: ${result.data.message}`);
} else { } else {
log.error(`Failed to expand tasks: ${result.error.message}`); log.error(`Failed to expand tasks: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error expanding tasks'); return handleApiResult(result, log, 'Error expanding tasks');
} catch (error) { } catch (error) {
log.error(`Error in expandAll tool: ${error.message}`); log.error(`Error in expandAll tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { expandTaskDirect } from "../core/task-master-core.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"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z projectRoot: z
.string() .string()
.optional()
.describe( .describe(
"Root directory of the project (default: current working directory)" "Root directory of the project (default: current working directory)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Expanding task with args: ${JSON.stringify(args)}`); log.info(`Expanding task with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await expandTaskDirect(args, 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) { if (result.success) {
log.info(`Successfully expanded task ID: ${args.id} with ${result.data.subtasksAdded} new subtasks${result.data.hasExistingSubtasks ? ' (appended to existing subtasks)' : ''}`); log.info(`Successfully expanded task ID: ${args.id} with ${result.data.subtasksAdded} new subtasks${result.data.hasExistingSubtasks ? ' (appended to existing subtasks)' : ''}`);
} else { } else {
log.error(`Failed to expand task: ${result.error.message}`); log.error(`Failed to expand task: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error expanding task'); return handleApiResult(result, log, 'Error expanding task');
} catch (error) { } catch (error) {
log.error(`Error in expand-task tool: ${error.message}`); log.error(`Error in expand-task tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { fixDependenciesDirect } from "../core/task-master-core.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"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") 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 { try {
log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`); log.info(`Fixing dependencies with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await fixDependenciesDirect(args, 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) { if (result.success) {
log.info(`Successfully fixed dependencies: ${result.data.message}`); log.info(`Successfully fixed dependencies: ${result.data.message}`);
} else { } else {
log.error(`Failed to fix dependencies: ${result.error.message}`); log.error(`Failed to fix dependencies: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error fixing dependencies'); return handleApiResult(result, log, 'Error fixing dependencies');
} catch (error) { } catch (error) {
log.error(`Error in fixDependencies tool: ${error.message}`); log.error(`Error in fixDependencies tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { generateTaskFilesDirect } from "../core/task-master-core.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)"), output: z.string().optional().describe("Output directory (default: same directory as tasks file)"),
projectRoot: z projectRoot: z
.string() .string()
.optional()
.describe( .describe(
"Root directory of the project (default: current working directory)" "Root directory of the project (default: current working directory)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Generating task files with args: ${JSON.stringify(args)}`); log.info(`Generating task files with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await generateTaskFilesDirect(args, log);
// Log result if (!rootFolder && args.projectRoot) {
log.info(`${result.success ? 'Successfully generated task files' : 'Failed to generate task files'}`); 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'); return handleApiResult(result, log, 'Error generating task files');
} catch (error) { } catch (error) {
log.error(`Error in generate tool: ${error.message}`); log.error(`Error in generate tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { showTaskDirect } from "../core/task-master-core.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)" "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 { try {
log.info(`Getting task details for ID: ${args.id}`); 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 let rootFolder = getProjectRootFromSession(session, log);
const result = await showTaskDirect(args, 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) { if (result.success) {
log.info(`Successfully retrieved task details for ID: ${args.id}${result.fromCache ? ' (from cache)' : ''}`); log.info(`Successfully retrieved task details for ID: ${args.id}${result.fromCache ? ' (from cache)' : ''}`);
} else { } else {
log.error(`Failed to get task: ${result.error.message}`); log.error(`Failed to get task: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error retrieving task details'); return handleApiResult(result, log, 'Error retrieving task details');
} catch (error) { } 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}`); return createErrorResponse(`Failed to get task: ${error.message}`);
} }
}, },

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
createErrorResponse, createErrorResponse,
handleApiResult handleApiResult,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { listTasksDirect } from "../core/task-master-core.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) { export function registerListTasksTool(server) {
server.addTool({ server.addTool({
name: "get-tasks", name: "get_tasks",
description: "Get all tasks from Task Master", description: "Get all tasks from Task Master, optionally filtering by status and including subtasks.",
parameters: z.object({ 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 withSubtasks: z
.boolean() .boolean()
.optional() .optional()
.describe("Include subtasks in the response"), .describe("Include subtasks nested within their parent tasks in the response"),
file: z.string().optional().describe("Path to the tasks file"), file: z.string().optional().describe("Path to the tasks file (relative to project root or absolute)"),
projectRoot: z projectRoot: z
.string() .string()
.optional() .optional()
.describe( .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 { try {
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`); log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call core function - args contains projectRoot which is handled internally let rootFolder = getProjectRootFromSession(session, log);
const result = await listTasksDirect(args, log);
// Log result and use handleApiResult utility if (!rootFolder && args.projectRoot) {
log.info(`Retrieved ${result.success ? (result.data?.tasks?.length || 0) : 0} tasks`); 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'); return handleApiResult(result, log, 'Error getting tasks');
} catch (error) { } catch (error) {
log.error(`Error getting tasks: ${error.message}`); log.error(`Error getting tasks: ${error.message}`);

View File

@@ -30,9 +30,7 @@ import { registerAddDependencyTool } from "./add-dependency.js";
* Register all Task Master tools with the MCP server * Register all Task Master tools with the MCP server
* @param {Object} server - FastMCP server instance * @param {Object} server - FastMCP server instance
*/ */
export function registerTaskMasterTools(server) { export function registerTaskMasterTools(server) {
logger.info("Registering Task Master tools with MCP server");
try { try {
// Register each tool // Register each tool
registerListTasksTool(server); registerListTasksTool(server);
@@ -56,8 +54,6 @@ export function registerTaskMasterTools(server) {
registerFixDependenciesTool(server); registerFixDependenciesTool(server);
registerComplexityReportTool(server); registerComplexityReportTool(server);
registerAddDependencyTool(server); registerAddDependencyTool(server);
logger.info("Successfully registered all Task Master tools");
} catch (error) { } catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`); logger.error(`Error registering Task Master tools: ${error.message}`);
throw error; throw error;

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { nextTaskDirect } from "../core/task-master-core.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"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z projectRoot: z
.string() .string()
.optional()
.describe( .describe(
"Root directory of the project (default: current working directory)" "Root directory of the project (default: current working directory)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Finding next task with args: ${JSON.stringify(args)}`); log.info(`Finding next task with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await nextTaskDirect(args, 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.success) {
if (result.data.nextTask) { if (result.data.nextTask) {
log.info(`Successfully found next task ID: ${result.data.nextTask.id}${result.fromCache ? ' (from cache)' : ''}`); 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}`); log.error(`Failed to find next task: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error finding next task'); return handleApiResult(result, log, 'Error finding next task');
} catch (error) { } catch (error) {
log.error(`Error in next-task tool: ${error.message}`); log.error(`Error in next-task tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { parsePRDDirect } from "../core/task-master-core.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) { export function registerParsePRDTool(server) {
server.addTool({ server.addTool({
name: "parse_prd_document", name: "parse_prd",
description: "Parse PRD document and generate tasks", description: "Parse a Product Requirements Document (PRD) or text file to automatically generate initial tasks.",
parameters: z.object({ parameters: z.object({
input: z.string().describe("Path to the PRD document file"), 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("Number of tasks to generate (default: 10)"), 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 (default: tasks/tasks.json)"), 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 projectRoot: z
.string() .string()
.optional()
.describe( .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 { try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`); log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await parsePRDDirect(args, log);
// Log result if (!rootFolder && args.projectRoot) {
log.info(`${result.success ? `Successfully generated ${result.data?.taskCount || 0} tasks` : 'Failed to parse PRD'}`); 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'); return handleApiResult(result, log, 'Error parsing PRD document');
} catch (error) { } catch (error) {
log.error(`Error in parsePRD tool: ${error.message}`); log.error(`Error in parse_prd tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}, },

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { removeDependencyDirect } from "../core/task-master-core.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)"), 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)") 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 { try {
log.info(`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`); 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 let rootFolder = getProjectRootFromSession(session, log);
const result = await removeDependencyDirect(args, 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) { if (result.success) {
log.info(`Successfully removed dependency: ${result.data.message}`); log.info(`Successfully removed dependency: ${result.data.message}`);
} else { } else {
log.error(`Failed to remove dependency: ${result.error.message}`); log.error(`Failed to remove dependency: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error removing dependency'); return handleApiResult(result, log, 'Error removing dependency');
} catch (error) { } catch (error) {
log.error(`Error in removeDependency tool: ${error.message}`); log.error(`Error in removeDependency tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}, }
}); });
} }

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { removeSubtaskDirect } from "../core/task-master-core.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"), skipGenerate: z.boolean().optional().describe("Skip regenerating task files"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") 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 { try {
log.info(`Removing subtask with args: ${JSON.stringify(args)}`); log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await removeSubtaskDirect(args, 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) { if (result.success) {
log.info(`Subtask removed successfully: ${result.data.message}`); log.info(`Subtask removed successfully: ${result.data.message}`);
} else { } else {
log.error(`Failed to remove subtask: ${result.error.message}`); log.error(`Failed to remove subtask: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error removing subtask'); return handleApiResult(result, log, 'Error removing subtask');
} catch (error) { } catch (error) {
log.error(`Error in removeSubtask tool: ${error.message}`); log.error(`Error in removeSubtask tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { setTaskStatusDirect } from "../core/task-master-core.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) { export function registerSetTaskStatusTool(server) {
server.addTool({ server.addTool({
name: "set_task_status", 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({ parameters: z.object({
id: z id: z
.string() .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 status: z
.string() .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"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z projectRoot: z
.string() .string()
@@ -33,17 +34,31 @@ export function registerSetTaskStatusTool(server) {
"Root directory of the project (default: automatically detected)" "Root directory of the project (default: automatically detected)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`); log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await setTaskStatusDirect(args, log);
// Log result if (!rootFolder && args.projectRoot) {
log.info(`${result.success ? `Successfully updated task ${args.id} status to "${args.status}"` : 'Failed to update task status'}`); 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'); return handleApiResult(result, log, 'Error setting task status');
} catch (error) { } catch (error) {
log.error(`Error in setTaskStatus tool: ${error.message}`); log.error(`Error in setTaskStatus tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { updateSubtaskByIdDirect } from "../core/task-master-core.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) { export function registerUpdateSubtaskTool(server) {
server.addTool({ server.addTool({
name: "update-subtask", name: "update_subtask",
description: "Appends additional information to a specific subtask without replacing existing content", description: "Appends additional information to a specific subtask without replacing existing content",
parameters: z.object({ parameters: z.object({
id: z.string().describe("ID of the subtask to update in format \"parentId.subtaskId\" (e.g., \"5.2\")"), 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)" "Root directory of the project (default: current working directory)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Updating subtask with args: ${JSON.stringify(args)}`); log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await updateSubtaskByIdDirect(args, log);
// Log result if (!rootFolder && args.projectRoot) {
log.info(`${result.success ? `Successfully updated subtask with ID ${args.id}` : 'Failed to update subtask'}`); 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'); return handleApiResult(result, log, 'Error updating subtask');
} catch (error) { } catch (error) {
log.error(`Error in update-subtask tool: ${error.message}`); log.error(`Error in update_subtask tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}, },

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { updateTaskByIdDirect } from "../core/task-master-core.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) { export function registerUpdateTaskTool(server) {
server.addTool({ server.addTool({
name: "update-task", name: "update_task",
description: "Updates a single task by ID with new information", description: "Updates a single task by ID with new information or context provided in the prompt.",
parameters: z.object({ parameters: z.object({
id: z.union([z.number(), z.string()]).describe("ID of the task to update"), 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 update the task"), 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"), research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"),
file: z.string().optional().describe("Path to the tasks file"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z projectRoot: z
@@ -30,20 +31,34 @@ export function registerUpdateTaskTool(server) {
"Root directory of the project (default: current working directory)" "Root directory of the project (default: current working directory)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Updating task with args: ${JSON.stringify(args)}`); log.info(`Updating task with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await updateTaskByIdDirect(args, log);
// Log result if (!rootFolder && args.projectRoot) {
log.info(`${result.success ? `Successfully updated task with ID ${args.id}` : 'Failed to update task'}`); 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'); return handleApiResult(result, log, 'Error updating task');
} catch (error) { } catch (error) {
log.error(`Error in update-task tool: ${error.message}`); log.error(`Error in update_task tool: ${error.message}`);
return createErrorResponse(error.message); return createErrorResponse(error.message);
} }
}, },

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { updateTasksDirect } from "../core/task-master-core.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) { export function registerUpdateTool(server) {
server.addTool({ server.addTool({
name: "update", 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({ parameters: z.object({
from: z.union([z.number(), z.string()]).describe("Task ID from which to start updating"), 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"), prompt: z.string().describe("Explanation of changes or new context to apply"),
research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"), research: z.boolean().optional().describe("Use Perplexity AI for research-backed updates"),
file: z.string().optional().describe("Path to the tasks file"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z projectRoot: z
@@ -30,17 +31,31 @@ export function registerUpdateTool(server) {
"Root directory of the project (default: current working directory)" "Root directory of the project (default: current working directory)"
), ),
}), }),
execute: async (args, { log }) => { execute: async (args, { log, session, reportProgress }) => {
try { try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`); log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await updateTasksDirect(args, log);
// Log result if (!rootFolder && args.projectRoot) {
log.info(`${result.success ? `Successfully updated tasks from ID ${args.from}` : 'Failed to update tasks'}`); 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'); return handleApiResult(result, log, 'Error updating tasks');
} catch (error) { } catch (error) {
log.error(`Error in update tool: ${error.message}`); log.error(`Error in update tool: ${error.message}`);

View File

@@ -5,12 +5,11 @@
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import path from "path"; import path from "path";
import { contextManager } from '../core/context-manager.js'; // Import the singleton
import fs from 'fs'; 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 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 * Get normalized project root path
@@ -18,7 +17,7 @@ import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/u
* @param {Object} log - Logger object * @param {Object} log - Logger object
* @returns {string} - Normalized absolute path to project root * @returns {string} - Normalized absolute path to project root
*/ */
export function getProjectRoot(projectRootRaw, log) { function getProjectRoot(projectRootRaw, log) {
// PRECEDENCE ORDER: // PRECEDENCE ORDER:
// 1. Environment variable override // 1. Environment variable override
// 2. Explicitly provided projectRoot in args // 2. Explicitly provided projectRoot in args
@@ -74,28 +73,69 @@ export function getProjectRoot(projectRootRaw, log) {
* @param {Object} log - Logger object. * @param {Object} log - Logger object.
* @returns {string|null} - The absolute path to the project root, or null if not found. * @returns {string|null} - The absolute path to the project root, or null if not found.
*/ */
export function getProjectRootFromSession(session, log) { function getProjectRootFromSession(session, log) {
if (session && session.roots && session.roots.length > 0) { try {
const firstRoot = session.roots[0]; // If we have a session with roots array
if (firstRoot && firstRoot.uri) { if (session?.roots?.[0]?.uri) {
try { const rootUri = session.roots[0].uri;
const rootUri = firstRoot.uri; const rootPath = rootUri.startsWith('file://')
const rootPath = rootUri.startsWith('file://') ? decodeURIComponent(rootUri.slice(7))
? decodeURIComponent(rootUri.slice(7)) // Remove 'file://' and decode : rootUri;
: rootUri; // Assume it's a path if no scheme return rootPath;
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.');
} }
} 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 * @param {Function} processFunction - Optional function to process successful result data
* @returns {Object} - Standardized MCP response object * @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) { if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`; const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
// Include cache status in error logs // 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) * @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
* @returns {Object} - The result of the command execution * @returns {Object} - The result of the command execution
*/ */
export function executeTaskMasterCommand( function executeTaskMasterCommand(
command, command,
log, log,
args = [], args = [],
@@ -215,7 +255,7 @@ export function executeTaskMasterCommand(
* @returns {Promise<Object>} - An object containing the result, indicating if it was from cache. * @returns {Promise<Object>} - An object containing the result, indicating if it was from cache.
* Format: { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean } * 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 // Check cache first
const cachedResult = contextManager.getCachedData(cacheKey); 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. * @param {string[]} fieldsToRemove - An array of field names to remove.
* @returns {Object|Array} - The processed data with specified fields removed. * @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) { if (!taskOrData) {
return taskOrData; return taskOrData;
} }
@@ -316,7 +356,7 @@ export function processMCPResponseData(taskOrData, fieldsToRemove = ['details',
* @param {string|Object} content - Content to include in response * @param {string|Object} content - Content to include in response
* @returns {Object} - Content response object in FastMCP format * @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 // FastMCP requires text type, so we format objects as JSON strings
return { return {
content: [ content: [
@@ -351,10 +391,11 @@ export function createErrorResponse(errorMessage) {
// Ensure all functions are exported // Ensure all functions are exported
export { export {
getProjectRoot,
getProjectRootFromSession,
handleApiResult, handleApiResult,
executeTaskMasterCommand, executeTaskMasterCommand,
getCachedOrExecute, getCachedOrExecute,
processMCPResponseData, processMCPResponseData,
createContentResponse, createContentResponse,
createErrorResponse
}; };

View File

@@ -6,7 +6,8 @@
import { z } from "zod"; import { z } from "zod";
import { import {
handleApiResult, handleApiResult,
createErrorResponse createErrorResponse,
getProjectRootFromSession
} from "./utils.js"; } from "./utils.js";
import { validateDependenciesDirect } from "../core/task-master-core.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) { export function registerValidateDependenciesTool(server) {
server.addTool({ server.addTool({
name: "validate_dependencies", 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({ parameters: z.object({
file: z.string().optional().describe("Path to the tasks file"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)") 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 { try {
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`); log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper let rootFolder = getProjectRootFromSession(session, log);
const result = await validateDependenciesDirect(args, 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) { if (result.success) {
log.info(`Successfully validated dependencies: ${result.data.message}`); log.info(`Successfully validated dependencies: ${result.data.message}`);
} else { } else {
log.error(`Failed to validate dependencies: ${result.error.message}`); log.error(`Failed to validate dependencies: ${result.error.message}`);
} }
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error validating dependencies'); return handleApiResult(result, log, 'Error validating dependencies');
} catch (error) { } catch (error) {
log.error(`Error in validateDependencies tool: ${error.message}`); log.error(`Error in validateDependencies tool: ${error.message}`);
@@ -44,4 +55,4 @@ export function registerValidateDependenciesTool(server) {
} }
}, },
}); });
} }

View File

@@ -136,9 +136,13 @@ function handleClaudeError(error) {
* @param {string} prdPath - Path to the PRD file * @param {string} prdPath - Path to the PRD file
* @param {number} numTasks - Number of tasks to generate * @param {number} numTasks - Number of tasks to generate
* @param {number} retryCount - Retry count * @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 * @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 { try {
log('info', 'Calling Claude...'); 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.`; Important: Your response must be valid JSON only, with no additional explanation or comments.`;
// Use streaming request to handle large responses and show progress // 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) { } catch (error) {
// Get user-friendly error message // Get user-friendly error message
const userMessage = handleClaudeError(error); 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} numTasks - Number of tasks to generate
* @param {number} maxTokens - Maximum tokens * @param {number} maxTokens - Maximum tokens
* @param {string} systemPrompt - System prompt * @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 * @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...'); const loadingIndicator = startLoadingIndicator('Generating tasks from PRD...');
if (reportProgress) { await reportProgress({ progress: 0 }); }
let responseText = ''; let responseText = '';
let streamingInterval = null; let streamingInterval = null;
try { try {
// Use streaming for handling large responses // Use streaming for handling large responses
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
model: CONFIG.model, model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
max_tokens: maxTokens, max_tokens: session?.env?.MAX_TOKENS || maxTokens,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
system: systemPrompt, system: systemPrompt,
messages: [ messages: [
{ {
@@ -261,6 +270,12 @@ async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens,
if (chunk.type === 'content_block_delta' && chunk.delta.text) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += 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); 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} numSubtasks - Number of subtasks to generate
* @param {number} nextSubtaskId - Next subtask ID * @param {number} nextSubtaskId - Next subtask ID
* @param {string} additionalContext - Additional context * @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 * @returns {Array} Generated subtasks
*/ */
async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '') { async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '', { reportProgress, mcpLog, session } = {}) {
try { try {
log('info', `Generating ${numSubtasks} subtasks for task ${task.id}: ${task.title}`); 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)}`); process.stdout.write(`Generating subtasks for task ${task.id}${'.'.repeat(dotCount)}`);
dotCount = (dotCount + 1) % 4; dotCount = (dotCount + 1) % 4;
}, 500); }, 500);
// TODO: MOVE THIS TO THE STREAM REQUEST FUNCTION (DRY)
// Use streaming API call // Use streaming API call
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
model: CONFIG.model, model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
system: systemPrompt, system: systemPrompt,
messages: [ 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) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += 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); 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} numSubtasks - Number of subtasks to generate
* @param {number} nextSubtaskId - Next subtask ID * @param {number} nextSubtaskId - Next subtask ID
* @param {string} additionalContext - Additional context * @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 * @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 { try {
// First, perform research to get context // First, perform research to get context
log('info', `Researching context for task ${task.id}: ${task.title}`); log('info', `Researching context for task ${task.id}: ${task.title}`);
const perplexityClient = getPerplexityClient(); 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...'); const researchLoadingIndicator = startLoadingIndicator('Researching best practices with Perplexity AI...');
// Formulate research query based on task // 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 // Use streaming API call
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
model: CONFIG.model, model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
system: systemPrompt, system: systemPrompt,
messages: [ 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) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += 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); if (streamingInterval) clearInterval(streamingInterval);

View File

@@ -49,19 +49,19 @@ import {
// Initialize Anthropic client // Initialize Anthropic client
const anthropic = new Anthropic({ 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 // Import perplexity if available
let perplexity; let perplexity;
try { 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 // Using the existing approach from ai-services.js
const OpenAI = (await import('openai')).default; const OpenAI = (await import('openai')).default;
perplexity = new OpenAI({ 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', baseURL: 'https://api.perplexity.ai',
}); });
@@ -77,8 +77,11 @@ try {
* @param {string} prdPath - Path to the PRD file * @param {string} prdPath - Path to the PRD file
* @param {string} tasksPath - Path to the tasks.json file * @param {string} tasksPath - Path to the tasks.json file
* @param {number} numTasks - Number of tasks to generate * @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 { try {
log('info', `Parsing PRD file: ${prdPath}`); log('info', `Parsing PRD file: ${prdPath}`);
@@ -86,22 +89,20 @@ async function parsePRD(prdPath, tasksPath, numTasks) {
const prdContent = fs.readFileSync(prdPath, 'utf8'); const prdContent = fs.readFileSync(prdPath, 'utf8');
// Call Claude to generate tasks // 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 // Create the directory if it doesn't exist
const tasksDir = path.dirname(tasksPath); const tasksDir = path.dirname(tasksPath);
if (!fs.existsSync(tasksDir)) { if (!fs.existsSync(tasksDir)) {
fs.mkdirSync(tasksDir, { recursive: true }); fs.mkdirSync(tasksDir, { recursive: true });
} }
// Write the tasks to the file // Write the tasks to the file
writeJSON(tasksPath, tasksData); writeJSON(tasksPath, tasksData);
log('success', `Successfully generated ${tasksData.tasks.length} tasks from PRD`); log('success', `Successfully generated ${tasksData.tasks.length} tasks from PRD`);
log('info', `Tasks saved to: ${tasksPath}`); log('info', `Tasks saved to: ${tasksPath}`);
// Generate individual task files // Generate individual task files
await generateTaskFiles(tasksPath, tasksDir); await generateTaskFiles(tasksPath, tasksDir, { reportProgress, mcpLog, session } = {});
console.log(boxen( console.log(boxen(
chalk.green(`Successfully generated ${tasksData.tasks.length} tasks from PRD`), 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 {number} fromId - Task ID to start updating from
* @param {string} prompt - Prompt with new context * @param {string} prompt - Prompt with new context
* @param {boolean} useResearch - Whether to use Perplexity AI for research * @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 { try {
log('info', `Updating tasks from ID ${fromId} with prompt: "${prompt}"`); log('info', `Updating tasks from ID ${fromId} with prompt: "${prompt}"`);
// Validate research flag // 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.'); 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.')); console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.'));
useResearch = false; 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'); log('info', 'Using Perplexity AI for research-backed task updates');
// Call Perplexity AI using format consistent with ai-services.js // 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({ const result = await perplexity.chat.completions.create({
model: perplexityModel, model: perplexityModel,
messages: [ 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.` Return only the updated tasks as a valid JSON array.`
} }
], ],
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature), temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature),
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens), max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens),
}); });
const responseText = result.choices[0].message.content; 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 // Use streaming API call
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
model: CONFIG.model, model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
system: systemPrompt, system: systemPrompt,
messages: [ messages: [
{ {
@@ -304,6 +308,13 @@ Return only the updated tasks as a valid JSON array.`
if (chunk.type === 'content_block_delta' && chunk.delta.text) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += 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); 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 {number} taskId - Task ID to update
* @param {string} prompt - Prompt with new context * @param {string} prompt - Prompt with new context
* @param {boolean} useResearch - Whether to use Perplexity AI for research * @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 * @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 { try {
log('info', `Updating single task ${taskId} with prompt: "${prompt}"`); log('info', `Updating single task ${taskId} with prompt: "${prompt}"`);
@@ -383,7 +397,7 @@ async function updateTaskById(tasksPath, taskId, prompt, useResearch = false) {
} }
// Validate research flag // 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.'); 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.')); console.log(chalk.yellow('Perplexity AI is not available (API key may be missing). Falling back to Claude AI.'));
useResearch = false; 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'); log('info', 'Using Perplexity AI for research-backed task update');
// Verify Perplexity API key exists // 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.'); throw new Error('PERPLEXITY_API_KEY environment variable is missing but --research flag was used.');
} }
try { try {
// Call Perplexity AI // 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({ const result = await perplexity.chat.completions.create({
model: perplexityModel, model: perplexityModel,
messages: [ 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.` Return only the updated task as a valid JSON object.`
} }
], ],
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature), temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature),
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens), max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens),
}); });
const responseText = result.choices[0].message.content; const responseText = result.choices[0].message.content;
@@ -542,7 +556,7 @@ Return only the updated task as a valid JSON object.`
try { try {
// Verify Anthropic API key exists // 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.'); 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 // Use streaming API call
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
model: CONFIG.model, model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
system: systemPrompt, system: systemPrompt,
messages: [ messages: [
{ {
@@ -583,6 +597,12 @@ Return only the updated task as a valid JSON object.`
if (chunk.type === 'content_block_delta' && chunk.delta.text) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += 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); if (streamingInterval) clearInterval(streamingInterval);
@@ -2034,9 +2054,12 @@ function clearSubtasks(tasksPath, taskIds) {
* @param {string} prompt - Description of the task to add * @param {string} prompt - Description of the task to add
* @param {Array} dependencies - Task dependencies * @param {Array} dependencies - Task dependencies
* @param {string} priority - Task priority * @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 * @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(); displayBanner();
// Read the existing tasks // Read the existing tasks
@@ -2112,9 +2135,9 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
try { try {
// Call Claude with streaming enabled // Call Claude with streaming enabled
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
model: CONFIG.model, model: session?.env?.ANTHROPIC_MODEL || CONFIG.model,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
messages: [{ role: "user", content: userPrompt }], messages: [{ role: "user", content: userPrompt }],
system: systemPrompt, system: systemPrompt,
stream: true stream: true
@@ -2133,6 +2156,13 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
if (chunk.type === 'content_block_delta' && chunk.delta.text) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
fullResponse += 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); if (streamingInterval) clearInterval(streamingInterval);
@@ -2213,8 +2243,11 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
/** /**
* Analyzes task complexity and generates expansion recommendations * Analyzes task complexity and generates expansion recommendations
* @param {Object} options Command options * @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 tasksPath = options.file || 'tasks/tasks.json';
const outputPath = options.output || 'scripts/task-complexity-report.json'; const outputPath = options.output || 'scripts/task-complexity-report.json';
const modelOverride = options.model; 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.`; DO NOT include any text before or after the JSON array. No explanations, no markdown formatting.`;
const result = await perplexity.chat.completions.create({ 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: [ messages: [
{ {
role: "system", role: "system",
@@ -2285,8 +2318,8 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
content: researchPrompt content: researchPrompt
} }
], ],
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
}); });
// Extract the response text // 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() { async function useClaudeForComplexityAnalysis() {
// Call the LLM API with streaming // Call the LLM API with streaming
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
model: modelOverride || CONFIG.model, model: modelOverride || CONFIG.model || session?.env?.ANTHROPIC_MODEL,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
messages: [{ role: "user", content: prompt }], messages: [{ role: "user", content: prompt }],
system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.", system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.",
stream: true 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) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
fullResponse += 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); 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.`; DO NOT include any text before or after the JSON array. No explanations, no markdown formatting.`;
const result = await perplexity.chat.completions.create({ 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: [ messages: [
{ {
role: "system", role: "system",
@@ -2541,8 +2580,8 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
content: missingTasksResearchPrompt content: missingTasksResearchPrompt
} }
], ],
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
}); });
// Extract the response // Extract the response
@@ -2550,9 +2589,9 @@ DO NOT include any text before or after the JSON array. No explanations, no mark
} else { } else {
// Use Claude // Use Claude
const stream = await anthropic.messages.create({ const stream = await anthropic.messages.create({
max_tokens: CONFIG.maxTokens, max_tokens: session?.env?.MAX_TOKENS || CONFIG.maxTokens,
model: modelOverride || CONFIG.model, model: modelOverride || CONFIG.model || session?.env?.ANTHROPIC_MODEL,
temperature: CONFIG.temperature, temperature: session?.env?.TEMPERATURE || CONFIG.temperature,
messages: [{ role: "user", content: missingTasksPrompt }], messages: [{ role: "user", content: missingTasksPrompt }],
system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.", system: "You are an expert software architect and project manager analyzing task complexity. Respond only with valid JSON.",
stream: true 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) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
missingAnalysisResponse += 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} subtaskId - ID of the subtask to update in format "parentId.subtaskId"
* @param {string} prompt - Prompt for generating additional information * @param {string} prompt - Prompt for generating additional information
* @param {boolean} useResearch - Whether to use Perplexity AI for research-backed updates * @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 * @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; let loadingIndicator = null;
try { try {
log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`); 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') { if (modelType === 'perplexity') {
// Construct Perplexity payload // 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({ const response = await client.chat.completions.create({
model: perplexityModel, model: perplexityModel,
messages: [ messages: [
{ role: 'system', content: systemPrompt }, { role: 'system', content: systemPrompt },
{ role: 'user', content: userMessageContent } { role: 'user', content: userMessageContent }
], ],
temperature: parseFloat(process.env.TEMPERATURE || CONFIG.temperature), temperature: parseFloat(process.env.TEMPERATURE || session?.env?.TEMPERATURE || CONFIG.temperature),
max_tokens: parseInt(process.env.MAX_TOKENS || CONFIG.maxTokens), max_tokens: parseInt(process.env.MAX_TOKENS || session?.env?.MAX_TOKENS || CONFIG.maxTokens),
}); });
additionalInformation = response.choices[0].message.content.trim(); additionalInformation = response.choices[0].message.content.trim();
} else { // Claude } 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) { if (chunk.type === 'content_block_delta' && chunk.delta.text) {
responseText += 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 { } finally {
if (streamingInterval) clearInterval(streamingInterval); if (streamingInterval) clearInterval(streamingInterval);