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,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.
*
* @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} context - Additional context (session)
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function setTaskStatusDirect(args, log) {
// Destructure expected args, including the resolved tasksJsonPath
const { tasksJsonPath, id, status, complexityReportPath } = args;
export async function setTaskStatusDirect(args, log, context = {}) {
// Destructure expected args, including the resolved tasksJsonPath and projectRoot
const { tasksJsonPath, id, status, complexityReportPath, projectRoot } = args;
const { session } = context;
try {
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
try {
// 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}`);

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.prompt - New information/context prompt.
* @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 {Object} log - Logger object.
* @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 = {}) {
const { session } = context;
// Destructure expected args, including projectRoot
const { tasksJsonPath, id, prompt, research, projectRoot } = args;
const { tasksJsonPath, id, prompt, research, append, projectRoot } = args;
const logWrapper = createLogWrapper(log);
@@ -118,7 +119,8 @@ export async function updateTaskByIdDirect(args, log, context = {}) {
commandName: 'update-task',
outputType: 'mcp'
},
'json'
'json',
append || false
);
// 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 { moveTaskDirect } from './direct-functions/move-task.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
export { findTasksPath } from './utils/path-utils.js';
@@ -64,7 +70,13 @@ export const directFunctions = new Map([
['initializeProjectDirect', initializeProjectDirect],
['modelsDirect', modelsDirect],
['moveTaskDirect', moveTaskDirect],
['researchDirect', researchDirect]
['researchDirect', researchDirect],
['addTagDirect', addTagDirect],
['deleteTagDirect', deleteTagDirect],
['listTagsDirect', listTagsDirect],
['useTagDirect', useTagDirect],
['renameTagDirect', renameTagDirect],
['copyTagDirect', copyTagDirect]
]);
// Re-export all direct function implementations
@@ -95,5 +107,11 @@ export {
initializeProjectDirect,
modelsDirect,
moveTaskDirect,
researchDirect
researchDirect,
addTagDirect,
deleteTagDirect,
listTagsDirect,
useTagDirect,
renameTagDirect,
copyTagDirect
};

View File

@@ -88,9 +88,11 @@ export function registerAddSubtaskTool(server) {
details: args.details,
status: args.status,
dependencies: args.dependencies,
skipGenerate: args.skipGenerate
skipGenerate: args.skipGenerate,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
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,
id: args.id,
all: args.all
all: args.all,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
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(
{
tasksJsonPath: tasksJsonPath,
outputDir: outputDir
outputDir: outputDir,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {

View File

@@ -29,6 +29,12 @@ import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { registerModelsTool } from './models.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';
/**
@@ -44,17 +50,22 @@ export function registerTaskMasterTools(server) {
registerModelsTool(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);
registerShowTaskTool(server);
registerNextTaskTool(server);
registerComplexityReportTool(server);
// Group 3: Task Status & Management
// Group 4: Task Status & Management
registerSetTaskStatusTool(server);
registerGenerateTool(server);
// Group 4: Task Creation & Modification
// Group 5: Task Creation & Modification
registerAddTaskTool(server);
registerAddSubtaskTool(server);
registerUpdateTool(server);
@@ -65,18 +76,21 @@ export function registerTaskMasterTools(server) {
registerClearSubtasksTool(server);
registerMoveTaskTool(server);
// Group 5: Task Analysis & Expansion
registerAnalyzeProjectComplexityTool(server);
registerExpandTaskTool(server);
registerExpandAllTool(server);
// Group 6: Dependency Management
registerAddDependencyTool(server);
registerRemoveDependencyTool(server);
registerValidateDependenciesTool(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);
} catch (error) {
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';
import { nextTaskDirect } from '../core/task-master-core.js';
import {
resolveTasksPath,
resolveComplexityReportPath
findTasksPath,
findComplexityReportPath
} from '../core/utils/path-utils.js';
/**
@@ -40,10 +40,13 @@ export function registerNextTaskTool(server) {
try {
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;
try {
tasksJsonPath = resolveTasksPath(args, session);
tasksJsonPath = findTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
@@ -54,7 +57,13 @@ export function registerNextTaskTool(server) {
// Resolve the path to complexity report (optional)
let complexityReportPath;
try {
complexityReportPath = resolveComplexityReportPath(args, session);
complexityReportPath = findComplexityReportPath(
{
projectRoot: args.projectRoot,
complexityReport: args.complexityReport
},
log
);
} catch (error) {
log.error(`Error finding complexity report: ${error.message}`);
// This is optional, so we don't fail the operation
@@ -64,9 +73,11 @@ export function registerNextTaskTool(server) {
const result = await nextTaskDirect(
{
tasksJsonPath: tasksJsonPath,
reportPath: complexityReportPath
reportPath: complexityReportPath,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
log.info(`Next task result: ${result.success ? 'found' : 'none'}`);

View File

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

View File

@@ -35,7 +35,7 @@ export function registerRemoveTaskTool(server) {
.optional()
.describe('Whether to skip confirmation prompt (default: false)')
}),
execute: withNormalizedProjectRoot(async (args, { log }) => {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Removing task(s) with ID(s): ${args.id}`);
@@ -58,9 +58,11 @@ export function registerRemoveTaskTool(server) {
const result = await removeTaskDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id
id: args.id,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
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()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log }) => {
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
@@ -85,9 +85,11 @@ export function registerSetTaskStatusTool(server) {
tasksJsonPath: tasksJsonPath,
id: args.id,
status: args.status,
complexityReportPath
complexityReportPath,
projectRoot: args.projectRoot
},
log
log,
{ session }
);
if (result.success) {

View File

@@ -34,6 +34,12 @@ export function registerUpdateTaskTool(server) {
.boolean()
.optional()
.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'),
projectRoot: z
.string()
@@ -67,6 +73,7 @@ export function registerUpdateTaskTool(server) {
id: args.id,
prompt: args.prompt,
research: args.research,
append: args.append,
projectRoot: args.projectRoot
},
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);
}
})
});
}