mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
feat: connect get-task and get-tasks to remote (#1346)
This commit is contained in:
@@ -4,5 +4,6 @@
|
||||
*/
|
||||
|
||||
export * from './tools/autopilot/index.js';
|
||||
export * from './tools/tasks/index.js';
|
||||
export * from './shared/utils.js';
|
||||
export * from './shared/types.js';
|
||||
|
||||
@@ -45,14 +45,27 @@ export async function handleApiResult<T>(options: {
|
||||
log?: any;
|
||||
errorPrefix?: string;
|
||||
projectRoot?: string;
|
||||
tag?: string; // Optional tag/brief to use instead of reading from state.json
|
||||
}): Promise<ContentResult> {
|
||||
const { result, log, errorPrefix = 'API error', projectRoot } = options;
|
||||
const {
|
||||
result,
|
||||
log,
|
||||
errorPrefix = 'API error',
|
||||
projectRoot,
|
||||
tag: providedTag
|
||||
} = options;
|
||||
|
||||
// Get version info for every response
|
||||
const versionInfo = getVersionInfo();
|
||||
|
||||
// Get current tag if project root is provided
|
||||
const currentTag = projectRoot ? getCurrentTag(projectRoot) : null;
|
||||
// Use provided tag if available, otherwise get from state.json
|
||||
// Note: For API storage, tm-core returns the brief name as the tag
|
||||
const currentTag =
|
||||
providedTag !== undefined
|
||||
? providedTag
|
||||
: projectRoot
|
||||
? getCurrentTag(projectRoot)
|
||||
: null;
|
||||
|
||||
if (!result.success) {
|
||||
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
|
||||
|
||||
136
apps/mcp/src/tools/tasks/get-task.tool.ts
Normal file
136
apps/mcp/src/tools/tasks/get-task.tool.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* @fileoverview get-task MCP tool
|
||||
* Get detailed information about a specific task by ID
|
||||
*/
|
||||
|
||||
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
|
||||
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
|
||||
import { z } from 'zod/v3';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, type Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTaskSchema = z.object({
|
||||
id: z
|
||||
.string()
|
||||
.describe(
|
||||
'Task ID(s) to get (can be comma-separated for multiple tasks)'
|
||||
),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Filter subtasks by status (e.g., 'pending', 'done')"),
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe(
|
||||
'Absolute path to the project root directory (Optional, usually from session)'
|
||||
),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
});
|
||||
|
||||
type GetTaskArgs = z.infer<typeof GetTaskSchema>;
|
||||
|
||||
/**
|
||||
* Register the get_task tool with the MCP server
|
||||
*/
|
||||
export function registerGetTaskTool(server: FastMCP) {
|
||||
server.addTool({
|
||||
name: 'get_task',
|
||||
description: 'Get detailed information about a specific task',
|
||||
parameters: GetTaskSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: GetTaskArgs, context: MCPContext) => {
|
||||
const { id, status, projectRoot, tag } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
`Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}`
|
||||
);
|
||||
|
||||
// Create tm-core with logging callback
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: projectRoot,
|
||||
loggerConfig: {
|
||||
mcpMode: true,
|
||||
logCallback: context.log
|
||||
}
|
||||
});
|
||||
|
||||
// Handle comma-separated IDs - parallelize for better performance
|
||||
const taskIds = id.split(',').map((tid) => tid.trim());
|
||||
const results = await Promise.all(
|
||||
taskIds.map((taskId) => tmCore.tasks.get(taskId, tag))
|
||||
);
|
||||
|
||||
const tasks: Task[] = [];
|
||||
for (const result of results) {
|
||||
if (!result.task) continue;
|
||||
|
||||
// If status filter is provided, filter subtasks (create copy to avoid mutation)
|
||||
if (status && result.task.subtasks) {
|
||||
const statusFilters = status
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase());
|
||||
const filteredSubtasks = result.task.subtasks.filter((st) =>
|
||||
statusFilters.includes(String(st.status).toLowerCase())
|
||||
);
|
||||
tasks.push({ ...result.task, subtasks: filteredSubtasks });
|
||||
} else {
|
||||
tasks.push(result.task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.length === 0) {
|
||||
context.log.warn(`No tasks found for ID(s): ${id}`);
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: {
|
||||
message: `No tasks found for ID(s): ${id}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
|
||||
context.log.info(
|
||||
`Successfully retrieved ${tasks.length} task(s) for ID(s): ${id}`
|
||||
);
|
||||
|
||||
// Return single task if only one ID was requested, otherwise array
|
||||
const responseData = taskIds.length === 1 ? tasks[0] : tasks;
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: responseData
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot,
|
||||
tag
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in get-task: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Failed to get task: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
166
apps/mcp/src/tools/tasks/get-tasks.tool.ts
Normal file
166
apps/mcp/src/tools/tasks/get-tasks.tool.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/**
|
||||
* @fileoverview get-tasks MCP tool
|
||||
* Get all tasks from Task Master with optional filtering
|
||||
*/
|
||||
|
||||
// TEMPORARY: Using zod/v3 for Draft-07 JSON Schema compatibility with FastMCP's zod-to-json-schema
|
||||
// TODO: Revert to 'zod' when MCP spec issue is resolved (see PR #1323)
|
||||
import { z } from 'zod/v3';
|
||||
import {
|
||||
handleApiResult,
|
||||
withNormalizedProjectRoot
|
||||
} from '../../shared/utils.js';
|
||||
import type { MCPContext } from '../../shared/types.js';
|
||||
import { createTmCore, type TaskStatus, type Task } from '@tm/core';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GetTasksSchema = z.object({
|
||||
projectRoot: z
|
||||
.string()
|
||||
.describe('The directory of the project. Must be an absolute path.'),
|
||||
status: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
"Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
|
||||
),
|
||||
withSubtasks: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe('Include subtasks nested within their parent tasks in the response'),
|
||||
tag: z.string().optional().describe('Tag context to operate on')
|
||||
});
|
||||
|
||||
type GetTasksArgs = z.infer<typeof GetTasksSchema>;
|
||||
|
||||
/**
|
||||
* Register the get_tasks tool with the MCP server
|
||||
*/
|
||||
export function registerGetTasksTool(server: FastMCP) {
|
||||
server.addTool({
|
||||
name: 'get_tasks',
|
||||
description:
|
||||
'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
|
||||
parameters: GetTasksSchema,
|
||||
execute: withNormalizedProjectRoot(
|
||||
async (args: GetTasksArgs, context: MCPContext) => {
|
||||
const { projectRoot, status, withSubtasks, tag } = args;
|
||||
|
||||
try {
|
||||
context.log.info(
|
||||
`Getting tasks from ${projectRoot}${status ? ` with status filter: ${status}` : ''}${tag ? ` for tag: ${tag}` : ''}`
|
||||
);
|
||||
|
||||
// Create tm-core with logging callback
|
||||
const tmCore = await createTmCore({
|
||||
projectPath: projectRoot,
|
||||
loggerConfig: {
|
||||
mcpMode: true,
|
||||
logCallback: context.log
|
||||
}
|
||||
});
|
||||
|
||||
// Build filter
|
||||
const filter =
|
||||
status && status !== 'all'
|
||||
? {
|
||||
status: status
|
||||
.split(',')
|
||||
.map((s: string) => s.trim() as TaskStatus)
|
||||
}
|
||||
: undefined;
|
||||
|
||||
// Call tm-core tasks.list()
|
||||
const result = await tmCore.tasks.list({
|
||||
tag,
|
||||
filter,
|
||||
includeSubtasks: withSubtasks
|
||||
});
|
||||
|
||||
context.log.info(
|
||||
`Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)`
|
||||
);
|
||||
|
||||
// Calculate stats using reduce for cleaner code
|
||||
const totalTasks = result.total;
|
||||
const taskCounts = result.tasks.reduce(
|
||||
(acc, task) => {
|
||||
acc[task.status] = (acc[task.status] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>
|
||||
);
|
||||
|
||||
const completionPercentage =
|
||||
totalTasks > 0 ? ((taskCounts.done || 0) / totalTasks) * 100 : 0;
|
||||
|
||||
// Count subtasks using reduce
|
||||
const subtaskCounts = result.tasks.reduce(
|
||||
(acc, task) => {
|
||||
task.subtasks?.forEach((st) => {
|
||||
acc.total++;
|
||||
acc[st.status] = (acc[st.status] || 0) + 1;
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{ total: 0 } as Record<string, number>
|
||||
);
|
||||
|
||||
const subtaskCompletionPercentage =
|
||||
subtaskCounts.total > 0
|
||||
? ((subtaskCounts.done || 0) / subtaskCounts.total) * 100
|
||||
: 0;
|
||||
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: true,
|
||||
data: {
|
||||
tasks: result.tasks as Task[],
|
||||
filter: status || 'all',
|
||||
stats: {
|
||||
total: totalTasks,
|
||||
completed: taskCounts.done || 0,
|
||||
inProgress: taskCounts['in-progress'] || 0,
|
||||
pending: taskCounts.pending || 0,
|
||||
blocked: taskCounts.blocked || 0,
|
||||
deferred: taskCounts.deferred || 0,
|
||||
cancelled: taskCounts.cancelled || 0,
|
||||
review: taskCounts.review || 0,
|
||||
completionPercentage,
|
||||
subtasks: {
|
||||
total: subtaskCounts.total,
|
||||
completed: subtaskCounts.done || 0,
|
||||
inProgress: subtaskCounts['in-progress'] || 0,
|
||||
pending: subtaskCounts.pending || 0,
|
||||
blocked: subtaskCounts.blocked || 0,
|
||||
deferred: subtaskCounts.deferred || 0,
|
||||
cancelled: subtaskCounts.cancelled || 0,
|
||||
completionPercentage: subtaskCompletionPercentage
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot,
|
||||
tag: result.tag
|
||||
});
|
||||
} catch (error: any) {
|
||||
context.log.error(`Error in get-tasks: ${error.message}`);
|
||||
if (error.stack) {
|
||||
context.log.debug(error.stack);
|
||||
}
|
||||
return handleApiResult({
|
||||
result: {
|
||||
success: false,
|
||||
error: {
|
||||
message: `Failed to get tasks: ${error.message}`
|
||||
}
|
||||
},
|
||||
log: context.log,
|
||||
projectRoot
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
});
|
||||
}
|
||||
7
apps/mcp/src/tools/tasks/index.ts
Normal file
7
apps/mcp/src/tools/tasks/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @fileoverview Tasks MCP tools index
|
||||
* Exports all task-related tool registration functions
|
||||
*/
|
||||
|
||||
export { registerGetTasksTool } from './get-tasks.tool.js';
|
||||
export { registerGetTaskTool } from './get-task.tool.js';
|
||||
Reference in New Issue
Block a user