feat(tags): Implement full MCP support for Tagged Task Lists and update-task append mode

This commit is contained in:
Eyal Toledano
2025-06-13 13:20:37 -04:00
parent 932825c2d6
commit 514fdb0b78
29 changed files with 8089 additions and 6787 deletions

View File

@@ -0,0 +1,18 @@
---
"task-master-ai": minor
---
Enhance update-task with --append flag for timestamped task updates
Adds the `--append` flag to `update-task` command, enabling it to behave like `update-subtask` with timestamped information appending. This provides more flexible task updating options:
**CLI Enhancement:**
- `task-master update-task --id=5 --prompt="New info"` - Full task update (existing behavior)
- `task-master update-task --id=5 --append --prompt="Progress update"` - Append timestamped info to task details
**Full MCP Integration:**
- MCP tool `update_task` now supports `append` parameter
- Seamless integration with Cursor and other MCP clients
- Consistent behavior between CLI and MCP interfaces
Instead of requiring separate subtask creation for progress tracking, you can now append timestamped information directly to parent tasks while preserving the option for comprehensive task updates.

View File

@@ -25,6 +25,16 @@ The new tagged system fundamentally changes how tasks are organized:
- `task-master rename-tag <old> <new>` - Rename tags with automatic current tag reference updates - `task-master rename-tag <old> <new>` - Rename tags with automatic current tag reference updates
- `task-master copy-tag <source> <target> [options]` - Duplicate tag contexts for experimentation - `task-master copy-tag <source> <target> [options]` - Duplicate tag contexts for experimentation
**🤖 Full MCP Integration for Tag Management:**
Task Master's multi-context capabilities are now fully exposed through the MCP server, enabling powerful agentic workflows:
- **`list_tags`**: List all available tag contexts.
- **`add_tag`**: Programmatically create new tags.
- **`delete_tag`**: Remove tag contexts.
- **`use_tag`**: Switch the agent's active task context.
- **`rename_tag`**: Rename existing tags.
- **`copy_tag`**: Duplicate entire task contexts for experimentation.
**Tag Creation Options:** **Tag Creation Options:**
- `--copy-from-current` - Copy tasks from currently active tag - `--copy-from-current` - Copy tasks from currently active tag
- `--copy-from=<tag>` - Copy tasks from specific tag - `--copy-from=<tag>` - Copy tasks from specific tag

View File

@@ -1,6 +1,6 @@
{ {
"currentTag": "master", "currentTag": "master",
"lastSwitched": "2025-06-13T07:55:31.313Z", "lastSwitched": "2025-06-13T15:17:22.946Z",
"branchTagMapping": {}, "branchTagMapping": {},
"migrationNoticeShown": true "migrationNoticeShown": true
} }

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,121 @@
/**
* add-tag.js
* Direct function implementation for creating a new tag
*/
import { createTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for creating a new tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.name - Name of the new tag to create
* @param {boolean} [args.copyFromCurrent=false] - Whether to copy tasks from current tag
* @param {string} [args.copyFromTag] - Specific tag to copy tasks from
* @param {string} [args.description] - Optional description for the tag
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function addTagDirect(args, log, context = {}) {
// Destructure expected args
const {
tasksJsonPath,
name,
copyFromCurrent = false,
copyFromTag,
description,
projectRoot
} = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('addTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!name || typeof name !== 'string') {
log.error('Missing required parameter: name');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Tag name is required and must be a string'
}
};
}
log.info(`Creating new tag: ${name}`);
// Prepare options
const options = {
copyFromCurrent,
copyFromTag,
description
};
// Call the createTag function
const result = await createTag(
tasksJsonPath,
name,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tagName: result.tagName,
created: result.created,
tasksCopied: result.tasksCopied,
sourceTag: result.sourceTag,
description: result.description,
message: `Successfully created tag "${result.tagName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in addTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'ADD_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,125 @@
/**
* copy-tag.js
* Direct function implementation for copying a tag
*/
import { copyTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for copying a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.sourceName - Name of the source tag to copy from
* @param {string} args.targetName - Name of the new tag to create
* @param {string} [args.description] - Optional description for the new tag
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function copyTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, sourceName, targetName, description, projectRoot } =
args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('copyTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!sourceName || typeof sourceName !== 'string') {
log.error('Missing required parameter: sourceName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Source tag name is required and must be a string'
}
};
}
if (!targetName || typeof targetName !== 'string') {
log.error('Missing required parameter: targetName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Target tag name is required and must be a string'
}
};
}
log.info(`Copying tag from "${sourceName}" to "${targetName}"`);
// Prepare options
const options = {
description
};
// Call the copyTag function
const result = await copyTag(
tasksJsonPath,
sourceName,
targetName,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
sourceName: result.sourceName,
targetName: result.targetName,
copied: result.copied,
tasksCopied: result.tasksCopied,
description: result.description,
message: `Successfully copied tag from "${result.sourceName}" to "${result.targetName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in copyTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'COPY_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,110 @@
/**
* delete-tag.js
* Direct function implementation for deleting a tag
*/
import { deleteTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for deleting a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.name - Name of the tag to delete
* @param {boolean} [args.yes=false] - Skip confirmation prompts
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function deleteTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, name, yes = false, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('deleteTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!name || typeof name !== 'string') {
log.error('Missing required parameter: name');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Tag name is required and must be a string'
}
};
}
log.info(`Deleting tag: ${name}`);
// Prepare options
const options = {
yes // For MCP, we always skip confirmation prompts
};
// Call the deleteTag function
const result = await deleteTag(
tasksJsonPath,
name,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tagName: result.tagName,
deleted: result.deleted,
tasksDeleted: result.tasksDeleted,
wasCurrentTag: result.wasCurrentTag,
switchedToMaster: result.switchedToMaster,
message: `Successfully deleted tag "${result.tagName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in deleteTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'DELETE_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,132 @@
/**
* list-tags.js
* Direct function implementation for listing all tags
*/
import { tags } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for listing all tags with error handling.
*
* @param {Object} args - Command arguments
* @param {boolean} [args.showMetadata=false] - Whether to include metadata in the output
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function listTagsDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, showMetadata = false, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('listTagsDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
log.info('Listing all tags');
// Prepare options
const options = {
showMetadata
};
// Call the tags function
const result = await tags(
tasksJsonPath,
options,
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Transform the result to remove full task data and provide summary info
const tagsSummary = result.tags.map((tag) => {
const tasks = tag.tasks || [];
// Calculate status breakdown
const statusBreakdown = tasks.reduce((acc, task) => {
const status = task.status || 'pending';
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
// Calculate subtask counts
const subtaskCounts = tasks.reduce(
(acc, task) => {
if (task.subtasks && task.subtasks.length > 0) {
acc.totalSubtasks += task.subtasks.length;
task.subtasks.forEach((subtask) => {
const subStatus = subtask.status || 'pending';
acc.subtasksByStatus[subStatus] =
(acc.subtasksByStatus[subStatus] || 0) + 1;
});
}
return acc;
},
{ totalSubtasks: 0, subtasksByStatus: {} }
);
return {
name: tag.name,
isCurrent: tag.isCurrent,
taskCount: tasks.length,
completedTasks: tag.completedTasks,
statusBreakdown,
subtaskCounts,
created: tag.created,
description: tag.description
};
});
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tags: tagsSummary,
currentTag: result.currentTag,
totalTags: result.totalTags,
message: `Found ${result.totalTags} tag(s)`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in listTagsDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'LIST_TAGS_ERROR',
message: error.message
}
};
}
}

View File

@@ -0,0 +1,118 @@
/**
* rename-tag.js
* Direct function implementation for renaming a tag
*/
import { renameTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for renaming a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.oldName - Current name of the tag to rename
* @param {string} args.newName - New name for the tag
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function renameTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, oldName, newName, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('renameTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!oldName || typeof oldName !== 'string') {
log.error('Missing required parameter: oldName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Old tag name is required and must be a string'
}
};
}
if (!newName || typeof newName !== 'string') {
log.error('Missing required parameter: newName');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'New tag name is required and must be a string'
}
};
}
log.info(`Renaming tag from "${oldName}" to "${newName}"`);
// Call the renameTag function
const result = await renameTag(
tasksJsonPath,
oldName,
newName,
{}, // options (empty for now)
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
oldName: result.oldName,
newName: result.newName,
renamed: result.renamed,
taskCount: result.taskCount,
wasCurrentTag: result.wasCurrentTag,
message: `Successfully renamed tag from "${result.oldName}" to "${result.newName}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in renameTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'RENAME_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -13,13 +13,15 @@ import { nextTaskDirect } from './next-task.js';
/** /**
* Direct function wrapper for setTaskStatus with error handling. * Direct function wrapper for setTaskStatus with error handling.
* *
* @param {Object} args - Command arguments containing id, status and tasksJsonPath. * @param {Object} args - Command arguments containing id, status, tasksJsonPath, and projectRoot.
* @param {Object} log - Logger object. * @param {Object} log - Logger object.
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object with success status and data/error information. * @returns {Promise<Object>} - Result object with success status and data/error information.
*/ */
export async function setTaskStatusDirect(args, log) { export async function setTaskStatusDirect(args, log, context = {}) {
// Destructure expected args, including the resolved tasksJsonPath // Destructure expected args, including the resolved tasksJsonPath and projectRoot
const { tasksJsonPath, id, status, complexityReportPath } = args; const { tasksJsonPath, id, status, complexityReportPath, projectRoot } = args;
const { session } = context;
try { try {
log.info(`Setting task status with args: ${JSON.stringify(args)}`); log.info(`Setting task status with args: ${JSON.stringify(args)}`);
@@ -67,7 +69,11 @@ export async function setTaskStatusDirect(args, log) {
enableSilentMode(); // Enable silent mode before calling core function enableSilentMode(); // Enable silent mode before calling core function
try { try {
// Call the core function // Call the core function
await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log }); await setTaskStatus(tasksPath, taskId, newStatus, {
mcpLog: log,
projectRoot,
session
});
log.info(`Successfully set task ${taskId} status to ${newStatus}`); log.info(`Successfully set task ${taskId} status to ${newStatus}`);

View File

@@ -19,6 +19,7 @@ import { createLogWrapper } from '../../tools/utils.js';
* @param {string} args.id - Task ID (or subtask ID like "1.2"). * @param {string} args.id - Task ID (or subtask ID like "1.2").
* @param {string} args.prompt - New information/context prompt. * @param {string} args.prompt - New information/context prompt.
* @param {boolean} [args.research] - Whether to use research role. * @param {boolean} [args.research] - Whether to use research role.
* @param {boolean} [args.append] - Whether to append timestamped information instead of full update.
* @param {string} [args.projectRoot] - Project root path. * @param {string} [args.projectRoot] - Project root path.
* @param {Object} log - Logger object. * @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data. * @param {Object} context - Context object containing session data.
@@ -27,7 +28,7 @@ import { createLogWrapper } from '../../tools/utils.js';
export async function updateTaskByIdDirect(args, log, context = {}) { export async function updateTaskByIdDirect(args, log, context = {}) {
const { session } = context; const { session } = context;
// Destructure expected args, including projectRoot // Destructure expected args, including projectRoot
const { tasksJsonPath, id, prompt, research, projectRoot } = args; const { tasksJsonPath, id, prompt, research, append, projectRoot } = args;
const logWrapper = createLogWrapper(log); const logWrapper = createLogWrapper(log);
@@ -118,7 +119,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
commandName: 'update-task', commandName: 'update-task',
outputType: 'mcp' outputType: 'mcp'
}, },
'json' 'json',
append || false
); );
// Check if the core function returned null or an object without success // Check if the core function returned null or an object without success

View File

@@ -0,0 +1,103 @@
/**
* use-tag.js
* Direct function implementation for switching to a tag
*/
import { useTag } from '../../../../scripts/modules/task-manager/tag-management.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for switching to a tag with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.name - Name of the tag to switch to
* @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function useTagDirect(args, log, context = {}) {
// Destructure expected args
const { tasksJsonPath, name, projectRoot } = args;
const { session } = context;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('useTagDirect called without tasksJsonPath');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Check required parameters
if (!name || typeof name !== 'string') {
log.error('Missing required parameter: name');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message: 'Tag name is required and must be a string'
}
};
}
log.info(`Switching to tag: ${name}`);
// Call the useTag function
const result = await useTag(
tasksJsonPath,
name,
{}, // options (empty for now)
{
session,
mcpLog,
projectRoot
},
'json' // outputFormat - use 'json' to suppress CLI UI
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
tagName: result.currentTag,
switched: result.switched,
previousTag: result.previousTag,
taskCount: result.taskCount,
message: `Successfully switched to tag "${result.currentTag}"`
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in useTagDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'USE_TAG_ERROR',
message: error.message
}
};
}
}

View File

@@ -32,6 +32,12 @@ import { initializeProjectDirect } from './direct-functions/initialize-project.j
import { modelsDirect } from './direct-functions/models.js'; import { modelsDirect } from './direct-functions/models.js';
import { moveTaskDirect } from './direct-functions/move-task.js'; import { moveTaskDirect } from './direct-functions/move-task.js';
import { researchDirect } from './direct-functions/research.js'; import { researchDirect } from './direct-functions/research.js';
import { addTagDirect } from './direct-functions/add-tag.js';
import { deleteTagDirect } from './direct-functions/delete-tag.js';
import { listTagsDirect } from './direct-functions/list-tags.js';
import { useTagDirect } from './direct-functions/use-tag.js';
import { renameTagDirect } from './direct-functions/rename-tag.js';
import { copyTagDirect } from './direct-functions/copy-tag.js';
// Re-export utility functions // Re-export utility functions
export { findTasksPath } from './utils/path-utils.js'; export { findTasksPath } from './utils/path-utils.js';
@@ -64,7 +70,13 @@ export const directFunctions = new Map([
['initializeProjectDirect', initializeProjectDirect], ['initializeProjectDirect', initializeProjectDirect],
['modelsDirect', modelsDirect], ['modelsDirect', modelsDirect],
['moveTaskDirect', moveTaskDirect], ['moveTaskDirect', moveTaskDirect],
['researchDirect', researchDirect] ['researchDirect', researchDirect],
['addTagDirect', addTagDirect],
['deleteTagDirect', deleteTagDirect],
['listTagsDirect', listTagsDirect],
['useTagDirect', useTagDirect],
['renameTagDirect', renameTagDirect],
['copyTagDirect', copyTagDirect]
]); ]);
// Re-export all direct function implementations // Re-export all direct function implementations
@@ -95,5 +107,11 @@ export {
initializeProjectDirect, initializeProjectDirect,
modelsDirect, modelsDirect,
moveTaskDirect, moveTaskDirect,
researchDirect researchDirect,
addTagDirect,
deleteTagDirect,
listTagsDirect,
useTagDirect,
renameTagDirect,
copyTagDirect
}; };

View File

@@ -88,9 +88,11 @@ export function registerAddSubtaskTool(server) {
details: args.details, details: args.details,
status: args.status, status: args.status,
dependencies: args.dependencies, dependencies: args.dependencies,
skipGenerate: args.skipGenerate skipGenerate: args.skipGenerate,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
if (result.success) { if (result.success) {

View File

@@ -0,0 +1,92 @@
/**
* tools/add-tag.js
* Tool to create a new tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { addTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the addTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerAddTagTool(server) {
server.addTool({
name: 'add_tag',
description: 'Create a new tag for organizing tasks in different contexts',
parameters: z.object({
name: z.string().describe('Name of the new tag to create'),
copyFromCurrent: z
.boolean()
.optional()
.describe(
'Whether to copy tasks from the current tag (default: false)'
),
copyFromTag: z
.string()
.optional()
.describe('Specific tag to copy tasks from'),
description: z
.string()
.optional()
.describe('Optional description for the tag'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting add-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await addTagDirect(
{
tasksJsonPath: tasksJsonPath,
name: args.name,
copyFromCurrent: args.copyFromCurrent,
copyFromTag: args.copyFromTag,
description: args.description,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error creating tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in add-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -63,9 +63,11 @@ export function registerClearSubtasksTool(server) {
{ {
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
id: args.id, id: args.id,
all: args.all all: args.all,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
if (result.success) { if (result.success) {

View File

@@ -0,0 +1,83 @@
/**
* tools/copy-tag.js
* Tool to copy an existing tag to a new tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { copyTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the copyTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerCopyTagTool(server) {
server.addTool({
name: 'copy_tag',
description:
'Copy an existing tag to create a new tag with all tasks and metadata',
parameters: z.object({
sourceName: z.string().describe('Name of the source tag to copy from'),
targetName: z.string().describe('Name of the new tag to create'),
description: z
.string()
.optional()
.describe('Optional description for the new tag'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting copy-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await copyTagDirect(
{
tasksJsonPath: tasksJsonPath,
sourceName: args.sourceName,
targetName: args.targetName,
description: args.description,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error copying tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in copy-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -0,0 +1,80 @@
/**
* tools/delete-tag.js
* Tool to delete an existing tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { deleteTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the deleteTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerDeleteTagTool(server) {
server.addTool({
name: 'delete_tag',
description: 'Delete an existing tag and all its tasks',
parameters: z.object({
name: z.string().describe('Name of the tag to delete'),
yes: z
.boolean()
.optional()
.describe('Skip confirmation prompts (default: true for MCP)'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting delete-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function (always skip confirmation for MCP)
const result = await deleteTagDirect(
{
tasksJsonPath: tasksJsonPath,
name: args.name,
yes: args.yes !== undefined ? args.yes : true, // Default to true for MCP
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error deleting tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in delete-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -57,9 +57,11 @@ export function registerGenerateTool(server) {
const result = await generateTaskFilesDirect( const result = await generateTaskFilesDirect(
{ {
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
outputDir: outputDir outputDir: outputDir,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
if (result.success) { if (result.success) {

View File

@@ -29,6 +29,12 @@ import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js'; import { registerInitializeProjectTool } from './initialize-project.js';
import { registerModelsTool } from './models.js'; import { registerModelsTool } from './models.js';
import { registerMoveTaskTool } from './move-task.js'; import { registerMoveTaskTool } from './move-task.js';
import { registerAddTagTool } from './add-tag.js';
import { registerDeleteTagTool } from './delete-tag.js';
import { registerListTagsTool } from './list-tags.js';
import { registerUseTagTool } from './use-tag.js';
import { registerRenameTagTool } from './rename-tag.js';
import { registerCopyTagTool } from './copy-tag.js';
import { registerResearchTool } from './research.js'; import { registerResearchTool } from './research.js';
/** /**
@@ -44,17 +50,22 @@ export function registerTaskMasterTools(server) {
registerModelsTool(server); registerModelsTool(server);
registerParsePRDTool(server); registerParsePRDTool(server);
// Group 2: Task Listing & Viewing // Group 2: Task Analysis & Expansion
registerAnalyzeProjectComplexityTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
// Group 3: Task Listing & Viewing
registerListTasksTool(server); registerListTasksTool(server);
registerShowTaskTool(server); registerShowTaskTool(server);
registerNextTaskTool(server); registerNextTaskTool(server);
registerComplexityReportTool(server); registerComplexityReportTool(server);
// Group 3: Task Status & Management // Group 4: Task Status & Management
registerSetTaskStatusTool(server); registerSetTaskStatusTool(server);
registerGenerateTool(server); registerGenerateTool(server);
// Group 4: Task Creation & Modification // Group 5: Task Creation & Modification
registerAddTaskTool(server); registerAddTaskTool(server);
registerAddSubtaskTool(server); registerAddSubtaskTool(server);
registerUpdateTool(server); registerUpdateTool(server);
@@ -65,18 +76,21 @@ export function registerTaskMasterTools(server) {
registerClearSubtasksTool(server); registerClearSubtasksTool(server);
registerMoveTaskTool(server); registerMoveTaskTool(server);
// Group 5: Task Analysis & Expansion
registerAnalyzeProjectComplexityTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
// Group 6: Dependency Management // Group 6: Dependency Management
registerAddDependencyTool(server); registerAddDependencyTool(server);
registerRemoveDependencyTool(server); registerRemoveDependencyTool(server);
registerValidateDependenciesTool(server); registerValidateDependenciesTool(server);
registerFixDependenciesTool(server); registerFixDependenciesTool(server);
// Group 7: AI-Powered Features // Group 7: Tag Management
registerListTagsTool(server);
registerAddTagTool(server);
registerDeleteTagTool(server);
registerUseTagTool(server);
registerRenameTagTool(server);
registerCopyTagTool(server);
// Group 8: Research Features
registerResearchTool(server); registerResearchTool(server);
} catch (error) { } catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`); logger.error(`Error registering Task Master tools: ${error.message}`);

View File

@@ -0,0 +1,78 @@
/**
* tools/list-tags.js
* Tool to list all available tags
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { listTagsDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the listTags tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerListTagsTool(server) {
server.addTool({
name: 'list_tags',
description: 'List all available tags with task counts and metadata',
parameters: z.object({
showMetadata: z
.boolean()
.optional()
.describe('Whether to include metadata in the output (default: false)'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting list-tags with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await listTagsDirect(
{
tasksJsonPath: tasksJsonPath,
showMetadata: args.showMetadata,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error listing tags',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in list-tags tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -11,8 +11,8 @@ import {
} from './utils.js'; } from './utils.js';
import { nextTaskDirect } from '../core/task-master-core.js'; import { nextTaskDirect } from '../core/task-master-core.js';
import { import {
resolveTasksPath, findTasksPath,
resolveComplexityReportPath findComplexityReportPath
} from '../core/utils/path-utils.js'; } from '../core/utils/path-utils.js';
/** /**
@@ -40,10 +40,13 @@ export function registerNextTaskTool(server) {
try { try {
log.info(`Finding next task with args: ${JSON.stringify(args)}`); log.info(`Finding next task with args: ${JSON.stringify(args)}`);
// Resolve the path to tasks.json using new path utilities // Resolve the path to tasks.json
let tasksJsonPath; let tasksJsonPath;
try { try {
tasksJsonPath = resolveTasksPath(args, session); tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) { } catch (error) {
log.error(`Error finding tasks.json: ${error.message}`); log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse( return createErrorResponse(
@@ -54,7 +57,13 @@ export function registerNextTaskTool(server) {
// Resolve the path to complexity report (optional) // Resolve the path to complexity report (optional)
let complexityReportPath; let complexityReportPath;
try { try {
complexityReportPath = resolveComplexityReportPath(args, session); complexityReportPath = findComplexityReportPath(
{
projectRoot: args.projectRoot,
complexityReport: args.complexityReport
},
log
);
} catch (error) { } catch (error) {
log.error(`Error finding complexity report: ${error.message}`); log.error(`Error finding complexity report: ${error.message}`);
// This is optional, so we don't fail the operation // This is optional, so we don't fail the operation
@@ -64,9 +73,11 @@ export function registerNextTaskTool(server) {
const result = await nextTaskDirect( const result = await nextTaskDirect(
{ {
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
reportPath: complexityReportPath reportPath: complexityReportPath,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
log.info(`Next task result: ${result.success ? 'found' : 'none'}`); log.info(`Next task result: ${result.success ? 'found' : 'none'}`);

View File

@@ -46,7 +46,7 @@ export function registerRemoveSubtaskTool(server) {
.string() .string()
.describe('The directory of the project. Must be an absolute path.') .describe('The directory of the project. Must be an absolute path.')
}), }),
execute: withNormalizedProjectRoot(async (args, { log }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try { try {
log.info(`Removing subtask with args: ${JSON.stringify(args)}`); log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
@@ -69,9 +69,11 @@ export function registerRemoveSubtaskTool(server) {
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
id: args.id, id: args.id,
convert: args.convert, convert: args.convert,
skipGenerate: args.skipGenerate skipGenerate: args.skipGenerate,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
if (result.success) { if (result.success) {

View File

@@ -35,7 +35,7 @@ export function registerRemoveTaskTool(server) {
.optional() .optional()
.describe('Whether to skip confirmation prompt (default: false)') .describe('Whether to skip confirmation prompt (default: false)')
}), }),
execute: withNormalizedProjectRoot(async (args, { log }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try { try {
log.info(`Removing task(s) with ID(s): ${args.id}`); log.info(`Removing task(s) with ID(s): ${args.id}`);
@@ -58,9 +58,11 @@ export function registerRemoveTaskTool(server) {
const result = await removeTaskDirect( const result = await removeTaskDirect(
{ {
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
id: args.id id: args.id,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
if (result.success) { if (result.success) {

View File

@@ -0,0 +1,77 @@
/**
* tools/rename-tag.js
* Tool to rename an existing tag
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { renameTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the renameTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerRenameTagTool(server) {
server.addTool({
name: 'rename_tag',
description: 'Rename an existing tag',
parameters: z.object({
oldName: z.string().describe('Current name of the tag to rename'),
newName: z.string().describe('New name for the tag'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting rename-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await renameTagDirect(
{
tasksJsonPath: tasksJsonPath,
oldName: args.oldName,
newName: args.newName,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error renaming tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in rename-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -49,7 +49,7 @@ export function registerSetTaskStatusTool(server) {
.string() .string()
.describe('The directory of the project. Must be an absolute path.') .describe('The directory of the project. Must be an absolute path.')
}), }),
execute: withNormalizedProjectRoot(async (args, { log }) => { execute: withNormalizedProjectRoot(async (args, { log, session }) => {
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}`);
@@ -85,9 +85,11 @@ export function registerSetTaskStatusTool(server) {
tasksJsonPath: tasksJsonPath, tasksJsonPath: tasksJsonPath,
id: args.id, id: args.id,
status: args.status, status: args.status,
complexityReportPath complexityReportPath,
projectRoot: args.projectRoot
}, },
log log,
{ session }
); );
if (result.success) { if (result.success) {

View File

@@ -34,6 +34,12 @@ export function registerUpdateTaskTool(server) {
.boolean() .boolean()
.optional() .optional()
.describe('Use Perplexity AI for research-backed updates'), .describe('Use Perplexity AI for research-backed updates'),
append: z
.boolean()
.optional()
.describe(
'Append timestamped information to task details instead of full update'
),
file: z.string().optional().describe('Absolute path to the tasks file'), file: z.string().optional().describe('Absolute path to the tasks file'),
projectRoot: z projectRoot: z
.string() .string()
@@ -67,6 +73,7 @@ export function registerUpdateTaskTool(server) {
id: args.id, id: args.id,
prompt: args.prompt, prompt: args.prompt,
research: args.research, research: args.research,
append: args.append,
projectRoot: args.projectRoot projectRoot: args.projectRoot
}, },
log, log,

View File

@@ -0,0 +1,75 @@
/**
* tools/use-tag.js
* Tool to switch to a different tag context
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { useTagDirect } from '../core/task-master-core.js';
import { findTasksPath } from '../core/utils/path-utils.js';
/**
* Register the useTag tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerUseTagTool(server) {
server.addTool({
name: 'use_tag',
description: 'Switch to a different tag context for task operations',
parameters: z.object({
name: z.string().describe('Name of the tag to switch to'),
file: z
.string()
.optional()
.describe('Path to the tasks file (default: tasks/tasks.json)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Starting use-tag with args: ${JSON.stringify(args)}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function
const result = await useTagDirect(
{
tasksJsonPath: tasksJsonPath,
name: args.name,
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(
result,
log,
'Error switching tag',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error in use-tag tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}

View File

@@ -948,6 +948,10 @@ function registerCommands(programInstance) {
'-r, --research', '-r, --research',
'Use Perplexity AI for research-backed task updates' 'Use Perplexity AI for research-backed task updates'
) )
.option(
'--append',
'Append timestamped information to task details instead of full update'
)
.option('--tag <tag>', 'Specify tag context for task operations') .option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => { .action(async (options) => {
try { try {
@@ -1058,7 +1062,9 @@ function registerCommands(programInstance) {
taskId, taskId,
prompt, prompt,
useResearch, useResearch,
{ projectRoot, tag } { projectRoot, tag },
'text',
options.append || false
); );
// If the task wasn't updated (e.g., if it was already marked as done) // If the task wasn't updated (e.g., if it was already marked as done)