/** * Task tools — ops tier, scope: ops:tasks * * All four tools (create_task, list_tasks, complete_task, delete_task) are * scoped to ops:tasks and operate on the calling user's tasks only (ABAC: * partition key = ctx.sub). The injected TasksClient is the only I/O path; * no AWS SDK or network call is made directly here. * * Finance-tier note: this is an ops-tier package. No finance fields are * present, so redact() is not called here. If this package is ever promoted * or a finance field is added, every sensitive field MUST be wrapped in * redact() before it is included in the tool output. */ import { defineTool, requireScope, type AuthContext } from '@sh-mcp/shared'; import type { TasksClient } from './client.js'; // --------------------------------------------------------------------------- // Tool factory — accepts an injected TasksClient so tests can pass a mock. // The MCP server entry point calls buildTaskTools(new DynamoDBTasksClient()). // --------------------------------------------------------------------------- export function buildTaskTools(client: TasksClient) { // ------------------------------------------------------------------------- // create_task // ------------------------------------------------------------------------- const createTask = defineTool< { title: string; description?: string }, { task: { taskId: string; title: string; description?: string; completed: boolean; createdAt: string } } >({ name: 'create_task', description: 'Create a new task for the calling user. Tasks are private — only the user who created a task can see or modify it.', tier: 'ops', requiredScope: 'ops:tasks', inputSchema: { type: 'object', properties: { title: { type: 'string', minLength: 1, maxLength: 256, description: 'Short title for the task (required).', }, description: { type: 'string', maxLength: 2048, description: 'Optional longer description or notes for the task.', }, }, required: ['title'], additionalProperties: false, }, handler: async (input, ctx: AuthContext) => { requireScope(ctx, 'ops:tasks'); const task = await client.createTask({ sub: ctx.sub, title: input.title, description: input.description, }); return { task: { taskId: task.taskId, title: task.title, description: task.description, completed: task.completed, createdAt: task.createdAt, }, }; }, }); // ------------------------------------------------------------------------- // list_tasks // ------------------------------------------------------------------------- const listTasks = defineTool< { includeCompleted?: boolean }, { tasks: Array<{ taskId: string; title: string; description?: string; completed: boolean; createdAt: string; completedAt?: string }> } >({ name: 'list_tasks', description: 'List tasks belonging to the calling user. By default only incomplete tasks are returned; pass includeCompleted: true to see all.', tier: 'ops', requiredScope: 'ops:tasks', inputSchema: { type: 'object', properties: { includeCompleted: { type: 'boolean', description: 'When true, completed tasks are included in the results. Defaults to false.', }, }, additionalProperties: false, }, handler: async (input, ctx: AuthContext) => { requireScope(ctx, 'ops:tasks'); const tasks = await client.listTasks({ sub: ctx.sub, includeCompleted: input.includeCompleted ?? false, }); return { tasks: tasks.map((t) => ({ taskId: t.taskId, title: t.title, description: t.description, completed: t.completed, createdAt: t.createdAt, completedAt: t.completedAt, })), }; }, }); // ------------------------------------------------------------------------- // complete_task // ------------------------------------------------------------------------- const completeTask = defineTool< { taskId: string }, { task: { taskId: string; title: string; completed: boolean; completedAt: string } } >({ name: 'complete_task', description: "Mark a task as completed. The task must belong to the calling user; completing another user's task is not permitted.", tier: 'ops', requiredScope: 'ops:tasks', inputSchema: { type: 'object', properties: { taskId: { type: 'string', minLength: 1, description: 'The ID of the task to mark as completed.', }, }, required: ['taskId'], additionalProperties: false, }, handler: async (input, ctx: AuthContext) => { requireScope(ctx, 'ops:tasks'); const task = await client.completeTask({ sub: ctx.sub, taskId: input.taskId, }); return { task: { taskId: task.taskId, title: task.title, completed: task.completed, completedAt: task.completedAt as string, }, }; }, }); // ------------------------------------------------------------------------- // delete_task // ------------------------------------------------------------------------- const deleteTask = defineTool<{ taskId: string }, { deleted: true; taskId: string }>({ name: 'delete_task', description: 'Permanently delete a task belonging to the calling user. This action is irreversible.', tier: 'ops', requiredScope: 'ops:tasks', inputSchema: { type: 'object', properties: { taskId: { type: 'string', minLength: 1, description: 'The ID of the task to delete.', }, }, required: ['taskId'], additionalProperties: false, }, handler: async (input, ctx: AuthContext) => { requireScope(ctx, 'ops:tasks'); await client.deleteTask({ sub: ctx.sub, taskId: input.taskId, }); return { deleted: true, taskId: input.taskId }; }, }); return [createTask, listTasks, completeTask, deleteTask] as const; }