feat(mcp): sprints, epics, backlog, trash, checklists and release notes

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>
This commit is contained in:
2026-10-09 10:17:10 +02:00
parent 58fe5e8301
commit e6ac02d328
16 changed files with 1129 additions and 54 deletions

View File

@@ -0,0 +1,70 @@
import { McpContext, Resolver, Tool } from '@nestjs-mcp/server';
import { z } from 'zod';
import { TaskStreamApi } from '../api/taskstream-api';
import { limit } from './params';
import { toolResult } from './tool-result';
@Resolver()
export class NotificationsResolver {
constructor(private readonly api: TaskStreamApi) {}
@Tool({
name: 'my_notifications',
description:
"The user's notifications, newest first: assignments, status changes, failed QA, comments, mentions, followed tickets, reminders.",
paramsSchema: z.object({
unreadOnly: z.boolean().default(true),
limit: limit(100, 30),
}),
annotations: { readOnlyHint: true, openWorldHint: false },
})
mine(
{ unreadOnly, limit }: { unreadOnly: boolean; limit: number },
ctx: McpContext,
) {
return toolResult(async () => {
const page = await this.api.data(
this.api.as(ctx).GET('/api/me/notifications', {
params: { query: { unreadOnly, limit } },
}),
);
return {
unread: page.unread,
items: page.items.map((n) => ({
id: n.id,
kind: n.kind,
project: n.projectKey,
issue: n.issueKey,
who: n.actor?.name ?? null,
summary: n.summary,
read: n.readAt !== null,
at: n.createdAt,
})),
};
});
}
@Tool({
name: 'mark_notifications_read',
description: 'Mark notifications as read: these ids, or all of them.',
paramsSchema: z.object({
ids: z
.array(z.string())
.optional()
.describe('Notification ids; absent: all'),
}),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
})
markRead({ ids }: { ids?: string[] }, ctx: McpContext) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).POST('/api/me/notifications/read', { body: { ids } }),
),
);
}
}