feat(research): Adds MCP tool for command

- New MCP Tool: research tool enables AI-powered research with project context
- Context Integration: Supports task IDs, file paths, custom context, and project tree
- Fuzzy Task Discovery: Automatically finds relevant tasks using semantic search
- Token Management: Detailed token counting and breakdown by context type
- Multiple Detail Levels: Support for low, medium, and high detail research responses
- Telemetry Integration: Full cost tracking and usage analytics
- Direct Function: researchDirect with comprehensive parameter validation
- Silent Mode: Prevents console output interference with MCP JSON responses
- Error Handling: Robust error handling with proper MCP response formatting

This completes subtasks 94.5 (Direct Function) and 94.6 (MCP Tool) for the research command implementation, providing a powerful research interface for integrated development environments like Cursor.

Updated documentation across taskmaster.mdc, README.md, command-reference.md, examples.md, tutorial.md, and docs/README.md to highlight research capabilities and usage patterns.
This commit is contained in:
Eyal Toledano
2025-05-25 20:16:48 -04:00
parent 1e020023ed
commit 023f51c579
11 changed files with 454 additions and 3 deletions

View File

@@ -0,0 +1,159 @@
/**
* research.js
* Direct function implementation for AI-powered research queries
*/
import { performResearch } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for performing AI-powered research with project context.
*
* @param {Object} args - Command arguments
* @param {string} args.query - Research query/prompt (required)
* @param {string} [args.taskIds] - Comma-separated list of task/subtask IDs for context
* @param {string} [args.filePaths] - Comma-separated list of file paths for context
* @param {string} [args.customContext] - Additional custom context text
* @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
* @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
* @param {string} [args.projectRoot] - Project root path
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function researchDirect(args, log, context = {}) {
// Destructure expected args
const {
query,
taskIds,
filePaths,
customContext,
includeProjectTree = false,
detailLevel = 'medium',
projectRoot
} = args;
const { session } = context; // Destructure session from context
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check required parameters
if (!query || typeof query !== 'string' || query.trim().length === 0) {
log.error('Missing or invalid required parameter: query');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message:
'The query parameter is required and must be a non-empty string'
}
};
}
// Parse comma-separated task IDs if provided
const parsedTaskIds = taskIds
? taskIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
: [];
// Parse comma-separated file paths if provided
const parsedFilePaths = filePaths
? filePaths
.split(',')
.map((path) => path.trim())
.filter((path) => path.length > 0)
: [];
// Validate detail level
const validDetailLevels = ['low', 'medium', 'high'];
if (!validDetailLevels.includes(detailLevel)) {
log.error(`Invalid detail level: ${detailLevel}`);
disableSilentMode();
return {
success: false,
error: {
code: 'INVALID_PARAMETER',
message: `Detail level must be one of: ${validDetailLevels.join(', ')}`
}
};
}
log.info(
`Performing research query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}", ` +
`taskIds: [${parsedTaskIds.join(', ')}], ` +
`filePaths: [${parsedFilePaths.join(', ')}], ` +
`detailLevel: ${detailLevel}, ` +
`includeProjectTree: ${includeProjectTree}, ` +
`projectRoot: ${projectRoot}`
);
// Prepare options for the research function
const researchOptions = {
taskIds: parsedTaskIds,
filePaths: parsedFilePaths,
customContext: customContext || '',
includeProjectTree,
detailLevel,
projectRoot
};
// Prepare context for the research function
const researchContext = {
session,
mcpLog,
commandName: 'research',
outputType: 'mcp'
};
// Call the performResearch function
const result = await performResearch(
query.trim(),
researchOptions,
researchContext,
'json', // outputFormat - use 'json' to suppress CLI UI
false // allowFollowUp - disable for MCP calls
);
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
query: result.query,
result: result.result,
contextSize: result.contextSize,
contextTokens: result.contextTokens,
tokenBreakdown: result.tokenBreakdown,
systemPromptTokens: result.systemPromptTokens,
userPromptTokens: result.userPromptTokens,
totalInputTokens: result.totalInputTokens,
detailLevel: result.detailLevel,
telemetryData: result.telemetryData
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in researchDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'RESEARCH_ERROR',
message: error.message
}
};
}
}

View File

@@ -31,6 +31,7 @@ import { removeTaskDirect } from './direct-functions/remove-task.js';
import { initializeProjectDirect } from './direct-functions/initialize-project.js';
import { modelsDirect } from './direct-functions/models.js';
import { moveTaskDirect } from './direct-functions/move-task.js';
import { researchDirect } from './direct-functions/research.js';
// Re-export utility functions
export { findTasksJsonPath } from './utils/path-utils.js';
@@ -62,7 +63,8 @@ export const directFunctions = new Map([
['removeTaskDirect', removeTaskDirect],
['initializeProjectDirect', initializeProjectDirect],
['modelsDirect', modelsDirect],
['moveTaskDirect', moveTaskDirect]
['moveTaskDirect', moveTaskDirect],
['researchDirect', researchDirect]
]);
// Re-export all direct function implementations
@@ -92,5 +94,6 @@ export {
removeTaskDirect,
initializeProjectDirect,
modelsDirect,
moveTaskDirect
moveTaskDirect,
researchDirect
};

View File

@@ -29,6 +29,7 @@ import { registerRemoveTaskTool } from './remove-task.js';
import { registerInitializeProjectTool } from './initialize-project.js';
import { registerModelsTool } from './models.js';
import { registerMoveTaskTool } from './move-task.js';
import { registerResearchTool } from './research.js';
/**
* Register all Task Master tools with the MCP server
@@ -74,6 +75,9 @@ export function registerTaskMasterTools(server) {
registerRemoveDependencyTool(server);
registerValidateDependenciesTool(server);
registerFixDependenciesTool(server);
// Group 7: AI-Powered Features
registerResearchTool(server);
} catch (error) {
logger.error(`Error registering Task Master tools: ${error.message}`);
throw error;

View File

@@ -0,0 +1,82 @@
/**
* tools/research.js
* Tool to perform AI-powered research queries with project context
*/
import { z } from 'zod';
import {
createErrorResponse,
handleApiResult,
withNormalizedProjectRoot
} from './utils.js';
import { researchDirect } from '../core/task-master-core.js';
/**
* Register the research tool with the MCP server
* @param {Object} server - FastMCP server instance
*/
export function registerResearchTool(server) {
server.addTool({
name: 'research',
description: 'Perform AI-powered research queries with project context',
parameters: z.object({
query: z.string().describe('Research query/prompt (required)'),
taskIds: z
.string()
.optional()
.describe(
'Comma-separated list of task/subtask IDs for context (e.g., "15,16.2,17")'
),
filePaths: z
.string()
.optional()
.describe(
'Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md")'
),
customContext: z
.string()
.optional()
.describe('Additional custom context text to include in the research'),
includeProjectTree: z
.boolean()
.optional()
.describe(
'Include project file tree structure in context (default: false)'
),
detailLevel: z
.enum(['low', 'medium', 'high'])
.optional()
.describe('Detail level for the research response (default: medium)'),
projectRoot: z
.string()
.describe('The directory of the project. Must be an absolute path.')
}),
execute: withNormalizedProjectRoot(async (args, { log, session }) => {
try {
log.info(
`Starting research with query: "${args.query.substring(0, 100)}${args.query.length > 100 ? '...' : ''}"`
);
// Call the direct function
const result = await researchDirect(
{
query: args.query,
taskIds: args.taskIds,
filePaths: args.filePaths,
customContext: args.customContext,
includeProjectTree: args.includeProjectTree || false,
detailLevel: args.detailLevel || 'medium',
projectRoot: args.projectRoot
},
log,
{ session }
);
return handleApiResult(result, log);
} catch (error) {
log.error(`Error in research tool: ${error.message}`);
return createErrorResponse(error.message);
}
})
});
}