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 1a74b50658
commit a49a77d19f
28 changed files with 658 additions and 257 deletions

View File

@@ -91,8 +91,23 @@ export function findTasksJsonPath(args, log) {
if (args.projectRoot) {
const projectRoot = args.projectRoot;
log.info(`Using explicitly provided project root: ${projectRoot}`);
// This will throw if tasks.json isn't found within this root
return findTasksJsonInDirectory(projectRoot, args.file, log);
try {
// This will throw if tasks.json isn't found within this root
return findTasksJsonInDirectory(projectRoot, args.file, log);
} catch (error) {
// Include debug info in error
const debugInfo = {
projectRoot,
currentDir: process.cwd(),
serverDir: path.dirname(process.argv[1]),
possibleProjectRoot: path.resolve(path.dirname(process.argv[1]), '../..'),
lastFoundProjectRoot,
searchedPaths: error.message
};
error.message = `Tasks file not found in any of the expected locations relative to project root "${projectRoot}" (from session).\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`;
throw error;
}
}
// --- Fallback logic primarily for CLI or when projectRoot isn't passed ---
@@ -120,7 +135,7 @@ export function findTasksJsonPath(args, log) {
return findTasksJsonWithParentSearch(startDir, args.file, log);
} catch (error) {
// If all attempts fail, augment and throw the original error from CWD search
error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)`;
error.message = `${error.message}\n\nPossible solutions:\n1. Run the command from your project directory containing tasks.json\n2. Use --project-root=/path/to/project to specify the project location (if using CLI)\n3. Ensure the project root is correctly passed from the client (if using MCP)\n\nCurrent working directory: ${startDir}\nLast known project root: ${lastFoundProjectRoot}\nProject root from args: ${args.projectRoot}`;
throw error;
}
}

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { analyzeTaskComplexityDirect } from "../core/task-master-core.js";
@@ -26,14 +27,25 @@ export function registerAnalyzeTool(server) {
research: z.boolean().optional().describe("Use Perplexity AI for research-backed complexity analysis"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper
const result = await analyzeTaskComplexityDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await analyzeTaskComplexityDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
// Log result
if (result.success) {
log.info(`Task complexity analysis complete: ${result.data.message}`);
log.info(`Report summary: ${JSON.stringify(result.data.reportSummary)}`);
@@ -41,7 +53,6 @@ export function registerAnalyzeTool(server) {
log.error(`Failed to analyze task complexity: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error analyzing task complexity');
} catch (error) {
log.error(`Error in analyze tool: ${error.message}`);

View File

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

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { complexityReportDirect } from "../core/task-master-core.js";
@@ -22,21 +23,31 @@ export function registerComplexityReportTool(server) {
file: z.string().optional().describe("Path to the report file (default: scripts/task-complexity-report.json)"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper
const result = await complexityReportDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await complexityReportDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
// Log result
if (result.success) {
log.info(`Successfully retrieved complexity report${result.fromCache ? ' (from cache)' : ''}`);
} else {
log.error(`Failed to retrieve complexity report: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error retrieving complexity report');
} catch (error) {
log.error(`Error in complexity-report tool: ${error.message}`);

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { expandAllTasksDirect } from "../core/task-master-core.js";
@@ -26,21 +27,31 @@ export function registerExpandAllTool(server) {
file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper
const result = await expandAllTasksDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await expandAllTasksDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
// Log result
if (result.success) {
log.info(`All tasks expanded successfully: ${result.data.message}`);
} else {
log.error(`Failed to expand tasks: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error expanding tasks');
} catch (error) {
log.error(`Error in expandAll tool: ${error.message}`);

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { showTaskDirect } from "../core/task-master-core.js";
@@ -28,24 +29,43 @@ export function registerShowTaskTool(server) {
"Root directory of the project (default: current working directory)"
),
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
// Log the session right at the start of execute
log.info(`Session object received in execute: ${JSON.stringify(session)}`); // Use JSON.stringify for better visibility
try {
log.info(`Getting task details for ID: ${args.id}`);
log.info(`Session object received in execute: ${JSON.stringify(session)}`); // Use JSON.stringify for better visibility
// Call the direct function wrapper
const result = await showTaskDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
} else if (!rootFolder) {
// Ensure we always have *some* root, even if session failed and args didn't provide one
rootFolder = process.cwd();
log.warn(`Session and args failed to provide root, using CWD: ${rootFolder}`);
}
log.info(`Attempting to use project root: ${rootFolder}`); // Log the final resolved root
log.info(`Root folder: ${rootFolder}`); // Log the final resolved root
const result = await showTaskDirect({
projectRoot: rootFolder,
...args
}, log);
// Log result
if (result.success) {
log.info(`Successfully retrieved task details for ID: ${args.id}${result.fromCache ? ' (from cache)' : ''}`);
} else {
log.error(`Failed to get task: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error retrieving task details');
} catch (error) {
log.error(`Error in get-task tool: ${error.message}`);
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`); // Add stack trace
return createErrorResponse(`Failed to get task: ${error.message}`);
}
},

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { parsePRDDirect } from "../core/task-master-core.js";
@@ -16,32 +17,47 @@ import { parsePRDDirect } from "../core/task-master-core.js";
*/
export function registerParsePRDTool(server) {
server.addTool({
name: "parse_prd_document",
description: "Parse PRD document and generate tasks",
name: "parse_prd",
description: "Parse a Product Requirements Document (PRD) or text file to automatically generate initial tasks.",
parameters: z.object({
input: z.string().describe("Path to the PRD document file"),
numTasks: z.union([z.number(), z.string()]).optional().describe("Number of tasks to generate (default: 10)"),
output: z.string().optional().describe("Output path for tasks.json file (default: tasks/tasks.json)"),
input: z.string().default("tasks/tasks.json").describe("Path to the PRD document file (relative to project root or absolute)"),
numTasks: z.union([z.number(), z.string()]).optional().describe("Approximate number of top-level tasks to generate (default: 10)"),
output: z.string().optional().describe("Output path for tasks.json file (relative to project root or absolute, default: tasks/tasks.json)"),
force: z.boolean().optional().describe("Allow overwriting an existing tasks.json file."),
projectRoot: z
.string()
.optional()
.describe(
"Root directory of the project (default: current working directory)"
"Root directory of the project (default: automatically detected from session or CWD)"
),
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
// Call the direct function wrapper
const result = await parsePRDDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
// Log result
log.info(`${result.success ? `Successfully generated ${result.data?.taskCount || 0} tasks` : 'Failed to parse PRD'}`);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await parsePRDDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully generated ${result.data?.taskCount || 0} tasks from PRD at ${result.data?.outputPath}`);
} else {
log.error(`Failed to parse PRD: ${result.error?.message || 'Unknown error'}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error parsing PRD document');
} catch (error) {
log.error(`Error in parsePRD tool: ${error.message}`);
log.error(`Error in parse_prd tool: ${error.message}`);
return createErrorResponse(error.message);
}
},

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { removeDependencyDirect } from "../core/task-master-core.js";
@@ -24,26 +25,36 @@ export function registerRemoveDependencyTool(server) {
file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper
const result = await removeDependencyDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await removeDependencyDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
// Log result
if (result.success) {
log.info(`Successfully removed dependency: ${result.data.message}`);
} else {
log.error(`Failed to remove dependency: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error removing dependency');
} catch (error) {
log.error(`Error in removeDependency tool: ${error.message}`);
return createErrorResponse(error.message);
}
},
}
});
}
}

View File

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

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { setTaskStatusDirect } from "../core/task-master-core.js";
@@ -17,14 +18,14 @@ import { setTaskStatusDirect } from "../core/task-master-core.js";
export function registerSetTaskStatusTool(server) {
server.addTool({
name: "set_task_status",
description: "Set the status of a task",
description: "Set the status of one or more tasks or subtasks.",
parameters: z.object({
id: z
.string()
.describe("Task ID (can be comma-separated for multiple tasks)"),
.describe("Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated for multiple updates."),
status: z
.string()
.describe("New status (todo, in-progress, review, done)"),
.describe("New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'."),
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z
.string()
@@ -33,17 +34,31 @@ export function registerSetTaskStatusTool(server) {
"Root directory of the project (default: automatically detected)"
),
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper
const result = await setTaskStatusDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
// Log result
log.info(`${result.success ? `Successfully updated task ${args.id} status to "${args.status}"` : 'Failed to update task status'}`);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await setTaskStatusDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}`);
} else {
log.error(`Failed to update task status: ${result.error?.message || 'Unknown error'}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error setting task status');
} catch (error) {
log.error(`Error in setTaskStatus tool: ${error.message}`);

View File

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

View File

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

View File

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

View File

@@ -5,12 +5,11 @@
import { spawnSync } from "child_process";
import path from "path";
import { contextManager } from '../core/context-manager.js'; // Import the singleton
import fs from 'fs';
import { decodeURIComponent } from 'querystring'; // Added for URI decoding
import { contextManager } from '../core/context-manager.js'; // Import the singleton
// Import path utilities to ensure consistent path resolution
import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/utils/path-utils.js';
import { lastFoundProjectRoot, PROJECT_MARKERS } from '../core/utils/path-utils.js';
/**
* Get normalized project root path
@@ -18,7 +17,7 @@ import { lastFoundProjectRoot, getPackagePath, PROJECT_MARKERS } from '../core/u
* @param {Object} log - Logger object
* @returns {string} - Normalized absolute path to project root
*/
export function getProjectRoot(projectRootRaw, log) {
function getProjectRoot(projectRootRaw, log) {
// PRECEDENCE ORDER:
// 1. Environment variable override
// 2. Explicitly provided projectRoot in args
@@ -74,28 +73,69 @@ export function getProjectRoot(projectRootRaw, log) {
* @param {Object} log - Logger object.
* @returns {string|null} - The absolute path to the project root, or null if not found.
*/
export function getProjectRootFromSession(session, log) {
if (session && session.roots && session.roots.length > 0) {
const firstRoot = session.roots[0];
if (firstRoot && firstRoot.uri) {
try {
const rootUri = firstRoot.uri;
const rootPath = rootUri.startsWith('file://')
? decodeURIComponent(rootUri.slice(7)) // Remove 'file://' and decode
: rootUri; // Assume it's a path if no scheme
log.info(`Extracted project root from session: ${rootPath}`);
return rootPath;
} catch (e) {
log.error(`Error decoding project root URI from session: ${firstRoot.uri}`, e);
return null;
}
} else {
log.info('Session exists, but first root or its URI is missing.');
function getProjectRootFromSession(session, log) {
try {
// If we have a session with roots array
if (session?.roots?.[0]?.uri) {
const rootUri = session.roots[0].uri;
const rootPath = rootUri.startsWith('file://')
? decodeURIComponent(rootUri.slice(7))
: rootUri;
return rootPath;
}
} else {
log.info('No session or session roots found to extract project root.');
// If we have a session with roots.roots array (different structure)
if (session?.roots?.roots?.[0]?.uri) {
const rootUri = session.roots.roots[0].uri;
const rootPath = rootUri.startsWith('file://')
? decodeURIComponent(rootUri.slice(7))
: rootUri;
return rootPath;
}
// Get the server's location and try to find project root -- this is a fallback necessary in Cursor IDE
const serverPath = process.argv[1]; // This should be the path to server.js, which is in mcp-server/
if (serverPath && serverPath.includes('mcp-server')) {
// Find the mcp-server directory first
const mcpServerIndex = serverPath.indexOf('mcp-server');
if (mcpServerIndex !== -1) {
// Get the path up to mcp-server, which should be the project root
const projectRoot = serverPath.substring(0, mcpServerIndex - 1); // -1 to remove trailing slash
// Verify this looks like our project root by checking for key files/directories
if (fs.existsSync(path.join(projectRoot, '.cursor')) ||
fs.existsSync(path.join(projectRoot, 'mcp-server')) ||
fs.existsSync(path.join(projectRoot, 'package.json'))) {
return projectRoot;
}
}
}
// If we get here, we'll try process.cwd() but only if it's not "/"
const cwd = process.cwd();
if (cwd !== '/') {
return cwd;
}
// Last resort: try to derive from the server path we found earlier
if (serverPath) {
const mcpServerIndex = serverPath.indexOf('mcp-server');
return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : cwd;
}
throw new Error('Could not determine project root');
} catch (e) {
// If we have a server path, use it as a basis for project root
const serverPath = process.argv[1];
if (serverPath && serverPath.includes('mcp-server')) {
const mcpServerIndex = serverPath.indexOf('mcp-server');
return mcpServerIndex !== -1 ? serverPath.substring(0, mcpServerIndex - 1) : process.cwd();
}
// Only use cwd if it's not "/"
const cwd = process.cwd();
return cwd !== '/' ? cwd : '/';
}
return null;
}
/**
@@ -106,7 +146,7 @@ export function getProjectRootFromSession(session, log) {
* @param {Function} processFunction - Optional function to process successful result data
* @returns {Object} - Standardized MCP response object
*/
export function handleApiResult(result, log, errorPrefix = 'API error', processFunction = processMCPResponseData) {
function handleApiResult(result, log, errorPrefix = 'API error', processFunction = processMCPResponseData) {
if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
// Include cache status in error logs
@@ -138,7 +178,7 @@ export function handleApiResult(result, log, errorPrefix = 'API error', processF
* @param {string|undefined} projectRootRaw - Optional raw project root path (will be normalized internally)
* @returns {Object} - The result of the command execution
*/
export function executeTaskMasterCommand(
function executeTaskMasterCommand(
command,
log,
args = [],
@@ -215,7 +255,7 @@ export function executeTaskMasterCommand(
* @returns {Promise<Object>} - An object containing the result, indicating if it was from cache.
* Format: { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }
*/
export async function getCachedOrExecute({ cacheKey, actionFn, log }) {
async function getCachedOrExecute({ cacheKey, actionFn, log }) {
// Check cache first
const cachedResult = contextManager.getCachedData(cacheKey);
@@ -259,7 +299,7 @@ export async function getCachedOrExecute({ cacheKey, actionFn, log }) {
* @param {string[]} fieldsToRemove - An array of field names to remove.
* @returns {Object|Array} - The processed data with specified fields removed.
*/
export function processMCPResponseData(taskOrData, fieldsToRemove = ['details', 'testStrategy']) {
function processMCPResponseData(taskOrData, fieldsToRemove = ['details', 'testStrategy']) {
if (!taskOrData) {
return taskOrData;
}
@@ -316,7 +356,7 @@ export function processMCPResponseData(taskOrData, fieldsToRemove = ['details',
* @param {string|Object} content - Content to include in response
* @returns {Object} - Content response object in FastMCP format
*/
export function createContentResponse(content) {
function createContentResponse(content) {
// FastMCP requires text type, so we format objects as JSON strings
return {
content: [
@@ -351,10 +391,11 @@ export function createErrorResponse(errorMessage) {
// Ensure all functions are exported
export {
getProjectRoot,
getProjectRootFromSession,
handleApiResult,
executeTaskMasterCommand,
getCachedOrExecute,
processMCPResponseData,
createContentResponse,
createErrorResponse
};

View File

@@ -6,7 +6,8 @@
import { z } from "zod";
import {
handleApiResult,
createErrorResponse
createErrorResponse,
getProjectRootFromSession
} from "./utils.js";
import { validateDependenciesDirect } from "../core/task-master-core.js";
@@ -17,26 +18,36 @@ import { validateDependenciesDirect } from "../core/task-master-core.js";
export function registerValidateDependenciesTool(server) {
server.addTool({
name: "validate_dependencies",
description: "Identify invalid dependencies in tasks without fixing them",
description: "Check tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.",
parameters: z.object({
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log }) => {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Validating dependencies with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// Call the direct function wrapper
const result = await validateDependenciesDirect(args, log);
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
const result = await validateDependenciesDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
await reportProgress({ progress: 100 });
// Log result
if (result.success) {
log.info(`Successfully validated dependencies: ${result.data.message}`);
} else {
log.error(`Failed to validate dependencies: ${result.error.message}`);
}
// Use handleApiResult to format the response
return handleApiResult(result, log, 'Error validating dependencies');
} catch (error) {
log.error(`Error in validateDependencies tool: ${error.message}`);
@@ -44,4 +55,4 @@ export function registerValidateDependenciesTool(server) {
}
},
});
}
}