feat: connect get-task and get-tasks to remote (#1346)

This commit is contained in:
Ralph Khreish
2025-10-27 15:23:27 +01:00
committed by GitHub
parent 5381e21e57
commit 25addf919f
20 changed files with 937 additions and 589 deletions

View File

@@ -1,114 +0,0 @@
/**
* list-tasks.js
* Direct function implementation for listing tasks
*/
import { listTasks } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for listTasks with error handling and caching.
*
* @param {Object} args - Command arguments (now expecting tasksJsonPath explicitly).
* @param {string} args.tasksJsonPath - Path to the tasks.json file.
* @param {string} args.reportPath - Path to the report file.
* @param {string} args.status - Status of the task.
* @param {boolean} args.withSubtasks - Whether to include subtasks.
* @param {string} args.projectRoot - Project root path (for MCP/env fallback)
* @param {string} args.tag - Tag for the task (optional)
* @param {Object} log - Logger object.
* @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string } }.
*/
export async function listTasksDirect(args, log, context = {}) {
// Destructure the explicit tasksJsonPath from args
const { tasksJsonPath, reportPath, status, withSubtasks, projectRoot, tag } =
args;
const { session } = context;
if (!tasksJsonPath) {
log.error('listTasksDirect called without tasksJsonPath');
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Use the explicit tasksJsonPath for cache key
const statusFilter = status || 'all';
const withSubtasksFilter = withSubtasks || false;
// Define the action function to be executed on cache miss
const coreListTasksAction = async () => {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
log.info(
`Executing core listTasks function for path: ${tasksJsonPath}, filter: ${statusFilter}, subtasks: ${withSubtasksFilter}`
);
// Pass the explicit tasksJsonPath to the core function
const resultData = listTasks(
tasksJsonPath,
statusFilter,
reportPath,
withSubtasksFilter,
'json',
{ projectRoot, session, tag }
);
if (!resultData || !resultData.tasks) {
log.error('Invalid or empty response from listTasks core function');
return {
success: false,
error: {
code: 'INVALID_CORE_RESPONSE',
message: 'Invalid or empty response from listTasks core function'
}
};
}
log.info(
`Core listTasks function retrieved ${resultData.tasks.length} tasks`
);
// Restore normal logging
disableSilentMode();
return { success: true, data: resultData };
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Core listTasks function failed: ${error.message}`);
return {
success: false,
error: {
code: 'LIST_TASKS_CORE_ERROR',
message: error.message || 'Failed to list tasks'
}
};
}
};
try {
const result = await coreListTasksAction();
log.info('listTasksDirect completed');
return result;
} catch (error) {
log.error(`Unexpected error during listTasks: ${error.message}`);
console.error(error.stack);
return {
success: false,
error: {
code: 'UNEXPECTED_ERROR',
message: error.message
}
};
}
}

View File

@@ -1,164 +0,0 @@
/**
* show-task.js
* Direct function implementation for showing task details
*/
import {
findTaskById,
readComplexityReport,
readJSON
} from '../../../../scripts/modules/utils.js';
import { findTasksPath } from '../utils/path-utils.js';
/**
* Direct function wrapper for getting task details.
*
* @param {Object} args - Command arguments.
* @param {string} args.id - Task ID to show.
* @param {string} [args.file] - Optional path to the tasks file (passed to findTasksPath).
* @param {string} args.reportPath - Explicit path to the complexity report file.
* @param {string} [args.status] - Optional status to filter subtasks by.
* @param {string} args.projectRoot - Absolute path to the project root directory (already normalized by tool).
* @param {string} [args.tag] - Tag for the task
* @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function showTaskDirect(args, log) {
// This function doesn't need session context since it only reads data
// Destructure projectRoot and other args. projectRoot is assumed normalized.
const { id, file, reportPath, status, projectRoot, tag } = args;
log.info(
`Showing task direct function. ID: ${id}, File: ${file}, Status Filter: ${status}, ProjectRoot: ${projectRoot}`
);
// --- Path Resolution using the passed (already normalized) projectRoot ---
let tasksJsonPath;
try {
// Use the projectRoot passed directly from args
tasksJsonPath = findTasksPath(
{ projectRoot: projectRoot, file: file },
log
);
log.info(`Resolved tasks path: ${tasksJsonPath}`);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return {
success: false,
error: {
code: 'TASKS_FILE_NOT_FOUND',
message: `Failed to find tasks.json: ${error.message}`
}
};
}
// --- End Path Resolution ---
// --- Rest of the function remains the same, using tasksJsonPath ---
try {
const tasksData = readJSON(tasksJsonPath, projectRoot, tag);
if (!tasksData || !tasksData.tasks) {
return {
success: false,
error: { code: 'INVALID_TASKS_DATA', message: 'Invalid tasks data' }
};
}
const complexityReport = readComplexityReport(reportPath);
// Parse comma-separated IDs
const taskIds = id
.split(',')
.map((taskId) => taskId.trim())
.filter((taskId) => taskId.length > 0);
if (taskIds.length === 0) {
return {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: 'No valid task IDs provided'
}
};
}
// Handle single task ID (existing behavior)
if (taskIds.length === 1) {
const { task, originalSubtaskCount } = findTaskById(
tasksData.tasks,
taskIds[0],
complexityReport,
status
);
if (!task) {
return {
success: false,
error: {
code: 'TASK_NOT_FOUND',
message: `Task or subtask with ID ${taskIds[0]} not found`
}
};
}
log.info(`Successfully retrieved task ${taskIds[0]}.`);
const returnData = { ...task };
if (originalSubtaskCount !== null) {
returnData._originalSubtaskCount = originalSubtaskCount;
returnData._subtaskFilter = status;
}
return { success: true, data: returnData };
}
// Handle multiple task IDs
const foundTasks = [];
const notFoundIds = [];
taskIds.forEach((taskId) => {
const { task, originalSubtaskCount } = findTaskById(
tasksData.tasks,
taskId,
complexityReport,
status
);
if (task) {
const taskData = { ...task };
if (originalSubtaskCount !== null) {
taskData._originalSubtaskCount = originalSubtaskCount;
taskData._subtaskFilter = status;
}
foundTasks.push(taskData);
} else {
notFoundIds.push(taskId);
}
});
log.info(
`Successfully retrieved ${foundTasks.length} of ${taskIds.length} requested tasks.`
);
// Return multiple tasks with metadata
return {
success: true,
data: {
tasks: foundTasks,
requestedIds: taskIds,
foundCount: foundTasks.length,
notFoundIds: notFoundIds,
isMultiple: true
}
};
} catch (error) {
log.error(`Error showing task ${id}: ${error.message}`);
return {
success: false,
error: {
code: 'TASK_OPERATION_ERROR',
message: error.message
}
};
}
}

View File

@@ -5,7 +5,6 @@
*/
// Import direct function implementations
import { listTasksDirect } from './direct-functions/list-tasks.js';
import { getCacheStatsDirect } from './direct-functions/cache-stats.js';
import { parsePRDDirect } from './direct-functions/parse-prd.js';
import { updateTasksDirect } from './direct-functions/update-tasks.js';
@@ -13,7 +12,6 @@ import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js';
import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js';
import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js';
import { setTaskStatusDirect } from './direct-functions/set-task-status.js';
import { showTaskDirect } from './direct-functions/show-task.js';
import { nextTaskDirect } from './direct-functions/next-task.js';
import { expandTaskDirect } from './direct-functions/expand-task.js';
import { addTaskDirect } from './direct-functions/add-task.js';
@@ -47,7 +45,6 @@ export { findTasksPath } from './utils/path-utils.js';
// Use Map for potential future enhancements like introspection or dynamic dispatch
export const directFunctions = new Map([
['listTasksDirect', listTasksDirect],
['getCacheStatsDirect', getCacheStatsDirect],
['parsePRDDirect', parsePRDDirect],
['updateTasksDirect', updateTasksDirect],
@@ -55,7 +52,6 @@ export const directFunctions = new Map([
['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
['generateTaskFilesDirect', generateTaskFilesDirect],
['setTaskStatusDirect', setTaskStatusDirect],
['showTaskDirect', showTaskDirect],
['nextTaskDirect', nextTaskDirect],
['expandTaskDirect', expandTaskDirect],
['addTaskDirect', addTaskDirect],
@@ -87,7 +83,6 @@ export const directFunctions = new Map([
// Re-export all direct function implementations
export {
listTasksDirect,
getCacheStatsDirect,
parsePRDDirect,
updateTasksDirect,
@@ -95,7 +90,6 @@ export {
updateSubtaskByIdDirect,
generateTaskFilesDirect,
setTaskStatusDirect,
showTaskDirect,
nextTaskDirect,
expandTaskDirect,
addTaskDirect,

View File

@@ -1,152 +0,0 @@
/**
* tools/get-task.js
* Tool to get task details by ID
*/
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
import { z } from 'zod/v3';
import {
handleApiResult,
createErrorResponse,
withNormalizedProjectRoot
} from './utils.js';
import { showTaskDirect } from '../core/task-master-core.js';
import {
findTasksPath,
findComplexityReportPath
} from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Custom processor function that removes allTasks from the response
* @param {Object} data - The data returned from showTaskDirect
* @returns {Object} - The processed data with allTasks removed
*/
function processTaskResponse(data) {
if (!data) return data;
// If we have the expected structure with task and allTasks
if (typeof data === 'object' && data !== null && data.id && data.title) {
// If the data itself looks like the task object, return it
return data;
} else if (data.task) {
return data.task;
}
// If structure is unexpected, return as is
return data;
}
/**
* Register the get-task tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerShowTaskTool(server) {
server.addTool({
name: 'get_task',
description: 'Get detailed information about a specific task',
parameters: z.object({
id: z
.string()
.describe(
'Task ID(s) to get (can be comma-separated for multiple tasks)'
),
status: z
.string()
.optional()
.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
file: z
.string()
.optional()
.describe('Path to the tasks file relative to project root'),
complexityReport: z
.string()
.optional()
.describe(
'Path to the complexity report file (relative to project root or absolute)'
),
projectRoot: z
.string()
.describe(
'Absolute path to the project root directory (Optional, usually from session)'
),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
const { id, file, status, projectRoot } = args;
try {
log.info(
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
// Resolve the path to tasks.json using the NORMALIZED projectRoot from args
let tasksJsonPath;
try {
tasksJsonPath = findTasksPath(
{ projectRoot: projectRoot, file: file },
log
);
log.info(`Resolved tasks path: ${tasksJsonPath}`);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Call the direct function, passing the normalized projectRoot
// Resolve the path to complexity report
let complexityReportPath;
try {
complexityReportPath = findComplexityReportPath(
{
projectRoot: projectRoot,
complexityReport: args.complexityReport,
tag: resolvedTag
},
log
);
} catch (error) {
log.error(`Error finding complexity report: ${error.message}`);
}
const result = await showTaskDirect(
{
tasksJsonPath: tasksJsonPath,
reportPath: complexityReportPath,
// Pass other relevant args
id: id,
status: status,
projectRoot: projectRoot,
tag: resolvedTag
},
log,
{ session }
);
if (result.success) {
log.info(`Successfully retrieved task details for ID: ${args.id}`);
} else {
log.error(`Failed to get task: ${result.error.message}`);
}
// Use our custom processor function
return handleApiResult(
result,
log,
'Error retrieving task details',
processTaskResponse,
projectRoot
);
} catch (error) {
log.error(`Error in get-task tool: ${error.message}\n${error.stack}`);
return createErrorResponse(`Failed to get task: ${error.message}`);
}
})
});
}

View File

@@ -1,124 +0,0 @@
/**
* tools/get-tasks.js
* Tool to get all tasks from Task Master
*/
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
import { z } from 'zod/v3';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { listTasksDirect } from '../core/task-master-core.js';
import {
resolveTasksPath,
resolveComplexityReportPath
} from '../core/utils/path-utils.js';
import { resolveTag } from '../../../scripts/modules/utils.js';
/**
* Register the getTasks tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerListTasksTool(server) {
server.addTool({
name: 'get_tasks',
description:
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
parameters: z.object({
status: z
.string()
.optional()
.describe(
"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
),
withSubtasks: z
.boolean()
.optional()
.describe(
'Include subtasks nested within their parent tasks in the response'
),
file: z
.string()
.optional()
.describe(
'Path to the tasks file (relative to project root or absolute)'
),
complexityReport: z
.string()
.optional()
.describe(
'Path to the complexity report file (relative to project root or absolute)'
),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.'),
tag: z.string().optional().describe('Tag context to operate on')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
const resolvedTag = resolveTag({
projectRoot: args.projectRoot,
tag: args.tag
});
// Resolve the path to tasks.json using new path utilities
let tasksJsonPath;
try {
tasksJsonPath = resolveTasksPath(args, log);
} catch (error) {
log.error(`Error finding tasks.json: ${error.message}`);
return createErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
// Resolve the path to complexity report
let complexityReportPath;
try {
complexityReportPath = resolveComplexityReportPath(
{ ...args, tag: resolvedTag },
session
);
} catch (error) {
log.error(`Error finding complexity report: ${error.message}`);
// This is optional, so we don't fail the operation
complexityReportPath = null;
}
const result = await listTasksDirect(
{
tasksJsonPath: tasksJsonPath,
status: args.status,
withSubtasks: args.withSubtasks,
reportPath: complexityReportPath,
projectRoot: args.projectRoot,
tag: resolvedTag
},
log,
{ session }
);
log.info(
`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
);
return handleApiResult(
result,
log,
'Error getting tasks',
undefined,
args.projectRoot
);
} catch (error) {
log.error(`Error getting tasks: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}
// We no longer need the formatTasksResponse function as we're returning raw JSON data

View File

@@ -1,16 +1,14 @@
/**
* tool-registry.js
* Tool Registry Object Structure - Maps all 36 tool names to registration functions
* Tool Registry - Maps tool names to registration functions
*/
import { registerListTasksTool } from './get-tasks.js';
import { registerSetTaskStatusTool } from './set-task-status.js';
import { registerParsePRDTool } from './parse-prd.js';
import { registerUpdateTool } from './update.js';
import { registerUpdateTaskTool } from './update-task.js';
import { registerUpdateSubtaskTool } from './update-subtask.js';
import { registerGenerateTool } from './generate.js';
import { registerShowTaskTool } from './get-task.js';
import { registerNextTaskTool } from './next-task.js';
import { registerExpandTaskTool } from './expand-task.js';
import { registerAddTaskTool } from './add-task.js';
@@ -40,7 +38,7 @@ import { registerRulesTool } from './rules.js';
import { registerScopeUpTool } from './scope-up.js';
import { registerScopeDownTool } from './scope-down.js';
// Import TypeScript autopilot tools from apps/mcp
// Import TypeScript tools from apps/mcp
import {
registerAutopilotStartTool,
registerAutopilotResumeTool,
@@ -49,7 +47,9 @@ import {
registerAutopilotCompleteTool,
registerAutopilotCommitTool,
registerAutopilotFinalizeTool,
registerAutopilotAbortTool
registerAutopilotAbortTool,
registerGetTasksTool,
registerGetTaskTool
} from '@tm/mcp';
/**
@@ -67,8 +67,8 @@ export const toolRegistry = {
expand_all: registerExpandAllTool,
scope_up_task: registerScopeUpTool,
scope_down_task: registerScopeDownTool,
get_tasks: registerListTasksTool,
get_task: registerShowTaskTool,
get_tasks: registerGetTasksTool,
get_task: registerGetTaskTool,
next_task: registerNextTaskTool,
complexity_report: registerComplexityReportTool,
set_task_status: registerSetTaskStatusTool,