chore: changeset + update rules.

This commit is contained in:
Eyal Toledano
2025-04-03 04:09:27 -04:00
parent 1582fe32c1
commit bad16b200f
54 changed files with 1462 additions and 316 deletions

View File

@@ -29,6 +29,33 @@
- Include new sections about handling dependencies during task removal operations
- Document naming conventions and implementation patterns for destructive operations
- **Implement silent mode across all direct functions:**
- Add `enableSilentMode` and `disableSilentMode` utility imports to all direct function files
- Wrap all core function calls with silent mode to prevent console logs from interfering with JSON responses
- Add comprehensive error handling to ensure silent mode is disabled even when errors occur
- Fix "Unexpected token 'I', "[INFO] Gene"... is not valid JSON" errors by suppressing log output
- Apply consistent silent mode pattern across all MCP direct functions
- Maintain clean JSON responses for better integration with client tools
- **Implement AsyncOperationManager for background task processing:**
- Add new `async-manager.js` module to handle long-running operations asynchronously
- Support background execution of computationally intensive tasks like expansion and analysis
- Implement unique operation IDs with UUID generation for reliable tracking
- Add operation status tracking (pending, running, completed, failed)
- Create `get_operation_status` MCP tool to check on background task progress
- Forward progress reporting from background tasks to the client
- Implement operation history with automatic cleanup of completed operations
- Support proper error handling in background tasks with detailed status reporting
- Maintain context (log, session) for background operations ensuring consistent behavior
- **Implement initialize_project command:**
- Add new MCP tool to allow project setup via integrated MCP clients
- Create `initialize_project` direct function with proper parameter handling
- Improve onboarding experience by adding to mcp.json configuration
- Support project-specific metadata like name, description, and version
- Handle shell alias creation with proper confirmation
- Improve first-time user experience in AI environments
- **Refactor project root handling for MCP Server:**
- **Prioritize Session Roots**: MCP tools now extract the project root path directly from `session.roots[0].uri` provided by the client (e.g., Cursor).
- **New Utility `getProjectRootFromSession`**: Added to `mcp-server/src/tools/utils.js` to encapsulate session root extraction and decoding. **Further refined for more reliable detection, especially in integrated environments, including deriving root from script path and avoiding fallback to '/'.**

View File

@@ -93,6 +93,7 @@ alwaysApply: false
- Includes string manipulation utilities (e.g., `truncate`, `sanitizePrompt`).
- Offers task-specific utility functions (e.g., `formatTaskId`, `findTaskById`, `taskExists`).
- Implements graph algorithms like cycle detection for dependency management.
- **Silent Mode Control**: Provides `enableSilentMode` and `disableSilentMode` functions to control log output.
- **Key Components**:
- `CONFIG`: Global configuration object.
- `log(level, ...args)`: Logging function.
@@ -100,6 +101,7 @@ alwaysApply: false
- `truncate(text, maxLength)`: String truncation utility.
- `formatTaskId(id)` / `findTaskById(tasks, taskId)`: Task ID and search utilities.
- `findCycles(subtaskId, dependencyMap)`: Cycle detection algorithm.
- `enableSilentMode()` / `disableSilentMode()`: Control console logging output.
- **[`mcp-server/`](mdc:mcp-server/): MCP Server Integration**
- **Purpose**: Provides an MCP (Model Context Protocol) interface for Task Master, allowing integration with external tools like Cursor. Uses FastMCP framework.
@@ -110,6 +112,9 @@ alwaysApply: false
- Tool `execute` methods use `getProjectRootFromSession` (from [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js)) to determine the project root from the client session and pass it to the direct function.
- **Direct function wrappers (`*Direct` functions in `mcp-server/src/core/direct-functions/*.js`) contain the main logic for handling MCP requests**, including path resolution, argument validation, caching, and calling core Task Master functions.
- Direct functions use `findTasksJsonPath` (from [`core/utils/path-utils.js`](mdc:mcp-server/src/core/utils/path-utils.js)) to locate `tasks.json` based on the provided `projectRoot`.
- **Silent Mode Implementation**: Direct functions use `enableSilentMode` and `disableSilentMode` to prevent logs from interfering with JSON responses.
- **Async Operations**: Uses `AsyncOperationManager` to handle long-running operations in the background.
- **Project Initialization**: Provides `initialize_project` command for setting up new projects from within integrated clients.
- Tool `execute` methods use `handleApiResult` from [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js) to process the result from the direct function and format the final MCP response.
- Uses CLI execution via `executeTaskMasterCommand` as a fallback only when necessary.
- **Implements Robust Path Finding**: The utility [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js) (specifically `getProjectRootFromSession`) and [`core/utils/path-utils.js`](mdc:mcp-server/src/core/utils/path-utils.js) (specifically `findTasksJsonPath`) work together. The tool gets the root via session, passes it to the direct function, which uses `findTasksJsonPath` to locate the specific `tasks.json` file within that root.
@@ -121,7 +126,7 @@ alwaysApply: false
- `mcp-server/src/server.js`: Main server setup and initialization.
- `mcp-server/src/tools/`: Directory containing individual tool definitions. Each tool's `execute` method orchestrates the call to core logic and handles the response.
- `mcp-server/src/tools/utils.js`: Provides MCP-specific utilities like `handleApiResult`, `processMCPResponseData`, `getCachedOrExecute`, and **`getProjectRootFromSession`**.
- `mcp-server/src/core/utils/`: Directory containing utility functions specific to the MCP server, like **`path-utils.js` for resolving `tasks.json` within a given root**.
- `mcp-server/src/core/utils/`: Directory containing utility functions specific to the MCP server, like **`path-utils.js` for resolving `tasks.json` within a given root** and **`async-manager.js` for handling background operations**.
- `mcp-server/src/core/direct-functions/`: Directory containing individual files for each **direct function wrapper (`*Direct`)**. These files contain the primary logic for MCP tool execution.
- `mcp-server/src/core/resources/`: Directory containing resource handlers for task templates, workflow definitions, and other static/dynamic data exposed to LLM clients.
- [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js): Acts as an import/export hub, collecting and exporting direct functions from the `direct-functions` directory and MCP utility functions.
@@ -131,6 +136,17 @@ alwaysApply: false
- **Tool Registration Functions** use **camelCase** with `Tool` suffix: `registerListTasksTool`, `registerSetTaskStatusTool`
- **MCP Tool Names** use **snake_case**: `list_tasks`, `set_task_status`, `parse_prd_document`
- **Resource Handlers** use **camelCase** with pattern URI: `@mcp.resource("tasks://templates/{template_id}")`
- **AsyncOperationManager**:
- **Purpose**: Manages background execution of long-running operations.
- **Location**: `mcp-server/src/core/utils/async-manager.js`
- **Key Features**:
- Operation tracking with unique IDs using UUID
- Status management (pending, running, completed, failed)
- Progress reporting forwarded from background tasks
- Operation history with automatic cleanup of completed operations
- Context preservation (log, session, reportProgress)
- Robust error handling for background tasks
- **Usage**: Used for CPU-intensive operations like task expansion and PRD parsing
- **Data Flow and Module Dependencies**:
@@ -191,10 +207,11 @@ Follow these steps to add MCP support for an existing Task Master command (see [
2. **Create Direct Function File in `mcp-server/src/core/direct-functions/`**:
- Create a new file (e.g., `your-command.js`) using **kebab-case** naming.
- Import necessary core functions and **`findTasksJsonPath` from `../utils/path-utils.js`**.
- Import necessary core functions, **`findTasksJsonPath` from `../utils/path-utils.js`**, and **silent mode utilities**.
- Implement `async function yourCommandDirect(args, log)` using **camelCase** with `Direct` suffix:
- **Path Resolution**: Obtain the tasks file path using `const tasksPath = findTasksJsonPath(args, log);`. This relies on `args.projectRoot` being provided.
- Parse other `args` and perform necessary validation.
- **Implement Silent Mode**: Wrap core function calls with `enableSilentMode()` and `disableSilentMode()`.
- Implement caching with `getCachedOrExecute` if applicable.
- Call core logic.
- Return `{ success: true/false, data/error, fromCache: boolean }`.
@@ -207,11 +224,41 @@ Follow these steps to add MCP support for an existing Task Master command (see [
- Import `zod`, `handleApiResult`, **`getProjectRootFromSession`**, and your `yourCommandDirect` function.
- Implement `registerYourCommandTool(server)`.
- **Define parameters, making `projectRoot` optional**: `projectRoot: z.string().optional().describe(...)`.
- Consider if this operation should run in the background using `AsyncOperationManager`.
- Implement the standard `execute` method:
- Get `rootFolder` using `getProjectRootFromSession` (with fallback to `args.projectRoot`).
- Call `yourCommandDirect({ ...args, projectRoot: rootFolder }, log)`.
- Call `yourCommandDirect({ ...args, projectRoot: rootFolder }, log)` or use `asyncOperationManager.addOperation`.
- Pass the result to `handleApiResult`.
5. **Register Tool**: Import and call `registerYourCommandTool` in `mcp-server/src/tools/index.js`.
6. **Update `mcp.json`**: Add the new tool definition.
## Project Initialization
The `initialize_project` command provides a way to set up a new Task Master project:
- **CLI Command**: `task-master init`
- **MCP Tool**: `initialize_project`
- **Functionality**:
- Creates necessary directories and files for a new project
- Sets up `tasks.json` and initial task files
- Configures project metadata (name, description, version)
- Handles shell alias creation if requested
- Works in both interactive and non-interactive modes
## Async Operation Management
The AsyncOperationManager provides background task execution capabilities:
- **Location**: `mcp-server/src/core/utils/async-manager.js`
- **Key Components**:
- `asyncOperationManager` singleton instance
- `addOperation(operationFn, args, context)` method
- `getStatus(operationId)` method
- **Usage Flow**:
1. Client calls an MCP tool that may take time to complete
2. Tool uses AsyncOperationManager to run the operation in background
3. Tool returns immediate response with operation ID
4. Client polls `get_operation_status` tool with the ID
5. Once completed, client can access operation results

View File

@@ -89,6 +89,44 @@ When implementing a new direct function in `mcp-server/src/core/direct-functions
}
```
5. **Silent Mode Implementation**:
- ✅ **DO**: Import silent mode utilities at the top of your file
```javascript
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
```
- ✅ **DO**: Wrap core function calls with silent mode control
```javascript
// Enable silent mode before the core function call
enableSilentMode();
// Execute core function
const result = await coreFunction(param1, param2);
// Restore normal logging
disableSilentMode();
```
- ✅ **DO**: Add proper error handling to ensure silent mode is disabled
```javascript
try {
enableSilentMode();
// Core function execution
const result = await coreFunction(param1, param2);
disableSilentMode();
return { success: true, data: result };
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in function: ${error.message}`);
return {
success: false,
error: { code: 'ERROR_CODE', message: error.message }
};
}
```
- ❌ **DON'T**: Forget to disable silent mode when errors occur
- ❌ **DON'T**: Leave silent mode enabled outside a direct function's scope
- ❌ **DON'T**: Skip silent mode for core function calls that generate logs
## Tool Definition and Execution
### Tool Structure
@@ -174,6 +212,114 @@ execute: async (args, { log, reportProgress, session }) => {
}
```
### Using AsyncOperationManager for Background Tasks
For tools that execute long-running operations, use the AsyncOperationManager to run them in the background:
```javascript
import { asyncOperationManager } from '../core/utils/async-manager.js';
import { getProjectRootFromSession, createContentResponse, createErrorResponse } from './utils.js';
import { someIntensiveDirect } from '../core/task-master-core.js';
// ... inside server.addTool({...})
execute: async (args, { log, reportProgress, session }) => {
try {
log.info(`Starting background operation with args: ${JSON.stringify(args)}`);
// 1. Get Project Root
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
// 2. Add operation to the async manager
const operationId = asyncOperationManager.addOperation(
someIntensiveDirect, // The direct function to execute
{ ...args, projectRoot: rootFolder }, // Args to pass
{ log, reportProgress, session } // Context to preserve
);
// 3. Return immediate response with operation ID
return createContentResponse({
message: "Operation started successfully",
operationId,
status: "pending"
});
} catch (error) {
log.error(`Error starting background operation: ${error.message}`);
return createErrorResponse(error.message);
}
}
```
Clients should then use the `get_operation_status` tool to check on operation progress:
```javascript
// In get-operation-status.js
import { asyncOperationManager } from '../core/utils/async-manager.js';
import { createContentResponse, createErrorResponse } from './utils.js';
// ... inside server.addTool({...})
execute: async (args, { log }) => {
try {
const { operationId } = args;
log.info(`Checking status of operation: ${operationId}`);
const status = asyncOperationManager.getStatus(operationId);
if (status.status === 'not_found') {
return createErrorResponse(status.error.message);
}
return createContentResponse({
...status,
message: `Operation status: ${status.status}`
});
} catch (error) {
log.error(`Error checking operation status: ${error.message}`);
return createErrorResponse(error.message);
}
}
```
### Project Initialization Tool
The `initialize_project` tool allows integrated clients like Cursor to set up a new Task Master project:
```javascript
// In initialize-project.js
import { z } from "zod";
import { initializeProjectDirect } from "../core/task-master-core.js";
import { handleApiResult, createErrorResponse } from "./utils.js";
export function registerInitializeProjectTool(server) {
server.addTool({
name: "initialize_project",
description: "Initialize a new Task Master project",
parameters: z.object({
projectName: z.string().optional().describe("The name for the new project"),
projectDescription: z.string().optional().describe("A brief description"),
projectVersion: z.string().optional().describe("Initial version (e.g., '0.1.0')"),
authorName: z.string().optional().describe("The author's name"),
skipInstall: z.boolean().optional().describe("Skip installing dependencies"),
addAliases: z.boolean().optional().describe("Add shell aliases"),
yes: z.boolean().optional().describe("Skip prompts and use defaults")
}),
execute: async (args, { log, reportProgress }) => {
try {
// Since we're initializing, we don't need project root
const result = await initializeProjectDirect(args, log);
return handleApiResult(result, log, 'Error initializing project');
} catch (error) {
log.error(`Error in initialize_project: ${error.message}`);
return createErrorResponse(`Failed to initialize project: ${error.message}`);
}
}
});
}
```
### Logging Convention
The `log` object (destructured from `context`) provides standardized logging methods. Use it within both the `execute` method and the `*Direct` functions.
@@ -215,6 +361,7 @@ These functions, located in `mcp-server/src/core/direct-functions/`, form the co
- Receive `args` (including the `projectRoot` determined by the tool) and `log` object.
- **Find `tasks.json`**: Use `findTasksJsonPath(args, log)` from [`core/utils/path-utils.js`](mdc:mcp-server/src/core/utils/path-utils.js). This function prioritizes the provided `args.projectRoot`.
- Validate arguments specific to the core logic.
- **Implement Silent Mode**: Import and use `enableSilentMode` and `disableSilentMode` around core function calls.
- **Implement Caching**: Use `getCachedOrExecute` from [`tools/utils.js`](mdc:mcp-server/src/tools/utils.js) for read operations.
- Call the underlying function from the core Task Master modules.
- Handle errors gracefully.
@@ -225,6 +372,9 @@ These functions, located in `mcp-server/src/core/direct-functions/`, form the co
- **Prefer Direct Function Calls**: MCP tools should always call `*Direct` wrappers instead of `executeTaskMasterCommand`.
- **Standardized Execution Flow**: Follow the pattern: MCP Tool -> `getProjectRootFromSession` -> `*Direct` Function -> Core Logic.
- **Path Resolution via Direct Functions**: The `*Direct` function is responsible for finding the exact `tasks.json` path using `findTasksJsonPath`, relying on the `projectRoot` passed in `args`.
- **Silent Mode in Direct Functions**: Wrap all core function calls with `enableSilentMode()` and `disableSilentMode()` to prevent logs from interfering with JSON responses.
- **Async Processing for Intensive Operations**: Use AsyncOperationManager for CPU-intensive or long-running operations.
- **Project Initialization**: Use the initialize_project tool for setting up new projects in integrated environments.
- **Centralized Utilities**: Use helpers from `mcp-server/src/tools/utils.js` (like `handleApiResult`, `getProjectRootFromSession`, `getCachedOrExecute`) and `mcp-server/src/core/utils/path-utils.js` (`findTasksJsonPath`). See [`utilities.mdc`](mdc:.cursor/rules/utilities.mdc).
- **Caching in Direct Functions**: Caching logic resides *within* the `*Direct` functions using `getCachedOrExecute`.
@@ -246,10 +396,11 @@ Follow these steps to add MCP support for an existing Task Master command (see [
2. **Create Direct Function File in `mcp-server/src/core/direct-functions/`**:
- Create a new file (e.g., `your-command.js`) using **kebab-case** naming.
- Import necessary core functions and **`findTasksJsonPath` from `../utils/path-utils.js`**.
- Import necessary core functions, **`findTasksJsonPath` from `../utils/path-utils.js`**, and **silent mode utilities**.
- Implement `async function yourCommandDirect(args, log)` using **camelCase** with `Direct` suffix:
- **Path Resolution**: Obtain the tasks file path using `const tasksPath = findTasksJsonPath(args, log);`. This handles project root detection automatically based on `args.projectRoot`.
- Parse other `args` and perform necessary validation.
- **Implement Silent Mode**: Wrap core function calls with enableSilentMode/disableSilentMode.
- **If Caching**: Implement caching using `getCachedOrExecute` from `../../tools/utils.js`.
- **If Not Caching**: Directly call the core logic function within a try/catch block.
- Format the return as `{ success: true/false, data/error, fromCache: boolean }`.
@@ -312,16 +463,18 @@ Follow these steps to add MCP support for an existing Task Master command (see [
- [ ] Identify all required parameters and their types
2. **Direct Function Wrapper**:
- [ ] Create the `*Direct` function in `task-master-core.js`
- [ ] Create the `*Direct` function in the appropriate file in `mcp-server/src/core/direct-functions/`
- [ ] Import silent mode utilities and implement them around core function calls
- [ ] Handle all parameter validations and type conversions
- [ ] Implement path resolving for relative paths
- [ ] Add appropriate error handling with standardized error codes
- [ ] Add to `directFunctions` map
- [ ] Add to imports/exports in `task-master-core.js`
3. **MCP Tool Implementation**:
- [ ] Create new file in `mcp-server/src/tools/` with kebab-case naming
- [ ] Define zod schema for all parameters
- [ ] Implement the `execute` method following the standard pattern
- [ ] Consider using AsyncOperationManager for long-running operations
- [ ] Register tool in `mcp-server/src/tools/index.js`
4. **Testing**:

View File

@@ -97,6 +97,35 @@ The standard pattern for adding a feature follows this workflow:
}
```
- **Silent Mode Implementation**:
- ✅ **DO**: Import silent mode utilities in direct functions: `import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';`
- ✅ **DO**: Wrap core function calls with silent mode:
```javascript
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core function
const result = await coreFunction(...);
// Restore normal logging
disableSilentMode();
```
- ✅ **DO**: Ensure silent mode is disabled in error handling:
```javascript
try {
enableSilentMode();
// Core function call
disableSilentMode();
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
throw error; // Rethrow to be caught by outer catch block
}
```
- ✅ **DO**: Add silent mode handling in all direct functions that call core functions
- ❌ **DON'T**: Forget to disable silent mode, which would suppress all future logs
- ❌ **DON'T**: Enable silent mode outside of direct functions in the MCP server
```javascript
// 1. CORE LOGIC: Add function to appropriate module (example in task-manager.js)
/**
@@ -388,69 +417,112 @@ Integrating Task Master commands with the MCP server (for use by tools like Curs
1. **Core Logic**: Ensure the command's core logic exists and is exported from the appropriate module (e.g., [`task-manager.js`](mdc:scripts/modules/task-manager.js)).
2. **Direct Function Wrapper (`mcp-server/src/core/direct-functions/`)**:
- Create a new file (e.g., `your-command.js`) in `mcp-server/src/core/direct-functions/` using **kebab-case** naming.
- Import the core logic function and necessary MCP utilities like **`findTasksJsonPath` from `../utils/path-utils.js`** and potentially `getCachedOrExecute` from `../../tools/utils.js`.
- Import the core logic function, necessary MCP utilities like **`findTasksJsonPath` from `../utils/path-utils.js`**, and **silent mode utilities**: `import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';`
- Implement an `async function yourCommandDirect(args, log)` using **camelCase** with `Direct` suffix.
- **Path Finding**: Inside this function, obtain the `tasksPath` by calling `const tasksPath = findTasksJsonPath(args, log);`. This relies on `args.projectRoot` (derived from the session) being passed correctly.
- Perform validation on other arguments received in `args`.
- **Implement Caching (if applicable)**:
- **Use Case**: Apply caching primarily for read-only operations that benefit from repeated calls (e.g., `get_tasks`, `get_task`, `next_task`). Avoid caching for operations that modify state (`set_task_status`, `add_task`, `parse_prd`, `update_task`, `add_dependency`, etc.).
- **Implementation**: Use the `getCachedOrExecute` utility (imported from `../../tools/utils.js`).
- Generate a unique `cacheKey` based on relevant arguments (e.g., file path, filters).
- Define an `async` function `coreActionFn` that wraps the actual call to the core logic and returns `{ success: true/false, data/error }`.
- Call `await getCachedOrExecute({ cacheKey, actionFn: coreActionFn, log });`.
- **Implement Silent Mode**: Wrap core function calls with `enableSilentMode()` and `disableSilentMode()` to prevent logs from interfering with JSON responses.
- **If Caching**: Implement caching using `getCachedOrExecute` from `../../tools/utils.js`.
- **If Not Caching**: Directly call the core logic function within a try/catch block.
- Handle errors and return a standard object: `{ success: true/false, data/error, fromCache: boolean }`. (For non-cached operations, `fromCache` is `false`).
- Export the function.
3. **Export from `task-master-core.js`**: Import your new `*Direct` function into [`task-master-core.js`](mdc:mcp-server/src/core/task-master-core.js), re-export it, and add it to the `directFunctions` map.
4. **MCP Tool File (`mcp-server/src/tools/`)**:
- Create a new file (e.g., `your-command.js`) using **kebab-case** naming.
- Import `zod` (for schema), `handleApiResult`, `createErrorResponse`, **`getProjectRootFromSession`** from `./utils.js`, and your `yourCommandDirect` function from `../core/task-master-core.js`.
- Implement `registerYourCommandTool(server)` using **camelCase** with `Tool` suffix, which calls `server.addTool`.
- Define the tool's `name` using **snake_case** (e.g., `your_command`), `description`, and `parameters` using `zod`. **Crucially, define `projectRoot` as optional**: `projectRoot: z.string().optional().describe(...)`. Include `file: z.string().optional().describe(...)` if applicable.
- Define the standard `async execute(args, { log, reportProgress, session })` method:
```javascript
// In mcp-server/src/tools/your-command.js
import { z } from "zod";
import { handleApiResult, createErrorResponse, getProjectRootFromSession } from "./utils.js";
import { yourCommandDirect } from "../core/task-master-core.js"; // Adjust path as needed
- Format the return as `{ success: true/false, data/error, fromCache: boolean }`.
- Export the wrapper function.
export function registerYourCommandTool(server) {
server.addTool({
name: "your_command", // snake_case
description: "Description of your command.",
parameters: z.object({
/* zod schema */
projectRoot: z.string().optional().describe("Optional project root path"),
file: z.string().optional().describe("Optional tasks file path relative to project root"),
/* other parameters */
}),
execute: async (args, { log, reportProgress, session }) => { // Destructure context
try {
log.info(`Executing your_command with args: ${JSON.stringify(args)}`);
3. **Update `task-master-core.js` with Import/Export**: Import and re-export your `*Direct` function and add it to the `directFunctions` map.
// 1. Get project root from session (or args fallback)
let rootFolder = getProjectRootFromSession(session, log);
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
4. **Create MCP Tool (`mcp-server/src/tools/`)**:
- Create a new file (e.g., `your-command.js`) using **kebab-case**.
- Import `zod`, `handleApiResult`, `createErrorResponse`, **`getProjectRootFromSession`**, and your `yourCommandDirect` function.
- Implement `registerYourCommandTool(server)`.
- Define the tool `name` using **snake_case** (e.g., `your_command`).
- Define the `parameters` using `zod`. **Crucially, define `projectRoot` as optional**: `projectRoot: z.string().optional().describe(...)`. Include `file` if applicable.
- Implement the standard `async execute(args, { log, reportProgress, session })` method:
- Get `rootFolder` using `getProjectRootFromSession` (with fallback to `args.projectRoot`).
- Call `yourCommandDirect({ ...args, projectRoot: rootFolder }, log)`.
- Pass the result to `handleApiResult(result, log, 'Error Message')`.
// 2. Call the direct function wrapper, passing the resolved root
const result = await yourCommandDirect({
...args,
projectRoot: rootFolder // Pass the resolved root
}, log);
5. **Register Tool**: Import and call `registerYourCommandTool` in `mcp-server/src/tools/index.js`.
// 3. Pass the result to handleApiResult for formatting
return handleApiResult(result, log, 'Error executing your_command'); // Provide default error
} catch (error) {
// Catch unexpected errors from the direct call or handleApiResult itself
log.error(`Unexpected error in your_command tool execute: ${error.message}`);
return createErrorResponse(`Tool execution failed: ${error.message}`);
}
}
});
}
```
5. **Register in Tool Index**: Import and call `registerYourCommandTool` in [`mcp-server/src/tools/index.js`](mdc:mcp-server/src/tools/index.js).
6. **Update `mcp.json`**: Add the tool definition (name, description, parameters) to `.cursor/mcp.json`.
6. **Update `mcp.json`**: Add the new tool definition to the `tools` array in `.cursor/mcp.json`.
## Implementing Background Operations
For long-running operations that should not block the client, use the AsyncOperationManager:
1. **Identify Background-Appropriate Operations**:
- ✅ **DO**: Use async operations for CPU-intensive tasks like task expansion or PRD parsing
- ✅ **DO**: Consider async operations for tasks that may take more than 1-2 seconds
- ❌ **DON'T**: Use async operations for quick read/status operations
- ❌ **DON'T**: Use async operations when immediate feedback is critical
2. **Use AsyncOperationManager in MCP Tools**:
```javascript
import { asyncOperationManager } from '../core/utils/async-manager.js';
// In execute method:
const operationId = asyncOperationManager.addOperation(
expandTaskDirect, // The direct function to run in background
{ ...args, projectRoot: rootFolder }, // Args to pass to the function
{ log, reportProgress, session } // Context to preserve for the operation
);
// Return immediate response with operation ID
return createContentResponse({
message: "Operation started successfully",
operationId,
status: "pending"
});
```
3. **Implement Progress Reporting**:
- ✅ **DO**: Use the reportProgress function in direct functions:
```javascript
// In your direct function:
if (reportProgress) {
await reportProgress({ progress: 50 }); // 50% complete
}
```
- AsyncOperationManager will forward progress updates to the client
4. **Check Operation Status**:
- Implement a way for clients to check status using the `get_operation_status` MCP tool
- Return appropriate status codes and messages
## Project Initialization
When implementing project initialization commands:
1. **Support Programmatic Initialization**:
- ✅ **DO**: Design initialization to work with both CLI and MCP
- ✅ **DO**: Support non-interactive modes with sensible defaults
- ✅ **DO**: Handle project metadata like name, description, version
- ✅ **DO**: Create necessary files and directories
2. **In MCP Tool Implementation**:
```javascript
// In initialize-project.js MCP tool:
import { z } from "zod";
import { initializeProjectDirect } from "../core/task-master-core.js";
export function registerInitializeProjectTool(server) {
server.addTool({
name: "initialize_project",
description: "Initialize a new Task Master project",
parameters: z.object({
projectName: z.string().optional().describe("The name for the new project"),
projectDescription: z.string().optional().describe("A brief description"),
projectVersion: z.string().optional().describe("Initial version (e.g., '0.1.0')"),
// Add other parameters as needed
}),
execute: async (args, { log, reportProgress, session }) => {
try {
// No need for project root since we're creating a new project
const result = await initializeProjectDirect(args, log);
return handleApiResult(result, log, 'Error initializing project');
} catch (error) {
log.error(`Error in initialize_project: ${error.message}`);
return createErrorResponse(`Failed to initialize project: ${error.message}`);
}
}
});
}
```

View File

@@ -5,6 +5,7 @@
import { addDependency } from '../../../../scripts/modules/dependency-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for addDependency with error handling.
@@ -51,9 +52,15 @@ export async function addDependencyDirect(args, log) {
log.info(`Adding dependency: task ${taskId} will depend on ${dependencyId}`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core function
await addDependency(tasksPath, taskId, dependencyId);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
@@ -63,6 +70,9 @@ export async function addDependencyDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in addDependencyDirect: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
import { addSubtask } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Add a subtask to an existing task
@@ -67,10 +68,17 @@ export async function addSubtaskDirect(args, log) {
// Determine if we should generate files
const generateFiles = !args.skipGenerate;
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Case 1: Convert existing task to subtask
if (existingTaskId) {
log.info(`Converting task ${existingTaskId} to a subtask of ${parentId}`);
const result = await addSubtask(tasksPath, parentId, existingTaskId, null, generateFiles);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
@@ -92,6 +100,10 @@ export async function addSubtaskDirect(args, log) {
};
const result = await addSubtask(tasksPath, parentId, null, newSubtaskData, generateFiles);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
@@ -101,6 +113,9 @@ export async function addSubtaskDirect(args, log) {
};
}
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in addSubtaskDirect: ${error.message}`);
return {
success: false,

View File

@@ -5,6 +5,7 @@
import { addTask } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for adding a new task with error handling.
@@ -20,6 +21,9 @@ import { findTasksJsonPath } from '../utils/path-utils.js';
*/
export async function addTaskDirect(args, log) {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Find the tasks.json path
const tasksPath = findTasksJsonPath(args, log);
@@ -44,8 +48,18 @@ export async function addTaskDirect(args, log) {
log.info(`Adding new task with prompt: "${prompt}", dependencies: [${dependencies.join(', ')}], priority: ${priority}`);
// Call the addTask function
const newTaskId = await addTask(tasksPath, prompt, dependencies, priority);
// Call the addTask function with 'json' outputFormat to prevent console output when called via MCP
const newTaskId = await addTask(
tasksPath,
prompt,
dependencies,
priority,
{ mcpLog: log },
'json'
);
// Restore normal logging
disableSilentMode();
return {
success: true,
@@ -55,6 +69,9 @@ export async function addTaskDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in addTaskDirect: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
import { analyzeTaskComplexity } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import path from 'path';
@@ -48,9 +49,15 @@ export async function analyzeTaskComplexityDirect(args, log) {
log.info('Using Perplexity AI for research-backed complexity analysis');
}
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core function
await analyzeTaskComplexity(options);
// Restore normal logging
disableSilentMode();
// Verify the report file was created
if (!fs.existsSync(outputPath)) {
return {
@@ -79,6 +86,9 @@ export async function analyzeTaskComplexityDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in analyzeTaskComplexityDirect: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
import { clearSubtasks } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import fs from 'fs';
/**
@@ -68,9 +69,15 @@ export async function clearSubtasksDirect(args, log) {
log.info(`Clearing subtasks from tasks: ${taskIds}`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core function
clearSubtasks(tasksPath, taskIds);
// Restore normal logging
disableSilentMode();
// Read the updated data to provide a summary
const updatedData = JSON.parse(fs.readFileSync(tasksPath, 'utf8'));
const taskIdArray = taskIds.split(',').map(id => parseInt(id.trim(), 10));
@@ -90,6 +97,9 @@ export async function clearSubtasksDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in clearSubtasksDirect: ${error.message}`);
return {
success: false,

View File

@@ -3,7 +3,7 @@
* Direct function implementation for displaying complexity analysis report
*/
import { readComplexityReport } from '../../../../scripts/modules/utils.js';
import { readComplexityReport, enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { getCachedOrExecute } from '../../tools/utils.js';
import path from 'path';
@@ -39,8 +39,14 @@ export async function complexityReportDirect(args, log) {
// Define the core action function to read the report
const coreActionFn = async () => {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
const report = readComplexityReport(reportPath);
// Restore normal logging
disableSilentMode();
if (!report) {
log.warn(`No complexity report found at ${reportPath}`);
return {
@@ -60,6 +66,9 @@ export async function complexityReportDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error reading complexity report: ${error.message}`);
return {
success: false,
@@ -82,6 +91,9 @@ export async function complexityReportDirect(args, log) {
return result; // Returns { success, data/error, fromCache }
} catch (error) {
// Catch unexpected errors from getCachedOrExecute itself
// Ensure silent mode is disabled
disableSilentMode();
log.error(`Unexpected error during getCachedOrExecute for complexityReport: ${error.message}`);
return {
success: false,
@@ -93,6 +105,9 @@ export async function complexityReportDirect(args, log) {
};
}
} catch (error) {
// Ensure silent mode is disabled if an outer error occurs
disableSilentMode();
log.error(`Error in complexityReportDirect: ${error.message}`);
return {
success: false,

View File

@@ -3,6 +3,7 @@
*/
import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
/**
@@ -41,23 +42,38 @@ export async function expandAllTasksDirect(args, log) {
log.info('Force regeneration of subtasks is enabled');
}
// Call the core function
await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag);
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// The expandAllTasks function doesn't have a return value, so we'll create our own success response
return {
success: true,
data: {
message: "Successfully expanded all pending tasks with subtasks",
details: {
numSubtasks: numSubtasks,
research: useResearch,
prompt: additionalContext,
force: forceFlag
// Call the core function
await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag);
// Restore normal logging
disableSilentMode();
// The expandAllTasks function doesn't have a return value, so we'll create our own success response
return {
success: true,
data: {
message: "Successfully expanded all pending tasks with subtasks",
details: {
numSubtasks: numSubtasks,
research: useResearch,
prompt: additionalContext,
force: forceFlag
}
}
}
};
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
throw error; // Rethrow to be caught by outer catch block
}
} catch (error) {
// Ensure silent mode is disabled
disableSilentMode();
log.error(`Error in expandAllTasksDirect: ${error.message}`);
return {
success: false,

View File

@@ -4,7 +4,7 @@
*/
import { expandTask } from '../../../../scripts/modules/task-manager.js';
import { readJSON, writeJSON } from '../../../../scripts/modules/utils.js';
import { readJSON, writeJSON, enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import path from 'path';
import fs from 'fs';
@@ -116,28 +116,50 @@ export async function expandTaskDirect(args, log) {
// Save tasks.json with potentially empty subtasks array
writeJSON(tasksPath, data);
// Call the core expandTask function
await expandTask(taskId, numSubtasks, useResearch, additionalContext);
// Process the request
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Read the updated data
const updatedData = readJSON(tasksPath);
const updatedTask = updatedData.tasks.find(t => t.id === taskId);
// Call expandTask
const result = await expandTask(taskId, numSubtasks, useResearch, additionalContext);
// Calculate how many subtasks were added
const subtasksAdded = updatedTask.subtasks ?
updatedTask.subtasks.length - subtasksCountBefore : 0;
// Restore normal logging
disableSilentMode();
// Return the result
log.info(`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`);
return {
success: true,
data: {
task: updatedTask,
subtasksAdded,
hasExistingSubtasks
},
fromCache: false
};
// Read the updated data
const updatedData = readJSON(tasksPath);
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
log.info(`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`);
return {
success: true,
data: {
task: updatedTask,
subtasksAdded,
hasExistingSubtasks
},
fromCache: false
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error expanding task: ${error.message}`);
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
message: error.message || 'Failed to expand task'
},
fromCache: false
};
}
} catch (error) {
log.error(`Error expanding task: ${error.message}`);
return {

View File

@@ -4,6 +4,7 @@
import { fixDependenciesCommand } from '../../../../scripts/modules/dependency-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import fs from 'fs';
/**
@@ -32,9 +33,15 @@ export async function fixDependenciesDirect(args, log) {
};
}
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the original command function
await fixDependenciesCommand(tasksPath);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
@@ -43,6 +50,9 @@ export async function fixDependenciesDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error fixing dependencies: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
*/
import { generateTaskFiles } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import path from 'path';
@@ -39,8 +40,27 @@ export async function generateTaskFilesDirect(args, log) {
log.info(`Generating task files from ${tasksPath} to ${outputDir}`);
// Execute core generateTaskFiles function
generateTaskFiles(tasksPath, outputDir);
// Execute core generateTaskFiles function in a separate try/catch
try {
// Enable silent mode to prevent logs from being written to stdout
enableSilentMode();
// The function is synchronous despite being awaited elsewhere
generateTaskFiles(tasksPath, outputDir);
// Restore normal logging after task generation
disableSilentMode();
} catch (genError) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in generateTaskFiles: ${genError.message}`);
return {
success: false,
error: { code: 'GENERATE_FILES_ERROR', message: genError.message },
fromCache: false
};
}
// Return success with file paths
return {
@@ -54,6 +74,9 @@ export async function generateTaskFilesDirect(args, log) {
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
// Make sure to restore normal logging if an outer error occurs
disableSilentMode();
log.error(`Error generating task files: ${error.message}`);
return {
success: false,

View File

@@ -6,6 +6,7 @@
import { listTasks } from '../../../../scripts/modules/task-manager.js';
import { getCachedOrExecute } from '../../tools/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for listTasks with error handling and caching.
@@ -38,6 +39,9 @@ export async function listTasksDirect(args, log) {
// 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: ${tasksPath}, filter: ${statusFilter}, subtasks: ${withSubtasks}`);
const resultData = listTasks(tasksPath, statusFilter, withSubtasks, 'json');
@@ -46,9 +50,16 @@ export async function listTasksDirect(args, log) {
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' } };
}

View File

@@ -7,6 +7,7 @@ import { findNextTask } from '../../../../scripts/modules/task-manager.js';
import { readJSON } from '../../../../scripts/modules/utils.js';
import { getCachedOrExecute } from '../../tools/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for finding the next task to work on with error handling and caching.
@@ -38,6 +39,9 @@ export async function nextTaskDirect(args, log) {
// Define the action function to be executed on cache miss
const coreNextTaskAction = async () => {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
log.info(`Finding next task from ${tasksPath}`);
// Read tasks data
@@ -67,6 +71,9 @@ export async function nextTaskDirect(args, log) {
};
}
// Restore normal logging
disableSilentMode();
// Return the next task data with the full tasks array for reference
log.info(`Successfully found next task ${nextTask.id}: ${nextTask.title}`);
return {
@@ -77,6 +84,9 @@ export async function nextTaskDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error finding next task: ${error.message}`);
return {
success: false,

View File

@@ -7,6 +7,7 @@ import path from 'path';
import fs from 'fs';
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for parsing PRD documents and generating tasks.
@@ -66,9 +67,15 @@ export async function parsePRDDirect(args, log) {
log.info(`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Execute core parsePRD function (which is not async but we'll await it to maintain consistency)
await parsePRD(inputPath, outputPath, numTasks);
// Restore normal logging
disableSilentMode();
// 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)) {
@@ -94,6 +101,9 @@ export async function parsePRDDirect(args, log) {
};
}
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error parsing PRD: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
import { removeDependency } from '../../../../scripts/modules/dependency-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Remove a dependency from a task
@@ -49,9 +50,15 @@ export async function removeDependencyDirect(args, log) {
log.info(`Removing dependency: task ${taskId} no longer depends on ${dependencyId}`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core function
await removeDependency(tasksPath, taskId, dependencyId);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
@@ -61,6 +68,9 @@ export async function removeDependencyDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in removeDependencyDirect: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
import { removeSubtask } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Remove a subtask from its parent task
@@ -18,6 +19,9 @@ import { findTasksJsonPath } from '../utils/path-utils.js';
*/
export async function removeSubtaskDirect(args, log) {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
if (!args.id) {
@@ -54,6 +58,9 @@ export async function removeSubtaskDirect(args, log) {
const result = await removeSubtask(tasksPath, args.id, convertToTask, generateFiles);
// Restore normal logging
disableSilentMode();
if (convertToTask && result) {
// Return info about the converted task
return {
@@ -73,6 +80,9 @@ export async function removeSubtaskDirect(args, log) {
};
}
} catch (error) {
// Ensure silent mode is disabled even if an outer error occurs
disableSilentMode();
log.error(`Error in removeSubtaskDirect: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
*/
import { removeTask } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
/**
@@ -49,9 +50,15 @@ export async function removeTaskDirect(args, log) {
log.info(`Removing task with ID: ${taskId} from ${tasksPath}`);
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the core removeTask function
const result = await removeTask(tasksPath, taskId);
// Restore normal logging
disableSilentMode();
log.info(`Successfully removed task: ${taskId}`);
// Return the result
@@ -66,6 +73,9 @@ export async function removeTaskDirect(args, log) {
fromCache: false
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error removing task: ${error.message}`);
return {
success: false,
@@ -77,6 +87,9 @@ export async function removeTaskDirect(args, log) {
};
}
} catch (error) {
// Ensure silent mode is disabled even if an outer error occurs
disableSilentMode();
// Catch any unexpected errors
log.error(`Unexpected error in removeTaskDirect: ${error.message}`);
return {

View File

@@ -5,6 +5,7 @@
import { setTaskStatus } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for setTaskStatus with error handling.
@@ -63,20 +64,40 @@ export async function setTaskStatusDirect(args, log) {
log.info(`Setting task ${taskId} status to "${newStatus}"`);
// Execute the setTaskStatus function with source=mcp to avoid console output
await setTaskStatus(tasksPath, taskId, newStatus);
// Call the core function
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Return success data
return {
success: true,
data: {
message: `Successfully updated task ${taskId} status to "${newStatus}"`,
taskId,
status: newStatus,
tasksPath
},
fromCache: false // This operation always modifies state and should never be cached
};
await setTaskStatus(tasksPath, taskId, newStatus);
// Restore normal logging
disableSilentMode();
log.info(`Successfully set task ${taskId} status to ${newStatus}`);
// Return success data
return {
success: true,
data: {
message: `Successfully updated task ${taskId} status to "${newStatus}"`,
taskId,
status: newStatus,
tasksPath
},
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error setting task status: ${error.message}`);
return {
success: false,
error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' },
fromCache: false
};
}
} catch (error) {
log.error(`Error setting task status: ${error.message}`);
return {

View File

@@ -7,6 +7,7 @@ import { findTaskById } from '../../../../scripts/modules/utils.js';
import { readJSON } from '../../../../scripts/modules/utils.js';
import { getCachedOrExecute } from '../../tools/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for showing task details with error handling and caching.
@@ -52,6 +53,9 @@ export async function showTaskDirect(args, log) {
// Define the action function to be executed on cache miss
const coreShowTaskAction = async () => {
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
log.info(`Retrieving task details for ID: ${taskId} from ${tasksPath}`);
// Read tasks data
@@ -79,6 +83,9 @@ export async function showTaskDirect(args, log) {
};
}
// Restore normal logging
disableSilentMode();
// Return the task data with the full tasks array for reference
// (needed for formatDependenciesWithStatus function in UI)
log.info(`Successfully found task ${taskId}`);
@@ -90,6 +97,9 @@ export async function showTaskDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error showing task: ${error.message}`);
return {
success: false,
@@ -112,6 +122,7 @@ export async function showTaskDirect(args, log) {
return result; // Returns { success, data/error, fromCache }
} catch (error) {
// Catch unexpected errors from getCachedOrExecute itself
disableSilentMode();
log.error(`Unexpected error during getCachedOrExecute for showTask: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
*/
import { updateSubtaskById } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
/**
@@ -68,35 +69,50 @@ export async function updateSubtaskByIdDirect(args, log) {
log.info(`Updating subtask with ID ${subtaskId} with prompt "${args.prompt}" and research: ${useResearch}`);
// Execute core updateSubtaskById function
const updatedSubtask = await updateSubtaskById(tasksPath, subtaskId, args.prompt, useResearch);
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Handle the case where the subtask couldn't be updated (e.g., already marked as done)
if (!updatedSubtask) {
// Execute core updateSubtaskById function
const updatedSubtask = await updateSubtaskById(tasksPath, subtaskId, args.prompt, useResearch);
// Restore normal logging
disableSilentMode();
// Handle the case where the subtask couldn't be updated (e.g., already marked as done)
if (!updatedSubtask) {
return {
success: false,
error: {
code: 'SUBTASK_UPDATE_FAILED',
message: 'Failed to update subtask. It may be marked as completed, or another error occurred.'
},
fromCache: false
};
}
// Return the updated subtask information
return {
success: false,
error: {
code: 'SUBTASK_UPDATE_FAILED',
message: 'Failed to update subtask. It may be marked as completed, or another error occurred.'
success: true,
data: {
message: `Successfully updated subtask with ID ${subtaskId}`,
subtaskId,
parentId: subtaskId.split('.')[0],
subtask: updatedSubtask,
tasksPath,
useResearch
},
fromCache: false
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
throw error; // Rethrow to be caught by outer catch block
}
// Return the updated subtask information
return {
success: true,
data: {
message: `Successfully updated subtask with ID ${subtaskId}`,
subtaskId,
parentId: subtaskId.split('.')[0],
subtask: updatedSubtask,
tasksPath,
useResearch
},
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
// Ensure silent mode is disabled
disableSilentMode();
log.error(`Error updating subtask by ID: ${error.message}`);
return {
success: false,

View File

@@ -5,6 +5,7 @@
import { updateTaskById } from '../../../../scripts/modules/task-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
/**
* Direct function wrapper for updateTaskById with error handling.
@@ -79,9 +80,15 @@ export async function updateTaskByIdDirect(args, log) {
log.info(`Updating task with ID ${taskId} with prompt "${args.prompt}" and research: ${useResearch}`);
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Execute core updateTaskById function
await updateTaskById(tasksPath, taskId, args.prompt, useResearch);
// Restore normal logging
disableSilentMode();
// Since updateTaskById doesn't return a value but modifies the tasks file,
// we'll return a success message
return {
@@ -95,6 +102,9 @@ export async function updateTaskByIdDirect(args, log) {
fromCache: false // This operation always modifies state and should never be cached
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error updating task by ID: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
*/
import { updateTasks } from '../../../../scripts/modules/task-manager.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
/**
@@ -73,22 +74,37 @@ export async function updateTasksDirect(args, log) {
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);
try {
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// 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
};
// Execute core updateTasks function
await updateTasks(tasksPath, fromId, args.prompt, useResearch);
// Restore normal logging
disableSilentMode();
// 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) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
throw error; // Rethrow to be caught by outer catch block
}
} catch (error) {
// Ensure silent mode is disabled
disableSilentMode();
log.error(`Error updating tasks: ${error.message}`);
return {
success: false,

View File

@@ -4,6 +4,7 @@
import { validateDependenciesCommand } from '../../../../scripts/modules/dependency-manager.js';
import { findTasksJsonPath } from '../utils/path-utils.js';
import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js';
import fs from 'fs';
/**
@@ -32,9 +33,15 @@ export async function validateDependenciesDirect(args, log) {
};
}
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Call the original command function
await validateDependenciesCommand(tasksPath);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
@@ -43,6 +50,9 @@ export async function validateDependenciesDirect(args, log) {
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error validating dependencies: ${error.message}`);
return {
success: false,

View File

@@ -0,0 +1,217 @@
import { v4 as uuidv4 } from 'uuid';
class AsyncOperationManager {
constructor() {
this.operations = new Map(); // Stores active operation state
this.completedOperations = new Map(); // Stores completed operations
this.maxCompletedOperations = 100; // Maximum number of completed operations to store
this.listeners = new Map(); // For potential future notifications
}
/**
* Adds an operation to be executed asynchronously.
* @param {Function} operationFn - The async function to execute (e.g., a Direct function).
* @param {Object} args - Arguments to pass to the operationFn.
* @param {Object} context - The MCP tool context { log, reportProgress, session }.
* @returns {string} The unique ID assigned to this operation.
*/
addOperation(operationFn, args, context) {
const operationId = `op-${uuidv4()}`;
const operation = {
id: operationId,
status: 'pending',
startTime: Date.now(),
endTime: null,
result: null,
error: null,
// Store necessary parts of context, especially log for background execution
log: context.log,
reportProgress: context.reportProgress, // Pass reportProgress through
session: context.session // Pass session through if needed by the operationFn
};
this.operations.set(operationId, operation);
this.log(operationId, 'info', `Operation added.`);
// Start execution in the background (don't await here)
this._runOperation(operationId, operationFn, args, context).catch(err => {
// Catch unexpected errors during the async execution setup itself
this.log(operationId, 'error', `Critical error starting operation: ${err.message}`, { stack: err.stack });
operation.status = 'failed';
operation.error = { code: 'MANAGER_EXECUTION_ERROR', message: err.message };
operation.endTime = Date.now();
// Move to completed operations
this._moveToCompleted(operationId);
});
return operationId;
}
/**
* Internal function to execute the operation.
* @param {string} operationId - The ID of the operation.
* @param {Function} operationFn - The async function to execute.
* @param {Object} args - Arguments for the function.
* @param {Object} context - The original MCP tool context.
*/
async _runOperation(operationId, operationFn, args, context) {
const operation = this.operations.get(operationId);
if (!operation) return; // Should not happen
operation.status = 'running';
this.log(operationId, 'info', `Operation running.`);
this.emit('statusChanged', { operationId, status: 'running' });
try {
// Pass the necessary context parts to the direct function
// The direct function needs to be adapted if it needs reportProgress
// We pass the original context's log, plus our wrapped reportProgress
const result = await operationFn(args, operation.log, {
reportProgress: (progress) => this._handleProgress(operationId, progress),
mcpLog: operation.log, // Pass log as mcpLog if direct fn expects it
session: operation.session
});
operation.status = result.success ? 'completed' : 'failed';
operation.result = result.success ? result.data : null;
operation.error = result.success ? null : result.error;
this.log(operationId, 'info', `Operation finished with status: ${operation.status}`);
} catch (error) {
this.log(operationId, 'error', `Operation failed with error: ${error.message}`, { stack: error.stack });
operation.status = 'failed';
operation.error = { code: 'OPERATION_EXECUTION_ERROR', message: error.message };
} finally {
operation.endTime = Date.now();
this.emit('statusChanged', { operationId, status: operation.status, result: operation.result, error: operation.error });
// Move to completed operations if done or failed
if (operation.status === 'completed' || operation.status === 'failed') {
this._moveToCompleted(operationId);
}
}
}
/**
* Move an operation from active operations to completed operations history.
* @param {string} operationId - The ID of the operation to move.
* @private
*/
_moveToCompleted(operationId) {
const operation = this.operations.get(operationId);
if (!operation) return;
// Store only the necessary data in completed operations
const completedData = {
id: operation.id,
status: operation.status,
startTime: operation.startTime,
endTime: operation.endTime,
result: operation.result,
error: operation.error,
};
this.completedOperations.set(operationId, completedData);
this.operations.delete(operationId);
// Trim completed operations if exceeding maximum
if (this.completedOperations.size > this.maxCompletedOperations) {
// Get the oldest operation (sorted by endTime)
const oldest = [...this.completedOperations.entries()]
.sort((a, b) => a[1].endTime - b[1].endTime)[0];
if (oldest) {
this.completedOperations.delete(oldest[0]);
}
}
}
/**
* Handles progress updates from the running operation and forwards them.
* @param {string} operationId - The ID of the operation reporting progress.
* @param {Object} progress - The progress object { progress, total? }.
*/
_handleProgress(operationId, progress) {
const operation = this.operations.get(operationId);
if (operation && operation.reportProgress) {
try {
// Use the reportProgress function captured from the original context
operation.reportProgress(progress);
this.log(operationId, 'debug', `Reported progress: ${JSON.stringify(progress)}`);
} catch(err) {
this.log(operationId, 'warn', `Failed to report progress: ${err.message}`);
// Don't stop the operation, just log the reporting failure
}
}
}
/**
* Retrieves the status and result/error of an operation.
* @param {string} operationId - The ID of the operation.
* @returns {Object | null} The operation details or null if not found.
*/
getStatus(operationId) {
// First check active operations
const operation = this.operations.get(operationId);
if (operation) {
return {
id: operation.id,
status: operation.status,
startTime: operation.startTime,
endTime: operation.endTime,
result: operation.result,
error: operation.error,
};
}
// Then check completed operations
const completedOperation = this.completedOperations.get(operationId);
if (completedOperation) {
return completedOperation;
}
// Operation not found in either active or completed
return {
error: {
code: 'OPERATION_NOT_FOUND',
message: `Operation ID ${operationId} not found. It may have been completed and removed from history, or the ID may be invalid.`
},
status: 'not_found'
};
}
/**
* Internal logging helper to prefix logs with the operation ID.
* @param {string} operationId - The ID of the operation.
* @param {'info'|'warn'|'error'|'debug'} level - Log level.
* @param {string} message - Log message.
* @param {Object} [meta] - Additional metadata.
*/
log(operationId, level, message, meta = {}) {
const operation = this.operations.get(operationId);
// Use the logger instance associated with the operation if available, otherwise console
const logger = operation?.log || console;
const logFn = logger[level] || logger.log || console.log; // Fallback
logFn(`[AsyncOp ${operationId}] ${message}`, meta);
}
// --- Basic Event Emitter ---
on(eventName, listener) {
if (!this.listeners.has(eventName)) {
this.listeners.set(eventName, []);
}
this.listeners.get(eventName).push(listener);
}
emit(eventName, data) {
if (this.listeners.has(eventName)) {
this.listeners.get(eventName).forEach(listener => listener(data));
}
}
}
// Export a singleton instance
const asyncOperationManager = new AsyncOperationManager();
// Export the manager and potentially the class if needed elsewhere
export { asyncOperationManager, AsyncOperationManager };

View File

@@ -5,6 +5,7 @@ import { fileURLToPath } from "url";
import fs from "fs";
import logger from "./logger.js";
import { registerTaskMasterTools } from "./tools/index.js";
import { asyncOperationManager } from './core/utils/async-manager.js';
// Load environment variables
dotenv.config();
@@ -34,6 +35,9 @@ class TaskMasterMCPServer {
this.server.addResourceTemplate({});
// Make the manager accessible (e.g., pass it to tool registration)
this.asyncManager = asyncOperationManager;
// Bind methods
this.init = this.init.bind(this);
this.start = this.start.bind(this);
@@ -49,8 +53,8 @@ class TaskMasterMCPServer {
async init() {
if (this.initialized) return;
// Register Task Master tools
registerTaskMasterTools(this.server);
// Pass the manager instance to the tool registration function
registerTaskMasterTools(this.server, this.asyncManager);
this.initialized = true;
@@ -83,4 +87,7 @@ class TaskMasterMCPServer {
}
}
// Export the manager from here as well, if needed elsewhere
export { asyncOperationManager };
export default TaskMasterMCPServer;

View File

@@ -11,7 +11,7 @@ const LOG_LEVELS = {
// Get log level from environment or default to info
const LOG_LEVEL = process.env.LOG_LEVEL
? LOG_LEVELS[process.env.LOG_LEVEL.toLowerCase()]
? LOG_LEVELS[process.env.LOG_LEVEL.toLowerCase()] ?? LOG_LEVELS.info
: LOG_LEVELS.info;
/**
@@ -20,43 +20,66 @@ const LOG_LEVEL = process.env.LOG_LEVEL
* @param {...any} args - Arguments to log
*/
function log(level, ...args) {
const icons = {
debug: chalk.gray("🔍"),
info: chalk.blue(""),
warn: chalk.yellow("⚠️"),
error: chalk.red("❌"),
success: chalk.green(""),
// Use text prefixes instead of emojis
const prefixes = {
debug: chalk.gray("[DEBUG]"),
info: chalk.blue("[INFO]"),
warn: chalk.yellow("[WARN]"),
error: chalk.red("[ERROR]"),
success: chalk.green("[SUCCESS]"),
};
if (LOG_LEVELS[level] >= LOG_LEVEL) {
const icon = icons[level] || "";
if (LOG_LEVELS[level] !== undefined && LOG_LEVELS[level] >= LOG_LEVEL) {
const prefix = prefixes[level] || "";
let coloredArgs = args;
if (level === "error") {
console.error(icon, chalk.red(...args));
} else if (level === "warn") {
console.warn(icon, chalk.yellow(...args));
} else if (level === "success") {
console.log(icon, chalk.green(...args));
} else if (level === "info") {
console.log(icon, chalk.blue(...args));
} else {
console.log(icon, ...args);
try {
switch(level) {
case "error":
coloredArgs = args.map(arg => typeof arg === 'string' ? chalk.red(arg) : arg);
break;
case "warn":
coloredArgs = args.map(arg => typeof arg === 'string' ? chalk.yellow(arg) : arg);
break;
case "success":
coloredArgs = args.map(arg => typeof arg === 'string' ? chalk.green(arg) : arg);
break;
case "info":
coloredArgs = args.map(arg => typeof arg === 'string' ? chalk.blue(arg) : arg);
break;
case "debug":
coloredArgs = args.map(arg => typeof arg === 'string' ? chalk.gray(arg) : arg);
break;
// default: use original args (no color)
}
} catch (colorError) {
// Fallback if chalk fails on an argument
// Use console.error here for internal logger errors, separate from normal logging
console.error("Internal Logger Error applying chalk color:", colorError);
coloredArgs = args;
}
// Revert to console.log - FastMCP's context logger (context.log)
// is responsible for directing logs correctly (e.g., to stderr)
// during tool execution without upsetting the client connection.
// Logs outside of tool execution (like startup) will go to stdout.
console.log(prefix, ...coloredArgs);
}
}
/**
* Create a logger object with methods for different log levels
* Can be used as a drop-in replacement for existing logger initialization
* @returns {Object} Logger object with info, error, debug, warn, and success methods
*/
export function createLogger() {
const createLogMethod = (level) => (...args) => log(level, ...args);
return {
debug: (message) => log("debug", message),
info: (message) => log("info", message),
warn: (message) => log("warn", message),
error: (message) => log("error", message),
success: (message) => log("success", message),
debug: createLogMethod("debug"),
info: createLogMethod("info"),
warn: createLogMethod("warn"),
error: createLogMethod("error"),
success: createLogMethod("success"),
log: log, // Also expose the raw log function
};
}

View File

@@ -7,6 +7,7 @@ import { z } from "zod";
import {
handleApiResult,
createErrorResponse,
createContentResponse,
getProjectRootFromSession
} from "./utils.js";
import { addTaskDirect } from "../core/task-master-core.js";
@@ -14,11 +15,12 @@ import { addTaskDirect } from "../core/task-master-core.js";
/**
* Register the add-task tool with the MCP server
* @param {Object} server - FastMCP server instance
* @param {AsyncOperationManager} asyncManager - The async operation manager instance.
*/
export function registerAddTaskTool(server) {
export function registerAddTaskTool(server, asyncManager) {
server.addTool({
name: "add_task",
description: "Add a new task using AI",
description: "Starts adding a new task using AI in the background.",
parameters: z.object({
prompt: z.string().describe("Description of the task to add"),
dependencies: z.string().optional().describe("Comma-separated list of task IDs this task depends on"),
@@ -26,29 +28,38 @@ export function registerAddTaskTool(server) {
file: z.string().optional().describe("Path to the tasks file"),
projectRoot: z.string().optional().describe("Root directory of the project (default: current working directory)")
}),
execute: async (args, { log, reportProgress, session }) => {
execute: async (args, context) => {
const { log, reportProgress, session } = context;
try {
log.info(`MCP add_task called with prompt: "${args.prompt}"`);
log.info(`MCP add_task request received with prompt: \"${args.prompt}\"`);
if (!args.prompt) {
return createErrorResponse("Prompt is required for add_task.", "VALIDATION_ERROR");
}
// Get project root using the utility function
let rootFolder = getProjectRootFromSession(session, log);
// Fallback to args.projectRoot if session didn't provide one
if (!rootFolder && args.projectRoot) {
rootFolder = args.projectRoot;
log.info(`Using project root from args as fallback: ${rootFolder}`);
}
// Call the direct function with the resolved rootFolder
const result = await addTaskDirect({
projectRoot: rootFolder, // Pass the resolved root
const directArgs = {
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
};
const operationId = asyncManager.addOperation(addTaskDirect, directArgs, context);
log.info(`Started background operation for add_task. Operation ID: ${operationId}`);
return createContentResponse({
message: "Add task operation started successfully.",
operationId: operationId
});
return handleApiResult(result, log);
} catch (error) {
log.error(`Error in add_task MCP tool: ${error.message}`);
return createErrorResponse(error.message, "ADD_TASK_ERROR");
log.error(`Error initiating add_task operation: ${error.message}`, { stack: error.stack });
return createErrorResponse(`Failed to start add task operation: ${error.message}`, "ADD_TASK_INIT_ERROR");
}
}
});

View File

@@ -30,7 +30,7 @@ export function registerAnalyzeTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -42,9 +42,9 @@ export function registerAnalyzeTool(server) {
const result = await analyzeTaskComplexityDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Task complexity analysis complete: ${result.data.message}`);

View File

@@ -26,7 +26,7 @@ export function registerComplexityReportTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Getting complexity report with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -38,9 +38,9 @@ export function registerComplexityReportTool(server) {
const result = await complexityReportDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully retrieved complexity report${result.fromCache ? ' (from cache)' : ''}`);

View File

@@ -30,7 +30,7 @@ export function registerExpandAllTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Expanding all tasks with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -42,19 +42,19 @@ export function registerExpandAllTool(server) {
const result = await expandAllTasksDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`All tasks expanded successfully: ${result.data.message}`);
log.info(`Successfully expanded all tasks: ${result.data.message}`);
} else {
log.error(`Failed to expand tasks: ${result.error.message}`);
log.error(`Failed to expand all tasks: ${result.error?.message || 'Unknown error'}`);
}
return handleApiResult(result, log, 'Error expanding tasks');
return handleApiResult(result, log, 'Error expanding all tasks');
} catch (error) {
log.error(`Error in expandAll tool: ${error.message}`);
log.error(`Error in expand-all tool: ${error.message}`);
return createErrorResponse(error.message);
}
},

View File

@@ -36,7 +36,7 @@ export function registerExpandTaskTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Expanding task with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -48,20 +48,20 @@ export function registerExpandTaskTool(server) {
const result = await expandTaskDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully expanded task ID: ${args.id} with ${result.data.subtasksAdded} new subtasks${result.data.hasExistingSubtasks ? ' (appended to existing subtasks)' : ''}`);
log.info(`Successfully expanded task with ID ${args.id}`);
} else {
log.error(`Failed to expand task: ${result.error.message}`);
log.error(`Failed to expand task: ${result.error?.message || 'Unknown error'}`);
}
return handleApiResult(result, log, 'Error expanding task');
} catch (error) {
log.error(`Error in expand-task tool: ${error.message}`);
return createErrorResponse(`Failed to expand task: ${error.message}`);
log.error(`Error in expand task tool: ${error.message}`);
return createErrorResponse(error.message);
}
},
});

View File

@@ -32,7 +32,7 @@ export function registerGenerateTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Generating task files with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -44,14 +44,14 @@ export function registerGenerateTool(server) {
const result = await generateTaskFilesDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully generated task files: ${result.data.message}`);
} else {
log.error(`Failed to generate task files: ${result.error.message}`);
log.error(`Failed to generate task files: ${result.error?.message || 'Unknown error'}`);
}
return handleApiResult(result, log, 'Error generating task files');

View File

@@ -0,0 +1,42 @@
// mcp-server/src/tools/get-operation-status.js
import { z } from 'zod';
import { createErrorResponse, createContentResponse } from './utils.js'; // Assuming these utils exist
/**
* Register the get_operation_status tool.
* @param {FastMCP} server - FastMCP server instance.
* @param {AsyncOperationManager} asyncManager - The async operation manager.
*/
export function registerGetOperationStatusTool(server, asyncManager) {
server.addTool({
name: 'get_operation_status',
description: 'Retrieves the status and result/error of a background operation.',
parameters: z.object({
operationId: z.string().describe('The ID of the operation to check.'),
}),
execute: async (args, { log }) => {
try {
const { operationId } = args;
log.info(`Checking status for operation ID: ${operationId}`);
const status = asyncManager.getStatus(operationId);
// Status will now always return an object, but it might have status='not_found'
if (status.status === 'not_found') {
log.warn(`Operation ID not found: ${operationId}`);
return createErrorResponse(
status.error?.message || `Operation ID not found: ${operationId}`,
status.error?.code || 'OPERATION_NOT_FOUND'
);
}
log.info(`Status for ${operationId}: ${status.status}`);
return createContentResponse(status);
} catch (error) {
log.error(`Error in get_operation_status tool: ${error.message}`, { stack: error.stack });
return createErrorResponse(`Failed to get operation status: ${error.message}`, 'GET_STATUS_ERROR');
}
},
});
}

View File

@@ -36,7 +36,7 @@ export function registerListTasksTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -48,9 +48,9 @@ export function registerListTasksTool(server) {
const result = await listTasksDirect({
projectRoot: rootFolder,
...args
}, log);
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
log.info(`Retrieved ${result.success ? (result.data?.tasks?.length || 0) : 0} tasks${result.fromCache ? ' (from cache)' : ''}`);
return handleApiResult(result, log, 'Error getting tasks');

View File

@@ -27,12 +27,15 @@ import { registerComplexityReportTool } from "./complexity-report.js";
import { registerAddDependencyTool } from "./add-dependency.js";
import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { asyncOperationManager } from '../core/utils/async-manager.js';
import { registerGetOperationStatusTool } from './get-operation-status.js';
/**
* Register all Task Master tools with the MCP server
* @param {Object} server - FastMCP server instance
* @param {asyncOperationManager} asyncManager - The async operation manager instance
*/
export function registerTaskMasterTools(server) {
export function registerTaskMasterTools(server, asyncManager) {
try {
// Register each tool
registerListTasksTool(server);
@@ -45,7 +48,7 @@ export function registerTaskMasterTools(server) {
registerShowTaskTool(server);
registerNextTaskTool(server);
registerExpandTaskTool(server);
registerAddTaskTool(server);
registerAddTaskTool(server, asyncManager);
registerAddSubtaskTool(server);
registerRemoveSubtaskTool(server);
registerAnalyzeTool(server);
@@ -58,10 +61,13 @@ export function registerTaskMasterTools(server) {
registerAddDependencyTool(server);
registerRemoveTaskTool(server);
registerInitializeProjectTool(server);
registerGetOperationStatusTool(server, asyncManager);
} catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`);
throw error;
}
logger.info('Registered Task Master MCP tools');
}
export default {

View File

@@ -31,7 +31,7 @@ export function registerNextTaskTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Finding next task with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -43,24 +43,20 @@ export function registerNextTaskTool(server) {
const result = await nextTaskDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
if (result.data.nextTask) {
log.info(`Successfully found next task ID: ${result.data.nextTask.id}${result.fromCache ? ' (from cache)' : ''}`);
} else {
log.info(`No eligible next task found${result.fromCache ? ' (from cache)' : ''}`);
}
log.info(`Successfully found next task: ${result.data?.task?.id || 'No available tasks'}`);
} else {
log.error(`Failed to find next task: ${result.error.message}`);
log.error(`Failed to find next task: ${result.error?.message || 'Unknown error'}`);
}
return handleApiResult(result, log, 'Error finding next task');
} catch (error) {
log.error(`Error in next-task tool: ${error.message}`);
return createErrorResponse(`Failed to find next task: ${error.message}`);
log.error(`Error in nextTask tool: ${error.message}`);
return createErrorResponse(error.message);
}
},
});

View File

@@ -33,7 +33,7 @@ export function registerParsePRDTool(server) {
}),
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
log.info(`Parsing PRD with args: ${JSON.stringify(args)}`);
let rootFolder = getProjectRootFromSession(session, log);
@@ -45,19 +45,19 @@ export function registerParsePRDTool(server) {
const result = await parsePRDDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully generated ${result.data?.taskCount || 0} tasks from PRD at ${result.data?.outputPath}`);
log.info(`Successfully parsed PRD: ${result.data.message}`);
} else {
log.error(`Failed to parse PRD: ${result.error?.message || 'Unknown error'}`);
}
return handleApiResult(result, log, 'Error parsing PRD document');
return handleApiResult(result, log, 'Error parsing PRD');
} catch (error) {
log.error(`Error in parse_prd tool: ${error.message}`);
log.error(`Error in parse-prd tool: ${error.message}`);
return createErrorResponse(error.message);
}
},

View File

@@ -28,7 +28,7 @@ export function registerRemoveDependencyTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Removing dependency for task ${args.id} from ${args.dependsOn} with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -40,9 +40,9 @@ export function registerRemoveDependencyTool(server) {
const result = await removeDependencyDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully removed dependency: ${result.data.message}`);

View File

@@ -29,7 +29,7 @@ export function registerRemoveSubtaskTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Removing subtask with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -41,9 +41,9 @@ export function registerRemoveSubtaskTool(server) {
const result = await removeSubtaskDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Subtask removed successfully: ${result.data.message}`);

View File

@@ -37,7 +37,7 @@ export function registerSetTaskStatusTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Setting status of task(s) ${args.id} to: ${args.status}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -49,9 +49,9 @@ export function registerSetTaskStatusTool(server) {
const result = await setTaskStatusDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}`);

View File

@@ -34,7 +34,7 @@ export function registerUpdateSubtaskTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Updating subtask with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +46,9 @@ export function registerUpdateSubtaskTool(server) {
const result = await updateSubtaskByIdDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully updated subtask with ID ${args.id}`);

View File

@@ -34,7 +34,7 @@ export function registerUpdateTaskTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Updating task with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +46,9 @@ export function registerUpdateTaskTool(server) {
const result = await updateTaskByIdDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully updated task with ID ${args.id}`);

View File

@@ -34,7 +34,7 @@ export function registerUpdateTool(server) {
execute: async (args, { log, session, reportProgress }) => {
try {
log.info(`Updating tasks with args: ${JSON.stringify(args)}`);
await reportProgress({ progress: 0 });
// await reportProgress({ progress: 0 });
let rootFolder = getProjectRootFromSession(session, log);
@@ -46,9 +46,9 @@ export function registerUpdateTool(server) {
const result = await updateTasksDirect({
projectRoot: rootFolder,
...args
}, log, { reportProgress, mcpLog: log, session});
}, log/*, { reportProgress, mcpLog: log, session}*/);
await reportProgress({ progress: 100 });
// await reportProgress({ progress: 100 });
if (result.success) {
log.info(`Successfully updated tasks from ID ${args.from}: ${result.data.message}`);

16
package-lock.json generated
View File

@@ -26,7 +26,8 @@
"jsonwebtoken": "^9.0.2",
"lru-cache": "^10.2.0",
"openai": "^4.89.0",
"ora": "^8.2.0"
"ora": "^8.2.0",
"uuid": "^11.1.0"
},
"bin": {
"task-master": "bin/task-master.js",
@@ -7740,6 +7741,19 @@
"node": ">= 0.4.0"
}
},
"node_modules/uuid": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",

View File

@@ -54,7 +54,8 @@
"jsonwebtoken": "^9.0.2",
"lru-cache": "^10.2.0",
"openai": "^4.89.0",
"ora": "^8.2.0"
"ora": "^8.2.0",
"uuid": "^11.1.0"
},
"engines": {
"node": ">=14.0.0"

View File

@@ -20,7 +20,9 @@ import {
findTaskById,
readComplexityReport,
findTaskInComplexityReport,
truncate
truncate,
enableSilentMode,
disableSilentMode
} from './utils.js';
import {
@@ -102,7 +104,14 @@ async function parsePRD(prdPath, tasksPath, numTasks, { reportProgress, mcpLog,
log('info', `Tasks saved to: ${tasksPath}`);
// Generate individual task files
await generateTaskFiles(tasksPath, tasksDir, { reportProgress, mcpLog, session } = {});
if (reportProgress && mcpLog) {
// Enable silent mode when being called from MCP server
enableSilentMode();
await generateTaskFiles(tasksPath, tasksDir);
disableSilentMode();
} else {
await generateTaskFiles(tasksPath, tasksDir);
}
console.log(boxen(
chalk.green(`Successfully generated ${tasksData.tasks.length} tasks from PRD`),
@@ -762,6 +771,7 @@ Return only the updated task as a valid JSON object.`
function generateTaskFiles(tasksPath, outputDir) {
try {
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
@@ -2059,8 +2069,16 @@ function clearSubtasks(tasksPath, taskIds) {
* @param {Object} session - Session object from MCP server (optional)
* @returns {number} The new task ID
*/
async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium', { reportProgress, mcpLog, session } = {}) {
displayBanner();
async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium', { reportProgress, mcpLog, session } = {}, outputFormat = 'text') {
// Only display banner and UI elements for text output (CLI)
if (outputFormat === 'text') {
displayBanner();
console.log(boxen(
chalk.white.bold(`Creating New Task`),
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
}
// Read the existing tasks
const data = readJSON(tasksPath);
@@ -2073,10 +2091,13 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
const highestId = Math.max(...data.tasks.map(t => t.id));
const newTaskId = highestId + 1;
console.log(boxen(
chalk.white.bold(`Creating New Task #${newTaskId}`),
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
// Only show UI box for CLI mode
if (outputFormat === 'text') {
console.log(boxen(
chalk.white.bold(`Creating New Task #${newTaskId}`),
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
}
// Validate dependencies before proceeding
const invalidDeps = dependencies.filter(depId => {
@@ -2126,8 +2147,11 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
IMPORTANT: Return ONLY the JSON object, nothing else.`;
// Start the loading indicator
const loadingIndicator = startLoadingIndicator('Generating new task with Claude AI...');
// Start the loading indicator - only for text mode
let loadingIndicator = null;
if (outputFormat === 'text') {
loadingIndicator = startLoadingIndicator('Generating new task with Claude AI...');
}
let fullResponse = '';
let streamingInterval = null;
@@ -2143,13 +2167,15 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
stream: true
});
// Update loading indicator to show streaming progress
// Update loading indicator to show streaming progress - only for text mode
let dotCount = 0;
streamingInterval = setInterval(() => {
readline.cursorTo(process.stdout, 0);
process.stdout.write(`Receiving streaming response from Claude${'.'.repeat(dotCount)}`);
dotCount = (dotCount + 1) % 4;
}, 500);
if (outputFormat === 'text') {
streamingInterval = setInterval(() => {
readline.cursorTo(process.stdout, 0);
process.stdout.write(`Receiving streaming response from Claude${'.'.repeat(dotCount)}`);
dotCount = (dotCount + 1) % 4;
}, 500);
}
// Process the stream
for await (const chunk of stream) {
@@ -2166,7 +2192,7 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
}
if (streamingInterval) clearInterval(streamingInterval);
stopLoadingIndicator(loadingIndicator);
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
log('info', "Completed streaming response from Claude API!");
log('debug', `Streaming response length: ${fullResponse.length} characters`);
@@ -2213,28 +2239,31 @@ async function addTask(tasksPath, prompt, dependencies = [], priority = 'medium'
// Write the updated tasks back to the file
writeJSON(tasksPath, data);
// Show success message
const successBox = boxen(
chalk.green(`Successfully added new task #${newTaskId}:\n`) +
chalk.white.bold(newTask.title) + "\n\n" +
chalk.white(newTask.description),
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
);
console.log(successBox);
// Only show success messages for text mode (CLI)
if (outputFormat === 'text') {
// Show success message
const successBox = boxen(
chalk.green(`Successfully added new task #${newTaskId}:\n`) +
chalk.white.bold(newTask.title) + "\n\n" +
chalk.white(newTask.description),
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
);
console.log(successBox);
// Next steps suggestion
console.log(boxen(
chalk.white.bold('Next Steps:') + '\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master generate')} to update task files\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=' + newTaskId)} to break it down into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master list --with-subtasks')} to see all tasks`,
{ padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } }
));
// Next steps suggestion
console.log(boxen(
chalk.white.bold('Next Steps:') + '\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master generate')} to update task files\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=' + newTaskId)} to break it down into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master list --with-subtasks')} to see all tasks`,
{ padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } }
));
}
return newTaskId;
} catch (error) {
if (streamingInterval) clearInterval(streamingInterval);
stopLoadingIndicator(loadingIndicator);
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
log('error', "Error generating task:", error.message);
process.exit(1);
}

View File

@@ -20,6 +20,9 @@ const CONFIG = {
projectVersion: "1.5.0" // Hardcoded version - ALWAYS use this value, ignore environment variable
};
// Global silent mode flag
let silentMode = false;
// Set up logging based on log level
const LOG_LEVELS = {
debug: 0,
@@ -28,23 +31,51 @@ const LOG_LEVELS = {
error: 3
};
/**
* Enable silent logging mode
*/
function enableSilentMode() {
silentMode = true;
}
/**
* Disable silent logging mode
*/
function disableSilentMode() {
silentMode = false;
}
/**
* Check if silent mode is enabled
* @returns {boolean} True if silent mode is enabled
*/
function isSilentMode() {
return silentMode;
}
/**
* Logs a message at the specified level
* @param {string} level - The log level (debug, info, warn, error)
* @param {...any} args - Arguments to log
*/
function log(level, ...args) {
const icons = {
debug: chalk.gray('🔍'),
info: chalk.blue(''),
warn: chalk.yellow('⚠️'),
error: chalk.red('❌'),
success: chalk.green('✅')
// Skip logging if silent mode is enabled
if (silentMode) {
return;
}
// Use text prefixes instead of emojis
const prefixes = {
debug: chalk.gray("[DEBUG]"),
info: chalk.blue("[INFO]"),
warn: chalk.yellow("[WARN]"),
error: chalk.red("[ERROR]"),
success: chalk.green("[SUCCESS]")
};
if (LOG_LEVELS[level] >= LOG_LEVELS[CONFIG.logLevel]) {
const icon = icons[level] || '';
console.log(`${icon} ${args.join(' ')}`);
const prefix = prefixes[level] || "";
console.log(`${prefix} ${args.join(' ')}`);
}
}
@@ -337,5 +368,8 @@ export {
truncate,
findCycles,
toKebabCase,
detectCamelCaseFlags
detectCamelCaseFlags,
enableSilentMode,
disableSilentMode,
isSilentMode
};

46
tasks/task_043.txt Normal file
View File

@@ -0,0 +1,46 @@
# Task ID: 43
# Title: Add Research Flag to Add-Task Command
# Status: pending
# Dependencies: None
# Priority: medium
# Description: Implement a '--research' flag for the add-task command that enables users to automatically generate research-related subtasks when creating a new task.
# Details:
Modify the add-task command to accept a new optional flag '--research'. When this flag is provided, the system should automatically generate and attach a set of research-oriented subtasks to the newly created task. These subtasks should follow a standard research methodology structure:
1. Background Investigation: Research existing solutions and approaches
2. Requirements Analysis: Define specific requirements and constraints
3. Technology/Tool Evaluation: Compare potential technologies or tools for implementation
4. Proof of Concept: Create a minimal implementation to validate approach
5. Documentation: Document findings and recommendations
The implementation should:
- Update the command-line argument parser to recognize the new flag
- Create a dedicated function to generate the research subtasks with appropriate descriptions
- Ensure subtasks are properly linked to the parent task
- Update help documentation to explain the new flag
- Maintain backward compatibility with existing add-task functionality
The research subtasks should be customized based on the main task's title and description when possible, rather than using generic templates.
# Test Strategy:
Testing should verify both the functionality and usability of the new feature:
1. Unit tests:
- Test that the '--research' flag is properly parsed
- Verify the correct number and structure of subtasks are generated
- Ensure subtask IDs are correctly assigned and linked to the parent task
2. Integration tests:
- Create a task with the research flag and verify all subtasks appear in the task list
- Test that the research flag works with other existing flags (e.g., --priority, --depends-on)
- Verify the task and subtasks are properly saved to the storage backend
3. Manual testing:
- Run 'taskmaster add-task "Test task" --research' and verify the output
- Check that the help documentation correctly describes the new flag
- Verify the research subtasks have meaningful descriptions
- Test the command with and without the flag to ensure backward compatibility
4. Edge cases:
- Test with very short or very long task descriptions
- Verify behavior when maximum task/subtask limits are reached

50
tasks/task_044.txt Normal file
View File

@@ -0,0 +1,50 @@
# Task ID: 44
# Title: Implement Task Automation with Webhooks and Event Triggers
# Status: pending
# Dependencies: None
# Priority: medium
# Description: Design and implement a system that allows users to automate task actions through webhooks and event triggers, enabling integration with external services and automated workflows.
# Details:
This feature will enable users to create automated workflows based on task events and external triggers. Implementation should include:
1. A webhook registration system that allows users to specify URLs to be called when specific task events occur (creation, status change, completion, etc.)
2. An event system that captures and processes all task-related events
3. A trigger definition interface where users can define conditions for automation (e.g., 'When task X is completed, create task Y')
4. Support for both incoming webhooks (external services triggering actions in Taskmaster) and outgoing webhooks (Taskmaster notifying external services)
5. A secure authentication mechanism for webhook calls
6. Rate limiting and retry logic for failed webhook deliveries
7. Integration with the existing task management system
8. Command-line interface for managing webhooks and triggers
9. Payload templating system allowing users to customize the data sent in webhooks
10. Logging system for webhook activities and failures
The implementation should be compatible with both the solo/local mode and the multiplayer/remote mode, with appropriate adaptations for each context. When operating in MCP mode, the system should leverage the MCP communication protocol implemented in Task #42.
# Test Strategy:
Testing should verify both the functionality and security of the webhook system:
1. Unit tests:
- Test webhook registration, modification, and deletion
- Verify event capturing for all task operations
- Test payload generation and templating
- Validate authentication logic
2. Integration tests:
- Set up a mock server to receive webhooks and verify payload contents
- Test the complete flow from task event to webhook delivery
- Verify rate limiting and retry behavior with intentionally failing endpoints
- Test webhook triggers creating new tasks and modifying existing ones
3. Security tests:
- Verify that authentication tokens are properly validated
- Test for potential injection vulnerabilities in webhook payloads
- Verify that sensitive information is not leaked in webhook payloads
- Test rate limiting to prevent DoS attacks
4. Mode-specific tests:
- Verify correct operation in both solo/local and multiplayer/remote modes
- Test the interaction with MCP protocol when in multiplayer mode
5. Manual verification:
- Set up integrations with common services (GitHub, Slack, etc.) to verify real-world functionality
- Verify that the CLI interface for managing webhooks works as expected

55
tasks/task_045.txt Normal file
View File

@@ -0,0 +1,55 @@
# Task ID: 45
# Title: Implement GitHub Issue Import Feature
# Status: pending
# Dependencies: None
# Priority: medium
# Description: Add a '--from-github' flag to the add-task command that accepts a GitHub issue URL and automatically generates a corresponding task with relevant details.
# Details:
Implement a new flag '--from-github' for the add-task command that allows users to create tasks directly from GitHub issues. The implementation should:
1. Accept a GitHub issue URL as an argument (e.g., 'taskmaster add-task --from-github https://github.com/owner/repo/issues/123')
2. Parse the URL to extract the repository owner, name, and issue number
3. Use the GitHub API to fetch the issue details including:
- Issue title (to be used as task title)
- Issue description (to be used as task description)
- Issue labels (to be potentially used as tags)
- Issue assignees (for reference)
- Issue status (open/closed)
4. Generate a well-formatted task with this information
5. Include a reference link back to the original GitHub issue
6. Handle authentication for private repositories using GitHub tokens from environment variables or config file
7. Implement proper error handling for:
- Invalid URLs
- Non-existent issues
- API rate limiting
- Authentication failures
- Network issues
8. Allow users to override or supplement the imported details with additional command-line arguments
9. Add appropriate documentation in help text and user guide
# Test Strategy:
Testing should cover the following scenarios:
1. Unit tests:
- Test URL parsing functionality with valid and invalid GitHub issue URLs
- Test GitHub API response parsing with mocked API responses
- Test error handling for various failure cases
2. Integration tests:
- Test with real GitHub public issues (use well-known repositories)
- Test with both open and closed issues
- Test with issues containing various elements (labels, assignees, comments)
3. Error case tests:
- Invalid URL format
- Non-existent repository
- Non-existent issue number
- API rate limit exceeded
- Authentication failures for private repos
4. End-to-end tests:
- Verify that a task created from a GitHub issue contains all expected information
- Verify that the task can be properly managed after creation
- Test the interaction with other flags and commands
Create mock GitHub API responses for testing to avoid hitting rate limits during development and testing. Use environment variables to configure test credentials if needed.

View File

@@ -2449,6 +2449,26 @@
"priority": "medium",
"details": "Modify the add-task command to accept a new optional flag '--research'. When this flag is provided, the system should automatically generate and attach a set of research-oriented subtasks to the newly created task. These subtasks should follow a standard research methodology structure:\n\n1. Background Investigation: Research existing solutions and approaches\n2. Requirements Analysis: Define specific requirements and constraints\n3. Technology/Tool Evaluation: Compare potential technologies or tools for implementation\n4. Proof of Concept: Create a minimal implementation to validate approach\n5. Documentation: Document findings and recommendations\n\nThe implementation should:\n- Update the command-line argument parser to recognize the new flag\n- Create a dedicated function to generate the research subtasks with appropriate descriptions\n- Ensure subtasks are properly linked to the parent task\n- Update help documentation to explain the new flag\n- Maintain backward compatibility with existing add-task functionality\n\nThe research subtasks should be customized based on the main task's title and description when possible, rather than using generic templates.",
"testStrategy": "Testing should verify both the functionality and usability of the new feature:\n\n1. Unit tests:\n - Test that the '--research' flag is properly parsed\n - Verify the correct number and structure of subtasks are generated\n - Ensure subtask IDs are correctly assigned and linked to the parent task\n\n2. Integration tests:\n - Create a task with the research flag and verify all subtasks appear in the task list\n - Test that the research flag works with other existing flags (e.g., --priority, --depends-on)\n - Verify the task and subtasks are properly saved to the storage backend\n\n3. Manual testing:\n - Run 'taskmaster add-task \"Test task\" --research' and verify the output\n - Check that the help documentation correctly describes the new flag\n - Verify the research subtasks have meaningful descriptions\n - Test the command with and without the flag to ensure backward compatibility\n\n4. Edge cases:\n - Test with very short or very long task descriptions\n - Verify behavior when maximum task/subtask limits are reached"
},
{
"id": 44,
"title": "Implement Task Automation with Webhooks and Event Triggers",
"description": "Design and implement a system that allows users to automate task actions through webhooks and event triggers, enabling integration with external services and automated workflows.",
"status": "pending",
"dependencies": [],
"priority": "medium",
"details": "This feature will enable users to create automated workflows based on task events and external triggers. Implementation should include:\n\n1. A webhook registration system that allows users to specify URLs to be called when specific task events occur (creation, status change, completion, etc.)\n2. An event system that captures and processes all task-related events\n3. A trigger definition interface where users can define conditions for automation (e.g., 'When task X is completed, create task Y')\n4. Support for both incoming webhooks (external services triggering actions in Taskmaster) and outgoing webhooks (Taskmaster notifying external services)\n5. A secure authentication mechanism for webhook calls\n6. Rate limiting and retry logic for failed webhook deliveries\n7. Integration with the existing task management system\n8. Command-line interface for managing webhooks and triggers\n9. Payload templating system allowing users to customize the data sent in webhooks\n10. Logging system for webhook activities and failures\n\nThe implementation should be compatible with both the solo/local mode and the multiplayer/remote mode, with appropriate adaptations for each context. When operating in MCP mode, the system should leverage the MCP communication protocol implemented in Task #42.",
"testStrategy": "Testing should verify both the functionality and security of the webhook system:\n\n1. Unit tests:\n - Test webhook registration, modification, and deletion\n - Verify event capturing for all task operations\n - Test payload generation and templating\n - Validate authentication logic\n\n2. Integration tests:\n - Set up a mock server to receive webhooks and verify payload contents\n - Test the complete flow from task event to webhook delivery\n - Verify rate limiting and retry behavior with intentionally failing endpoints\n - Test webhook triggers creating new tasks and modifying existing ones\n\n3. Security tests:\n - Verify that authentication tokens are properly validated\n - Test for potential injection vulnerabilities in webhook payloads\n - Verify that sensitive information is not leaked in webhook payloads\n - Test rate limiting to prevent DoS attacks\n\n4. Mode-specific tests:\n - Verify correct operation in both solo/local and multiplayer/remote modes\n - Test the interaction with MCP protocol when in multiplayer mode\n\n5. Manual verification:\n - Set up integrations with common services (GitHub, Slack, etc.) to verify real-world functionality\n - Verify that the CLI interface for managing webhooks works as expected"
},
{
"id": 45,
"title": "Implement GitHub Issue Import Feature",
"description": "Add a '--from-github' flag to the add-task command that accepts a GitHub issue URL and automatically generates a corresponding task with relevant details.",
"status": "pending",
"dependencies": [],
"priority": "medium",
"details": "Implement a new flag '--from-github' for the add-task command that allows users to create tasks directly from GitHub issues. The implementation should:\n\n1. Accept a GitHub issue URL as an argument (e.g., 'taskmaster add-task --from-github https://github.com/owner/repo/issues/123')\n2. Parse the URL to extract the repository owner, name, and issue number\n3. Use the GitHub API to fetch the issue details including:\n - Issue title (to be used as task title)\n - Issue description (to be used as task description)\n - Issue labels (to be potentially used as tags)\n - Issue assignees (for reference)\n - Issue status (open/closed)\n4. Generate a well-formatted task with this information\n5. Include a reference link back to the original GitHub issue\n6. Handle authentication for private repositories using GitHub tokens from environment variables or config file\n7. Implement proper error handling for:\n - Invalid URLs\n - Non-existent issues\n - API rate limiting\n - Authentication failures\n - Network issues\n8. Allow users to override or supplement the imported details with additional command-line arguments\n9. Add appropriate documentation in help text and user guide",
"testStrategy": "Testing should cover the following scenarios:\n\n1. Unit tests:\n - Test URL parsing functionality with valid and invalid GitHub issue URLs\n - Test GitHub API response parsing with mocked API responses\n - Test error handling for various failure cases\n\n2. Integration tests:\n - Test with real GitHub public issues (use well-known repositories)\n - Test with both open and closed issues\n - Test with issues containing various elements (labels, assignees, comments)\n\n3. Error case tests:\n - Invalid URL format\n - Non-existent repository\n - Non-existent issue number\n - API rate limit exceeded\n - Authentication failures for private repos\n\n4. End-to-end tests:\n - Verify that a task created from a GitHub issue contains all expected information\n - Verify that the task can be properly managed after creation\n - Test the interaction with other flags and commands\n\nCreate mock GitHub API responses for testing to avoid hitting rate limits during development and testing. Use environment variables to configure test credentials if needed."
}
]
}