Skip to main content
Glama

expand_task

Break down complex tasks into manageable subtasks for detailed implementation using the Task Master MCP server. Specify task ID, project root, and optional parameters like subtask count or research context to streamline task execution.

Instructions

Expand a task into subtasks for detailed implementation

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
fileNoPath to the tasks file relative to project root (e.g., tasks/tasks.json)
forceNoForce expansion even if subtasks exist
idYesID of task to expand
numNoNumber of subtasks to generate
projectRootYesThe directory of the project. Must be an absolute path.
promptNoAdditional context for subtask generation
researchNoUse research role for generation
tagNoTag context to operate on

Implementation Reference

  • Registers the MCP tool 'expand_task' with input schema, description, and execute handler that normalizes project root, finds tasks path, and calls expandTaskDirect.
    export function registerExpandTaskTool(server) {
    	server.addTool({
    		name: 'expand_task',
    		description: 'Expand a task into subtasks for detailed implementation',
    		parameters: z.object({
    			id: z.string().describe('ID of task to expand'),
    			num: z.string().optional().describe('Number of subtasks to generate'),
    			research: z
    				.boolean()
    				.optional()
    				.default(false)
    				.describe('Use research role for generation'),
    			prompt: z
    				.string()
    				.optional()
    				.describe('Additional context for subtask generation'),
    			file: z
    				.string()
    				.optional()
    				.describe(
    					'Path to the tasks file relative to project root (e.g., tasks/tasks.json)'
    				),
    			projectRoot: z
    				.string()
    				.describe('The directory of the project. Must be an absolute path.'),
    			force: z
    				.boolean()
    				.optional()
    				.default(false)
    				.describe('Force expansion even if subtasks exist'),
    			tag: z.string().optional().describe('Tag context to operate on')
    		}),
    		execute: withNormalizedProjectRoot(async (args, { log, session }) => {
    			try {
    				log.info(`Starting expand-task with args: ${JSON.stringify(args)}`);
    				const resolvedTag = resolveTag({
    					projectRoot: args.projectRoot,
    					tag: args.tag
    				});
    				// 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}`
    					);
    				}
    
    				const complexityReportPath = findComplexityReportPath(
    					{ ...args, tag: resolvedTag },
    					log
    				);
    
    				const result = await expandTaskDirect(
    					{
    						tasksJsonPath: tasksJsonPath,
    						id: args.id,
    						num: args.num,
    						research: args.research,
    						prompt: args.prompt,
    						force: args.force,
    						complexityReportPath,
    						projectRoot: args.projectRoot,
    						tag: resolvedTag
    					},
    					log,
    					{ session }
    				);
    
    				return handleApiResult({
    					result,
    					log: log,
    					errorPrefix: 'Error expanding task',
    					projectRoot: args.projectRoot
    				});
    			} catch (error) {
    				log.error(`Error in expand-task tool: ${error.message}`);
    				return createErrorResponse(error.message);
    			}
    		})
    	});
    }
  • Direct handler function expandTaskDirect called by the tool execute; handles MCP session, file I/O, task validation, and delegates to core expandTask implementation.
    export async function expandTaskDirect(args, log, context = {}) {
    	const { session } = context; // Extract session
    	// Destructure expected args, including projectRoot
    	const {
    		tasksJsonPath,
    		id,
    		num,
    		research,
    		prompt,
    		force,
    		projectRoot,
    		tag,
    		complexityReportPath
    	} = args;
    
    	// Log session root data for debugging
    	log.info(
    		`Session data in expandTaskDirect: ${JSON.stringify({
    			hasSession: !!session,
    			sessionKeys: session ? Object.keys(session) : [],
    			roots: session?.roots,
    			rootsStr: JSON.stringify(session?.roots)
    		})}`
    	);
    
    	// Check if tasksJsonPath was provided
    	if (!tasksJsonPath) {
    		log.error('expandTaskDirect called without tasksJsonPath');
    		return {
    			success: false,
    			error: {
    				code: 'MISSING_ARGUMENT',
    				message: 'tasksJsonPath is required'
    			}
    		};
    	}
    
    	// Use provided path
    	const tasksPath = tasksJsonPath;
    
    	log.info(`[expandTaskDirect] Using tasksPath: ${tasksPath}`);
    
    	// Validate task ID
    	const taskId = id ? parseInt(id, 10) : null;
    	if (!taskId) {
    		log.error('Task ID is required');
    		return {
    			success: false,
    			error: {
    				code: 'INPUT_VALIDATION_ERROR',
    				message: 'Task ID is required'
    			}
    		};
    	}
    
    	// Process other parameters
    	const numSubtasks = num ? parseInt(num, 10) : undefined;
    	const useResearch = research === true;
    	const additionalContext = prompt || '';
    	const forceFlag = force === true;
    
    	try {
    		log.info(
    			`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${forceFlag}`
    		);
    
    		// Read tasks data
    		log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
    		const data = readJSON(tasksPath, projectRoot);
    		log.info(
    			`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`
    		);
    
    		if (!data || !data.tasks) {
    			log.error(
    				`[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}`
    			);
    			return {
    				success: false,
    				error: {
    					code: 'INVALID_TASKS_FILE',
    					message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}`
    				}
    			};
    		}
    
    		// Find the specific task
    		log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`);
    		const task = data.tasks.find((t) => t.id === taskId);
    		log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`);
    
    		if (!task) {
    			return {
    				success: false,
    				error: {
    					code: 'TASK_NOT_FOUND',
    					message: `Task with ID ${taskId} not found`
    				}
    			};
    		}
    
    		// Check if task is completed
    		if (task.status === 'done' || task.status === 'completed') {
    			return {
    				success: false,
    				error: {
    					code: 'TASK_COMPLETED',
    					message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded`
    				}
    			};
    		}
    
    		// Check for existing subtasks and force flag
    		const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0;
    		if (hasExistingSubtasks && !forceFlag) {
    			log.info(
    				`Task ${taskId} already has ${task.subtasks.length} subtasks. Use --force to overwrite.`
    			);
    			return {
    				success: true,
    				data: {
    					message: `Task ${taskId} already has subtasks. Expansion skipped.`,
    					task,
    					subtasksAdded: 0,
    					hasExistingSubtasks
    				}
    			};
    		}
    
    		// If force flag is set, clear existing subtasks
    		if (hasExistingSubtasks && forceFlag) {
    			log.info(
    				`Force flag set. Clearing existing subtasks for task ${taskId}.`
    			);
    			task.subtasks = [];
    		}
    
    		// Keep a copy of the task before modification
    		const originalTask = JSON.parse(JSON.stringify(task));
    
    		// Tracking subtasks count before expansion
    		const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0;
    
    		// Directly modify the data instead of calling the CLI function
    		if (!task.subtasks) {
    			task.subtasks = [];
    		}
    
    		// Save tasks.json with potentially empty subtasks array and proper context
    		writeJSON(tasksPath, data, projectRoot, tag);
    
    		// Create logger wrapper using the utility
    		const mcpLog = createLogWrapper(log);
    
    		let wasSilent; // Declare wasSilent outside the try block
    		// Process the request
    		try {
    			// Enable silent mode to prevent console logs from interfering with JSON response
    			wasSilent = isSilentMode(); // Assign inside the try block
    			if (!wasSilent) enableSilentMode();
    
    			// Call the core expandTask function with the wrapped logger and projectRoot
    			const coreResult = await expandTask(
    				tasksPath,
    				taskId,
    				numSubtasks,
    				useResearch,
    				additionalContext,
    				{
    					complexityReportPath,
    					mcpLog,
    					session,
    					projectRoot,
    					commandName: 'expand-task',
    					outputType: 'mcp',
    					tag
    				},
    				forceFlag
    			);
    
    			// Restore normal logging
    			if (!wasSilent && isSilentMode()) disableSilentMode();
    
    			// Read the updated data
    			const updatedData = readJSON(tasksPath, projectRoot);
    			const updatedTask = updatedData.tasks.find((t) => t.id === taskId);
    
    			// Calculate how many subtasks were added
    			const subtasksAdded = updatedTask.subtasks
    				? updatedTask.subtasks.length - subtasksCountBefore
    				: 0;
    
    			// Return the result, including telemetryData
    			log.info(
    				`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`
    			);
    			return {
    				success: true,
    				data: {
    					task: coreResult.task,
    					subtasksAdded,
    					hasExistingSubtasks,
    					telemetryData: coreResult.telemetryData,
    					tagInfo: coreResult.tagInfo
    				}
    			};
    		} catch (error) {
    			// Make sure to restore normal logging even if there's an error
    			if (!wasSilent && isSilentMode()) disableSilentMode();
    
    			log.error(`Error expanding task: ${error.message}`);
    			return {
    				success: false,
    				error: {
    					code: 'CORE_FUNCTION_ERROR',
    					message: error.message || 'Failed to expand task'
    				}
    			};
    		}
    	} catch (error) {
    		log.error(`Error expanding task: ${error.message}`);
    		return {
    			success: false,
    			error: {
    				code: 'CORE_FUNCTION_ERROR',
    				message: error.message || 'Failed to expand task'
    			}
    		};
    	}
    }
  • Core implementation of task expansion: context gathering, AI-powered subtask generation using generateObjectService, complexity integration, file persistence.
    async function expandTask(
    	tasksPath,
    	taskId,
    	numSubtasks,
    	useResearch = false,
    	additionalContext = '',
    	context = {},
    	force = false
    ) {
    	const {
    		session,
    		mcpLog,
    		projectRoot: contextProjectRoot,
    		tag,
    		complexityReportPath
    	} = context;
    	const outputFormat = mcpLog ? 'json' : 'text';
    
    	// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
    	const projectRoot = contextProjectRoot || findProjectRoot(tasksPath);
    
    	// Create unified logger and report function
    	const { logger, report, isMCP } = createBridgeLogger(mcpLog, session);
    
    	if (isMCP) {
    		logger.info(`expandTask called with context: session=${!!session}`);
    	}
    
    	try {
    		// --- BRIDGE: Try remote expansion first (API storage) ---
    		const remoteResult = await tryExpandViaRemote({
    			taskId,
    			numSubtasks,
    			useResearch,
    			additionalContext,
    			force,
    			projectRoot,
    			tag,
    			isMCP,
    			outputFormat,
    			report
    		});
    
    		// If remote handled it, return the result
    		if (remoteResult) {
    			return remoteResult;
    		}
    		// Otherwise fall through to file-based logic below
    		// --- End BRIDGE ---
    
    		// --- Task Loading/Filtering (Unchanged) ---
    		logger.info(`Reading tasks from ${tasksPath}`);
    		const data = readJSON(tasksPath, projectRoot, tag);
    		if (!data || !data.tasks)
    			throw new Error(`Invalid tasks data in ${tasksPath}`);
    		const taskIndex = data.tasks.findIndex(
    			(t) => t.id === parseInt(taskId, 10)
    		);
    		if (taskIndex === -1) throw new Error(`Task ${taskId} not found`);
    		const task = data.tasks[taskIndex];
    		logger.info(
    			`Expanding task ${taskId}: ${task.title}${useResearch ? ' with research' : ''}`
    		);
    		// --- End Task Loading/Filtering ---
    
    		// --- Handle Force Flag: Clear existing subtasks if force=true ---
    		if (force && Array.isArray(task.subtasks) && task.subtasks.length > 0) {
    			logger.info(
    				`Force flag set. Clearing existing ${task.subtasks.length} subtasks for task ${taskId}.`
    			);
    			task.subtasks = []; // Clear existing subtasks
    		}
    		// --- End Force Flag Handling ---
    
    		// --- Context Gathering ---
    		let gatheredContext = '';
    		try {
    			const contextGatherer = new ContextGatherer(projectRoot, tag);
    			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
    			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
    			const searchQuery = `${task.title} ${task.description}`;
    			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
    				maxResults: 5,
    				includeSelf: true
    			});
    			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
    
    			const finalTaskIds = [
    				...new Set([taskId.toString(), ...relevantTaskIds])
    			];
    
    			if (finalTaskIds.length > 0) {
    				const contextResult = await contextGatherer.gather({
    					tasks: finalTaskIds,
    					format: 'research'
    				});
    				gatheredContext = contextResult.context || '';
    			}
    		} catch (contextError) {
    			logger.warn(`Could not gather context: ${contextError.message}`);
    		}
    		// --- End Context Gathering ---
    
    		// --- Complexity Report Integration ---
    		let finalSubtaskCount;
    		let complexityReasoningContext = '';
    		let taskAnalysis = null;
    
    		logger.info(
    			`Looking for complexity report at: ${complexityReportPath}${tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}`
    		);
    
    		try {
    			if (fs.existsSync(complexityReportPath)) {
    				const complexityReport = readJSON(complexityReportPath);
    				taskAnalysis = complexityReport?.complexityAnalysis?.find(
    					(a) => a.taskId === task.id
    				);
    				if (taskAnalysis) {
    					logger.info(
    						`Found complexity analysis for task ${task.id}: Score ${taskAnalysis.complexityScore}`
    					);
    					if (taskAnalysis.reasoning) {
    						complexityReasoningContext = `\nComplexity Analysis Reasoning: ${taskAnalysis.reasoning}`;
    					}
    				} else {
    					logger.info(
    						`No complexity analysis found for task ${task.id} in report.`
    					);
    				}
    			} else {
    				logger.info(
    					`Complexity report not found at ${complexityReportPath}. Skipping complexity check.`
    				);
    			}
    		} catch (reportError) {
    			logger.warn(
    				`Could not read or parse complexity report: ${reportError.message}. Proceeding without it.`
    			);
    		}
    
    		// Determine final subtask count
    		const explicitNumSubtasks = parseInt(numSubtasks, 10);
    		if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks >= 0) {
    			finalSubtaskCount = explicitNumSubtasks;
    			logger.info(
    				`Using explicitly provided subtask count: ${finalSubtaskCount}`
    			);
    		} else if (taskAnalysis?.recommendedSubtasks) {
    			finalSubtaskCount = parseInt(taskAnalysis.recommendedSubtasks, 10);
    			logger.info(
    				`Using subtask count from complexity report: ${finalSubtaskCount}`
    			);
    		} else {
    			finalSubtaskCount = getDefaultSubtasks(session);
    			logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
    		}
    		if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount < 0) {
    			logger.warn(
    				`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
    			);
    			finalSubtaskCount = 3;
    		}
    
    		// Determine prompt content AND system prompt
    		// Calculate the next subtask ID to match current behavior:
    		// - Start from the number of existing subtasks + 1
    		// - This creates sequential IDs: 1, 2, 3, 4...
    		// - Display format shows as parentTaskId.subtaskId (e.g., "1.1", "1.2", "2.1")
    		const nextSubtaskId = (task.subtasks?.length || 0) + 1;
    
    		// Load prompts using PromptManager
    		const promptManager = getPromptManager();
    
    		// Check if a codebase analysis provider is being used
    		const hasCodebaseAnalysisCapability = hasCodebaseAnalysis(
    			useResearch,
    			projectRoot,
    			session
    		);
    
    		// Combine all context sources into a single additionalContext parameter
    		let combinedAdditionalContext = '';
    		if (additionalContext || complexityReasoningContext) {
    			combinedAdditionalContext =
    				`\n\n${additionalContext}${complexityReasoningContext}`.trim();
    		}
    		if (gatheredContext) {
    			combinedAdditionalContext =
    				`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
    		}
    
    		// Ensure expansionPrompt is a string (handle both string and object formats)
    		let expansionPromptText = undefined;
    		if (taskAnalysis?.expansionPrompt) {
    			if (typeof taskAnalysis.expansionPrompt === 'string') {
    				expansionPromptText = taskAnalysis.expansionPrompt;
    			} else if (
    				typeof taskAnalysis.expansionPrompt === 'object' &&
    				taskAnalysis.expansionPrompt.text
    			) {
    				expansionPromptText = taskAnalysis.expansionPrompt.text;
    			}
    		}
    
    		// Ensure gatheredContext is a string (handle both string and object formats)
    		let gatheredContextText = gatheredContext;
    		if (typeof gatheredContext === 'object' && gatheredContext !== null) {
    			if (gatheredContext.data) {
    				gatheredContextText = gatheredContext.data;
    			} else if (gatheredContext.text) {
    				gatheredContextText = gatheredContext.text;
    			} else {
    				gatheredContextText = JSON.stringify(gatheredContext);
    			}
    		}
    
    		const promptParams = {
    			task: task,
    			subtaskCount: finalSubtaskCount,
    			nextSubtaskId: nextSubtaskId,
    			additionalContext: additionalContext,
    			complexityReasoningContext: complexityReasoningContext,
    			gatheredContext: gatheredContextText || '',
    			useResearch: useResearch,
    			expansionPrompt: expansionPromptText || undefined,
    			hasCodebaseAnalysis: hasCodebaseAnalysisCapability,
    			projectRoot: projectRoot || ''
    		};
    		let variantKey = 'default';
    		if (expansionPromptText) {
    			variantKey = 'complexity-report';
    			logger.info(
    				`Using expansion prompt from complexity report for task ${task.id}.`
    			);
    		} else if (useResearch) {
    			variantKey = 'research';
    			logger.info(`Using research variant for task ${task.id}.`);
    		} else {
    			logger.info(`Using standard prompt generation for task ${task.id}.`);
    		}
    
    		const { systemPrompt, userPrompt: promptContent } =
    			promptManager.loadPrompt('expand-task', promptParams, variantKey);
    
    		// Debug logging to identify the issue
    		logger.debug(`Selected variant: ${variantKey}`);
    		logger.debug(
    			`Prompt params passed: ${JSON.stringify(promptParams, null, 2)}`
    		);
    		logger.debug(
    			`System prompt (first 500 chars): ${systemPrompt.substring(0, 500)}...`
    		);
    		logger.debug(
    			`User prompt (first 500 chars): ${promptContent.substring(0, 500)}...`
    		);
    		// --- End Complexity Report / Prompt Logic ---
    
    		// --- AI Subtask Generation using generateObjectService ---
    		let generatedSubtasks = [];
    		let loadingIndicator = null;
    		if (outputFormat === 'text') {
    			loadingIndicator = startLoadingIndicator(
    				`Generating ${finalSubtaskCount || 'appropriate number of'} subtasks...\n`
    			);
    		}
    
    		let aiServiceResponse = null;
    		try {
    			const role = useResearch ? 'research' : 'main';
    
    			// Call generateObjectService with the determined prompts and telemetry params
    			aiServiceResponse = await generateObjectService({
    				prompt: promptContent,
    				systemPrompt: systemPrompt,
    				role,
    				session,
    				projectRoot,
    				schema: COMMAND_SCHEMAS['expand-task'],
    				objectName: 'subtasks',
    				commandName: 'expand-task',
    				outputType: outputFormat
    			});
    
    			// With generateObject, we expect structured data – verify it before use
    			const mainResult = aiServiceResponse?.mainResult;
    			if (!mainResult || !Array.isArray(mainResult.subtasks)) {
    				throw new Error('AI response did not include a valid subtasks array.');
    			}
    			generatedSubtasks = mainResult.subtasks;
    			logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
    		} catch (error) {
    			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
    			logger.error(
    				`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
    				'error'
    			);
    			throw error;
    		} finally {
    			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
    		}
    
    		// --- Task Update & File Writing ---
    		// Ensure task.subtasks is an array before appending
    		if (!Array.isArray(task.subtasks)) {
    			task.subtasks = [];
    		}
    		// Append the newly generated and validated subtasks
    		task.subtasks.push(...generatedSubtasks);
    		// --- End Change: Append instead of replace ---
    
    		data.tasks[taskIndex] = task; // Assign the modified task back
    		writeJSON(tasksPath, data, projectRoot, tag);
    		// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
    
    		// Display AI Usage Summary for CLI
    		if (
    			outputFormat === 'text' &&
    			aiServiceResponse &&
    			aiServiceResponse.telemetryData
    		) {
    			displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
    		}
    
    		// Return the updated task object AND telemetry data
    		return {
    			task,
    			telemetryData: aiServiceResponse?.telemetryData,
    			tagInfo: aiServiceResponse?.tagInfo
    		};
    	} catch (error) {
    		// Catches errors from file reading, parsing, AI call etc.
    		logger.error(`Error expanding task ${taskId}: ${error.message}`, 'error');
    		if (outputFormat === 'text' && getDebugFlag(session)) {
    			console.error(error); // Log full stack in debug CLI mode
    		}
    		throw error; // Re-throw for the caller
    	}
    }
    
    export default expandTask;
  • Zod schema for the expand-task tool response, defining subtasks array.
    export const ExpandTaskResponseSchema = z.object({
    	subtasks: z.array(SubtaskSchema)
    });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server