feat: connect get-task and get-tasks to remote (#1346)

This commit is contained in:
Ralph Khreish
2025-10-27 15:23:27 +01:00
committed by GitHub
parent 5381e21e57
commit 25addf919f
20 changed files with 937 additions and 589 deletions

View File

@@ -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';

View File

@@ -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}`;

View 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
});
}
}
)
});
}

View 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
});
}
}
)
});
}

View 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';