refactor(mcp): Modularize direct functions in MCP server

Split monolithic task-master-core.js into separate function files within
the mcp-server/src/core/direct-functions/ directory. This change:

- Creates individual files for each direct function implementation
- Moves findTasksJsonPath to a dedicated utils/path-utils.js file
- Converts task-master-core.js to be a simple import/export hub
- Improves maintainability and organization of the codebase
- Reduces potential merge conflicts when multiple developers contribute
- Follows standard module separation patterns

Each function is now in its own self-contained file with clear imports and
focused responsibility, while maintaining the same API endpoints.
This commit is contained in:
Eyal Toledano
2025-03-31 01:52:34 -04:00
parent 450549d875
commit 71410629ba
12 changed files with 873 additions and 377 deletions

View File

@@ -1,353 +1,29 @@
/**
* task-master-core.js
* Direct function imports from Task Master modules
*
* This module provides direct access to Task Master core functions
* for improved performance and error handling compared to CLI execution.
* Central module that imports and re-exports all direct function implementations
* for improved organization and maintainability.
*/
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import fs from 'fs';
// 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';
// Get the current module's directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Re-export utility functions
export { findTasksJsonPath } from './utils/path-utils.js';
// Import Task Master modules
import {
listTasks,
parsePRD,
updateTasks,
// We'll import more functions as we continue implementation
} from '../../../scripts/modules/task-manager.js';
// Import context manager
import { contextManager } from './context-manager.js';
import { getCachedOrExecute } from '../tools/utils.js'; // Import the utility here
/**
* Finds the absolute path to the tasks.json file based on project root and arguments.
* @param {Object} args - Command arguments, potentially including 'projectRoot' and 'file'.
* @param {Object} log - Logger object.
* @returns {string} - Absolute path to the tasks.json file.
* @throws {Error} - If tasks.json cannot be found.
*/
function findTasksJsonPath(args, log) {
// Assume projectRoot is already normalized absolute path if passed in args
// Or use getProjectRoot if we decide to centralize that logic
const projectRoot = args.projectRoot || process.cwd();
log.info(`Searching for tasks.json within project root: ${projectRoot}`);
const possiblePaths = [];
// 1. If a file is explicitly provided relative to projectRoot
if (args.file) {
possiblePaths.push(path.resolve(projectRoot, args.file));
}
// 2. Check the standard locations relative to projectRoot
possiblePaths.push(
path.join(projectRoot, 'tasks.json'),
path.join(projectRoot, 'tasks', 'tasks.json')
);
log.info(`Checking potential task file paths: ${possiblePaths.join(', ')}`);
// Find the first existing path
for (const p of possiblePaths) {
if (fs.existsSync(p)) {
log.info(`Found tasks file at: ${p}`);
return p;
}
}
// If no file was found, throw an error
const error = new Error(`Tasks file not found in any of the expected locations relative to ${projectRoot}: ${possiblePaths.join(', ')}`);
error.code = 'TASKS_FILE_NOT_FOUND';
throw error;
}
/**
* Direct function wrapper for listTasks with error handling and caching.
*
* @param {Object} args - Command arguments (projectRoot is expected to be resolved).
* @param {Object} log - Logger object.
* @returns {Promise<Object>} - Task list result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: boolean }.
*/
export async function listTasksDirect(args, log) {
let tasksPath;
try {
// Find the tasks path first - needed for cache key and execution
tasksPath = findTasksJsonPath(args, log);
} catch (error) {
if (error.code === 'TASKS_FILE_NOT_FOUND') {
log.error(`Tasks file not found: ${error.message}`);
// Return the error structure expected by the calling tool/handler
return { success: false, error: { code: error.code, message: error.message }, fromCache: false };
}
log.error(`Unexpected error finding tasks file: ${error.message}`);
// Re-throw for outer catch or return structured error
return { success: false, error: { code: 'FIND_TASKS_PATH_ERROR', message: error.message }, fromCache: false };
}
// Generate cache key *after* finding tasksPath
const statusFilter = args.status || 'all';
const withSubtasks = args.withSubtasks || false;
const cacheKey = `listTasks:${tasksPath}:${statusFilter}:${withSubtasks}`;
// Define the action function to be executed on cache miss
const coreListTasksAction = async () => {
try {
log.info(`Executing core listTasks function for path: ${tasksPath}, filter: ${statusFilter}, subtasks: ${withSubtasks}`);
const resultData = listTasks(tasksPath, statusFilter, withSubtasks, 'json');
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`);
return { success: true, data: resultData };
} catch (error) {
log.error(`Core listTasks function failed: ${error.message}`);
return { success: false, error: { code: 'LIST_TASKS_CORE_ERROR', message: error.message || 'Failed to list tasks' } };
}
};
// Use the caching utility
try {
const result = await getCachedOrExecute({
cacheKey,
actionFn: coreListTasksAction,
log
});
log.info(`listTasksDirect completed. From cache: ${result.fromCache}`);
return result; // Returns { success, data/error, fromCache }
} catch(error) {
// Catch unexpected errors from getCachedOrExecute itself (though unlikely)
log.error(`Unexpected error during getCachedOrExecute for listTasks: ${error.message}`);
console.error(error.stack);
return { success: false, error: { code: 'CACHE_UTIL_ERROR', message: error.message }, fromCache: false };
}
}
/**
* Get cache statistics for monitoring
* @param {Object} args - Command arguments
* @param {Object} log - Logger object
* @returns {Object} - Cache statistics
*/
export async function getCacheStatsDirect(args, log) {
try {
log.info('Retrieving cache statistics');
const stats = contextManager.getStats();
return {
success: true,
data: stats
};
} catch (error) {
log.error(`Error getting cache stats: ${error.message}`);
return {
success: false,
error: {
code: 'CACHE_STATS_ERROR',
message: error.message || 'Unknown error occurred'
}
};
}
}
/**
* Direct function wrapper for parsing PRD documents and generating tasks.
*
* @param {Object} args - Command arguments containing input, numTasks or tasks, and output options.
* @param {Object} log - Logger object.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function parsePRDDirect(args, log) {
try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
// Check required parameters
if (!args.input) {
const errorMessage = 'No input file specified. Please provide an input PRD document path.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_INPUT_FILE', message: errorMessage },
fromCache: false
};
}
// Resolve input path (relative to project root if provided)
const projectRoot = args.projectRoot || process.cwd();
const inputPath = path.isAbsolute(args.input) ? args.input : path.resolve(projectRoot, args.input);
// Determine output path
let outputPath;
if (args.output) {
outputPath = path.isAbsolute(args.output) ? args.output : path.resolve(projectRoot, args.output);
} else {
// Default to tasks/tasks.json in the project root
outputPath = path.resolve(projectRoot, 'tasks', 'tasks.json');
}
// Verify input file exists
if (!fs.existsSync(inputPath)) {
const errorMessage = `Input file not found: ${inputPath}`;
log.error(errorMessage);
return {
success: false,
error: { code: 'INPUT_FILE_NOT_FOUND', message: errorMessage },
fromCache: false
};
}
// Parse number of tasks - handle both string and number values
let numTasks = 10; // Default
if (args.numTasks) {
numTasks = typeof args.numTasks === 'string' ? parseInt(args.numTasks, 10) : args.numTasks;
if (isNaN(numTasks)) {
numTasks = 10; // Fallback to default if parsing fails
log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`);
}
}
log.info(`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`);
// Execute core parsePRD function (which is not async but we'll await it to maintain consistency)
await parsePRD(inputPath, outputPath, numTasks);
// Since parsePRD doesn't return a value but writes to a file, we'll read the result
// to return it to the caller
if (fs.existsSync(outputPath)) {
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
log.info(`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks`);
return {
success: true,
data: {
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
taskCount: tasksData.tasks?.length || 0,
outputPath
},
fromCache: false // This operation always modifies state and should never be cached
};
} else {
const errorMessage = `Tasks file was not created at ${outputPath}`;
log.error(errorMessage);
return {
success: false,
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
fromCache: false
};
}
} catch (error) {
log.error(`Error parsing PRD: ${error.message}`);
return {
success: false,
error: { code: 'PARSE_PRD_ERROR', message: error.message || 'Unknown error parsing PRD' },
fromCache: false
};
}
}
/**
* Direct function wrapper for updating tasks based on new context/prompt.
*
* @param {Object} args - Command arguments containing fromId, prompt, useResearch and file path options.
* @param {Object} log - Logger object.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function updateTasksDirect(args, log) {
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
// Check required parameters
if (!args.from) {
const errorMessage = 'No from ID specified. Please provide a task ID to start updating from.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_FROM_ID', message: errorMessage },
fromCache: false
};
}
if (!args.prompt) {
const errorMessage = 'No prompt specified. Please provide a prompt with new context for task updates.';
log.error(errorMessage);
return {
success: false,
error: { code: 'MISSING_PROMPT', message: errorMessage },
fromCache: false
};
}
// Parse fromId - handle both string and number values
let fromId;
if (typeof args.from === 'string') {
fromId = parseInt(args.from, 10);
if (isNaN(fromId)) {
const errorMessage = `Invalid from ID: ${args.from}. Task ID must be a positive integer.`;
log.error(errorMessage);
return {
success: false,
error: { code: 'INVALID_FROM_ID', message: errorMessage },
fromCache: false
};
}
} else {
fromId = args.from;
}
// Get tasks file path
let tasksPath;
try {
tasksPath = findTasksJsonPath(args, log);
} catch (error) {
log.error(`Error finding tasks file: ${error.message}`);
return {
success: false,
error: { code: 'TASKS_FILE_ERROR', message: error.message },
fromCache: false
};
}
// Get research flag
const useResearch = args.research === true;
log.info(`Updating tasks from ID ${fromId} with prompt "${args.prompt}" and research: ${useResearch}`);
// Execute core updateTasks function
await updateTasks(tasksPath, fromId, args.prompt, useResearch);
// Since updateTasks doesn't return a value but modifies the tasks file,
// we'll return a success message
return {
success: true,
data: {
message: `Successfully updated tasks from ID ${fromId} based on the prompt`,
fromId,
tasksPath,
useResearch
},
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
log.error(`Error updating tasks: ${error.message}`);
return {
success: false,
error: { code: 'UPDATE_TASKS_ERROR', message: error.message || 'Unknown error updating tasks' },
fromCache: false
};
}
}
// Re-export all direct functions
export {
listTasksDirect,
getCacheStatsDirect,
parsePRDDirect,
updateTasksDirect,
};
/**
* Maps Task Master functions to their direct implementation
* This map is used by tools to look up the appropriate function by name
*/
export const directFunctions = {
list: listTasksDirect,