Thirty new tools (54 in all) for what the API gained since the MCP: create a project and read its public showcase; sprints (list, progress and burndown, plan, change, start, close with carry-over, delete, move a ticket in or out); epics and the backlog (list epics with progress, set a ticket's parent, rank); bulk edit of up to 100 tickets by key, saying which refused; the trash; checklists; following a ticket, removing a relation, editing and deleting comments; editing and archiving releases, drafting and saving their notes; notifications. Searching filters by epic, sprint and dates and sorts by backlog order; get_issue shows parent, checklist, commits and followers; create and update take parent and start date. API: `unreadOnly` and `dryRun` were documented as objects in the OpenAPI; they are booleans now. Chapter 9 lists every tool; Word and PDF regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
241 lines
6.5 KiB
TypeScript
241 lines
6.5 KiB
TypeScript
import { McpContext, Resolver, Tool } from '@nestjs-mcp/server';
|
|
import { z } from 'zod';
|
|
import { TaskStreamApi } from '../api/taskstream-api';
|
|
import { issueRef, limit, projectRef, sprintId } from './params';
|
|
import { issueLine, sprintLine } from './summaries';
|
|
import { toolResult } from './tool-result';
|
|
|
|
const READ = { readOnlyHint: true, openWorldHint: false } as const;
|
|
const WRITE = {
|
|
readOnlyHint: false,
|
|
destructiveHint: false,
|
|
openWorldHint: false,
|
|
} as const;
|
|
|
|
const date = (what: string) => z.string().describe(`${what} (ISO date)`);
|
|
|
|
@Resolver()
|
|
export class SprintsResolver {
|
|
constructor(private readonly api: TaskStreamApi) {}
|
|
|
|
@Tool({
|
|
name: 'list_sprints',
|
|
description:
|
|
"A project's sprints (planned, active and closed) with their dates and goal.",
|
|
paramsSchema: z.object({ project: projectRef }),
|
|
annotations: READ,
|
|
})
|
|
list({ project }: { project: string }, ctx: McpContext) {
|
|
return toolResult(async () =>
|
|
(
|
|
await this.api.data(
|
|
this.api.as(ctx).GET('/api/projects/{project}/sprints', {
|
|
params: { path: { project } },
|
|
}),
|
|
)
|
|
).map(sprintLine),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'get_sprint',
|
|
description:
|
|
'A sprint with its progress (tickets and story points finished), its tickets in backlog order and, optionally, the day-by-day burndown.',
|
|
paramsSchema: z.object({
|
|
sprintId,
|
|
withBurndown: z.boolean().default(false),
|
|
limit: limit(200, 100),
|
|
}),
|
|
annotations: READ,
|
|
})
|
|
get(
|
|
params: { sprintId: string; withBurndown: boolean; limit: number },
|
|
ctx: McpContext,
|
|
) {
|
|
return toolResult(async () => {
|
|
const client = this.api.as(ctx);
|
|
const path = { sprint: params.sprintId };
|
|
const [sprint, progress, issues] = await Promise.all([
|
|
this.api.data(
|
|
client.GET('/api/sprints/{sprint}', { params: { path } }),
|
|
),
|
|
this.api.data(
|
|
client.GET('/api/sprints/{sprint}/progress', { params: { path } }),
|
|
),
|
|
this.api.data(
|
|
client.GET('/api/sprints/{sprint}/issues', {
|
|
params: { path, query: { limit: params.limit } },
|
|
}),
|
|
),
|
|
]);
|
|
const burndown = params.withBurndown
|
|
? await this.api.data(
|
|
client.GET('/api/sprints/{sprint}/burndown', { params: { path } }),
|
|
)
|
|
: undefined;
|
|
return {
|
|
...sprintLine(sprint),
|
|
progress,
|
|
total: issues.total,
|
|
issues: issues.items.map(issueLine),
|
|
burndown,
|
|
};
|
|
});
|
|
}
|
|
|
|
@Tool({
|
|
name: 'create_sprint',
|
|
description: 'Plan a sprint in a project (owners and admins).',
|
|
paramsSchema: z.object({
|
|
project: projectRef,
|
|
name: z.string().min(1).max(80).describe('e.g. Sprint 14'),
|
|
goal: z.string().max(500).optional(),
|
|
startDate: date('First day'),
|
|
endDate: date('Last day'),
|
|
}),
|
|
annotations: WRITE,
|
|
})
|
|
create(
|
|
params: {
|
|
project: string;
|
|
name: string;
|
|
goal?: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
},
|
|
ctx: McpContext,
|
|
) {
|
|
const { project, ...body } = params;
|
|
return toolResult(() =>
|
|
this.api.data(
|
|
this.api.as(ctx).POST('/api/projects/{project}/sprints', {
|
|
params: { path: { project } },
|
|
body,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'update_sprint',
|
|
description:
|
|
'Rename a sprint, change its goal or its dates (not once closed).',
|
|
paramsSchema: z.object({
|
|
sprintId,
|
|
name: z.string().min(1).max(80).optional(),
|
|
goal: z.string().max(500).nullable().optional(),
|
|
startDate: date('First day').optional(),
|
|
endDate: date('Last day').optional(),
|
|
}),
|
|
annotations: { ...WRITE, idempotentHint: true },
|
|
})
|
|
update(
|
|
params: {
|
|
sprintId: string;
|
|
name?: string;
|
|
goal?: string | null;
|
|
startDate?: string;
|
|
endDate?: string;
|
|
},
|
|
ctx: McpContext,
|
|
) {
|
|
const { sprintId, ...body } = params;
|
|
return toolResult(() =>
|
|
this.api.data(
|
|
this.api.as(ctx).PATCH('/api/sprints/{sprint}', {
|
|
params: { path: { sprint: sprintId } },
|
|
body,
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'start_sprint',
|
|
description:
|
|
'Start a planned sprint (only one can be under way per project).',
|
|
paramsSchema: z.object({ sprintId }),
|
|
annotations: WRITE,
|
|
})
|
|
start({ sprintId }: { sprintId: string }, ctx: McpContext) {
|
|
return toolResult(() =>
|
|
this.api.data(
|
|
this.api.as(ctx).POST('/api/sprints/{sprint}/start', {
|
|
params: { path: { sprint: sprintId } },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'close_sprint',
|
|
description:
|
|
'Close the sprint under way. Unfinished tickets move to carryOverTo (another open sprint of the project) or back to the backlog; finished ones stay as its record.',
|
|
paramsSchema: z.object({
|
|
sprintId,
|
|
carryOverTo: z
|
|
.string()
|
|
.nullable()
|
|
.optional()
|
|
.describe('Sprint id for the unfinished tickets; absent: the backlog'),
|
|
}),
|
|
annotations: WRITE,
|
|
})
|
|
close(
|
|
{
|
|
sprintId,
|
|
carryOverTo,
|
|
}: { sprintId: string; carryOverTo?: string | null },
|
|
ctx: McpContext,
|
|
) {
|
|
return toolResult(() =>
|
|
this.api.data(
|
|
this.api.as(ctx).POST('/api/sprints/{sprint}/close', {
|
|
params: { path: { sprint: sprintId } },
|
|
body: { carryOverTo },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'delete_sprint',
|
|
description: 'Delete a sprint that never started.',
|
|
paramsSchema: z.object({ sprintId }),
|
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
})
|
|
delete({ sprintId }: { sprintId: string }, ctx: McpContext) {
|
|
return toolResult(() =>
|
|
this.api.data(
|
|
this.api.as(ctx).DELETE('/api/sprints/{sprint}', {
|
|
params: { path: { sprint: sprintId } },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
@Tool({
|
|
name: 'set_issue_sprint',
|
|
description:
|
|
'Put a ticket in an open sprint of its project, or back to the backlog (null). For many at once: bulk_update_issues.',
|
|
paramsSchema: z.object({
|
|
issue: issueRef,
|
|
sprintId: z.string().nullable().describe('Sprint id, or null'),
|
|
}),
|
|
annotations: { ...WRITE, idempotentHint: true },
|
|
})
|
|
setSprint(
|
|
{ issue, sprintId }: { issue: string; sprintId: string | null },
|
|
ctx: McpContext,
|
|
) {
|
|
return toolResult(() =>
|
|
this.api.data(
|
|
this.api.as(ctx).PUT('/api/issues/{issue}/sprint', {
|
|
params: { path: { issue } },
|
|
body: { sprintId },
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
}
|