fix(mcp): make projectRoot optional in all MCP tools

- Update all tool definitions to use z.string().optional() for projectRoot
- Fix direct function implementations to use findTasksJsonPath(args, log) pattern
- Enables consistent project root detection without requiring explicit params
- Update changeset to document these improvements

This change ensures MCP tools work properly with the smart project root
detection system, removing the need for explicit projectRoot parameters in
client applications. Improves usability and reduces integration friction.
This commit is contained in:
Eyal Toledano
2025-04-01 03:12:44 -04:00
parent 65f56978b2
commit d5ecca25db
22 changed files with 27 additions and 37 deletions

View File

@@ -16,6 +16,11 @@
- Parent directory traversal to find tasks.json - Parent directory traversal to find tasks.json
- Package directory as fallback - Package directory as fallback
- Updated all MCP tools to work without explicitly requiring project root:
- Changed all tool definitions from `projectRoot: z.string().describe(...)` to `projectRoot: z.string().optional().describe(...)`
- Updated all direct function implementations from `findTasksJsonPath(args.file, args.projectRoot)` to use the proper `findTasksJsonPath(args, log)` pattern
- These changes allow MCP tools to work properly without requiring the projectRoot parameter, using smart detection to automatically determine the project root
- Add comprehensive PROJECT_MARKERS array for detecting common project files: - Add comprehensive PROJECT_MARKERS array for detecting common project files:
- Task Master specific files (tasks.json, tasks/tasks.json) - Task Master specific files (tasks.json, tasks/tasks.json)
- Version control markers (.git, .svn) - Version control markers (.git, .svn)

View File

@@ -47,7 +47,7 @@ export async function addSubtaskDirect(args, log) {
} }
// Find the tasks.json path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Parse dependencies if provided // Parse dependencies if provided
let dependencies = []; let dependencies = [];

View File

@@ -20,8 +20,8 @@ import { findTasksJsonPath } from '../utils/path-utils.js';
*/ */
export async function addTaskDirect(args, log) { export async function addTaskDirect(args, log) {
try { try {
// Resolve the tasks file path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Check required parameters // Check required parameters
if (!args.prompt) { if (!args.prompt) {

View File

@@ -24,7 +24,7 @@ export async function analyzeTaskComplexityDirect(args, log) {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`); log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
// Find the tasks.json path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Determine output path // Determine output path
let outputPath = args.output || 'scripts/task-complexity-report.json'; let outputPath = args.output || 'scripts/task-complexity-report.json';

View File

@@ -32,7 +32,7 @@ export async function clearSubtasksDirect(args, log) {
} }
// Find the tasks.json path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Check if tasks.json exists // Check if tasks.json exists
if (!fs.existsSync(tasksPath)) { if (!fs.existsSync(tasksPath)) {

View File

@@ -22,7 +22,7 @@ export async function expandAllTasksDirect(args, log) {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`); log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
// Find the tasks.json path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Parse parameters // Parse parameters
const numSubtasks = args.num ? parseInt(args.num, 10) : undefined; const numSubtasks = args.num ? parseInt(args.num, 10) : undefined;

View File

@@ -18,8 +18,8 @@ export async function fixDependenciesDirect(args, log) {
try { try {
log.info(`Fixing invalid dependencies in tasks...`); log.info(`Fixing invalid dependencies in tasks...`);
// Determine the tasks file path // Find the tasks.json path
const tasksPath = args.file || await findTasksJsonPath(args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Verify the file exists // Verify the file exists
if (!fs.existsSync(tasksPath)) { if (!fs.existsSync(tasksPath)) {

View File

@@ -41,7 +41,7 @@ export async function removeDependencyDirect(args, log) {
} }
// Find the tasks.json path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Format IDs for the core function // Format IDs for the core function
const taskId = args.id.includes && args.id.includes('.') ? args.id : parseInt(args.id, 10); const taskId = args.id.includes && args.id.includes('.') ? args.id : parseInt(args.id, 10);

View File

@@ -42,7 +42,7 @@ export async function removeSubtaskDirect(args, log) {
} }
// Find the tasks.json path // Find the tasks.json path
const tasksPath = findTasksJsonPath(args.file, args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Convert convertToTask to a boolean // Convert convertToTask to a boolean
const convertToTask = args.convert === true; const convertToTask = args.convert === true;

View File

@@ -18,8 +18,8 @@ export async function validateDependenciesDirect(args, log) {
try { try {
log.info(`Validating dependencies in tasks...`); log.info(`Validating dependencies in tasks...`);
// Determine the tasks file path // Find the tasks.json path
const tasksPath = args.file || await findTasksJsonPath(args.projectRoot); const tasksPath = findTasksJsonPath(args, log);
// Verify the file exists // Verify the file exists
if (!fs.existsSync(tasksPath)) { if (!fs.existsSync(tasksPath)) {

View File

@@ -22,7 +22,7 @@ export function registerAddDependencyTool(server) {
id: z.string().describe("ID of task that will depend on another task"), id: z.string().describe("ID of task that will depend on another task"),
dependsOn: z.string().describe("ID of task that will become a dependency"), dependsOn: z.string().describe("ID of task that will become a dependency"),
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().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 }) => {
try { try {

View File

@@ -28,7 +28,7 @@ export function registerAddSubtaskTool(server) {
dependencies: z.string().optional().describe("Comma-separated list of dependency IDs for the new subtask"), dependencies: z.string().optional().describe("Comma-separated list of dependency IDs for the new subtask"),
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)"),
skipGenerate: z.boolean().optional().describe("Skip regenerating task files"), skipGenerate: z.boolean().optional().describe("Skip regenerating task files"),
projectRoot: z.string().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 }) => {
try { try {

View File

@@ -23,7 +23,7 @@ export function registerAddTaskTool(server) {
dependencies: z.string().optional().describe("Comma-separated list of task IDs this task depends on"), dependencies: z.string().optional().describe("Comma-separated list of task IDs this task depends on"),
priority: z.string().optional().describe("Task priority (high, medium, low)"), priority: z.string().optional().describe("Task priority (high, medium, low)"),
file: z.string().optional().describe("Path to the tasks file"), file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().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 ({ prompt, dependencies, priority, file, projectRoot }, log) => { execute: async ({ prompt, dependencies, priority, file, projectRoot }, log) => {
try { try {

View File

@@ -24,7 +24,7 @@ export function registerAnalyzeTool(server) {
threshold: z.union([z.number(), z.string()]).optional().describe("Minimum complexity score to recommend expansion (1-10) (default: 5)"), threshold: z.union([z.number(), z.string()]).optional().describe("Minimum complexity score to recommend expansion (1-10) (default: 5)"),
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)"),
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().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 }) => {
try { try {

View File

@@ -22,7 +22,7 @@ export function registerClearSubtasksTool(server) {
id: z.string().optional().describe("Task IDs (comma-separated) to clear subtasks from"), id: z.string().optional().describe("Task IDs (comma-separated) to clear subtasks from"),
all: z.boolean().optional().describe("Clear subtasks from all tasks"), all: z.boolean().optional().describe("Clear subtasks from all tasks"),
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().describe("Root directory of the project (default: current working directory)") projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}).refine(data => data.id || data.all, { }).refine(data => data.id || data.all, {
message: "Either 'id' or 'all' parameter must be provided", message: "Either 'id' or 'all' parameter must be provided",
path: ["id", "all"] path: ["id", "all"]

View File

@@ -20,7 +20,7 @@ export function registerComplexityReportTool(server) {
description: "Display the complexity analysis report in a readable format", description: "Display the complexity analysis report in a readable format",
parameters: z.object({ parameters: z.object({
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().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 }) => {
try { try {

View File

@@ -24,7 +24,7 @@ export function registerExpandAllTool(server) {
prompt: z.string().optional().describe("Additional context to guide subtask generation"), prompt: z.string().optional().describe("Additional context to guide subtask generation"),
force: z.boolean().optional().describe("Force regeneration of subtasks for tasks that already have them"), force: z.boolean().optional().describe("Force regeneration of subtasks for tasks that already have them"),
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().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 }) => {
try { try {

View File

@@ -20,7 +20,7 @@ export function registerFixDependenciesTool(server) {
description: "Fix invalid dependencies in tasks automatically", description: "Fix invalid dependencies in tasks automatically",
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().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 }) => {
try { try {

View File

@@ -22,7 +22,7 @@ export function registerRemoveDependencyTool(server) {
id: z.string().describe("Task ID to remove dependency from"), id: z.string().describe("Task ID to remove dependency from"),
dependsOn: z.string().describe("Task ID to remove as a dependency"), dependsOn: z.string().describe("Task ID to remove as a dependency"),
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().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 }) => {
try { try {

View File

@@ -23,7 +23,7 @@ export function registerRemoveSubtaskTool(server) {
convert: z.boolean().optional().describe("Convert the subtask to a standalone task instead of deleting it"), convert: z.boolean().optional().describe("Convert the subtask to a standalone task instead of deleting it"),
file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"), file: z.string().optional().describe("Path to the tasks file (default: tasks/tasks.json)"),
skipGenerate: z.boolean().optional().describe("Skip regenerating task files"), skipGenerate: z.boolean().optional().describe("Skip regenerating task files"),
projectRoot: z.string().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 }) => {
try { try {

View File

@@ -1207,9 +1207,3 @@ This method provides a consistent way to access environment variables defined in
### Details: ### Details:
## 47. adjust rules so it prioritizes mcp commands over script [pending]
### Dependencies: None
### Description:
### Details:

View File

@@ -1780,15 +1780,6 @@
"status": "pending", "status": "pending",
"dependencies": [], "dependencies": [],
"parentTaskId": 23 "parentTaskId": 23
},
{
"id": 47,
"title": "adjust rules so it prioritizes mcp commands over script",
"description": "",
"details": "",
"status": "pending",
"dependencies": [],
"parentTaskId": 23
} }
] ]
}, },