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

@@ -11,6 +11,11 @@ const WRITE = {
destructiveHint: false,
openWorldHint: false,
} as const;
const REMOVE = {
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,
} as const;
const PRIORITY = z.enum(['LOWEST', 'LOW', 'MEDIUM', 'HIGH', 'CRITICAL']);
const CATEGORY = z.enum(['TODO', 'IN_PROGRESS', 'QA', 'DONE', 'CANCELLED']);
@@ -35,6 +40,10 @@ const issueFields = {
labels: z.array(z.string().max(40)).max(20).optional(),
figmaUrl: z.string().url().optional().describe('Figma frame (https)'),
storyPoints: z.number().int().min(0).max(100).optional(),
startDate: z
.string()
.optional()
.describe('ISO date when work should start (roadmap)'),
dueDate: z.string().optional().describe('ISO date, e.g. 2026-11-14'),
};
@@ -45,6 +54,7 @@ type IssueFields = {
labels?: string[];
figmaUrl?: string;
storyPoints?: number;
startDate?: string;
dueDate?: string;
};
@@ -95,7 +105,7 @@ export class IssuesResolver {
@Tool({
name: 'search_issues',
description:
'Search the tickets of a project by text (key, title, description) and filters.',
'Search the tickets of a project by text (key, title, description) and filters. The backlog: noSprint with sort RANK.',
paramsSchema: z.object({
project: projectRef,
search: z.string().max(200).optional(),
@@ -108,6 +118,21 @@ export class IssuesResolver {
unassigned: z.boolean().optional(),
releaseId: z.string().optional(),
labels: z.array(z.string()).optional(),
parent: z
.string()
.optional()
.describe('Only the tickets hanging from this epic or ticket (key)'),
sprintId: z.string().optional().describe('Only this sprint'),
noSprint: z
.boolean()
.optional()
.describe('Only tickets in no sprint (the backlog)'),
dueFrom: z.string().optional().describe('Due on or after (ISO date)'),
dueTo: z.string().optional().describe('Due on or before (ISO date)'),
sort: z
.enum(['RECENT', 'RANK'])
.default('RECENT')
.describe('RECENT: last updated first. RANK: backlog order'),
limit: limit(200, 30),
offset: z.number().int().min(0).default(0),
}),
@@ -126,6 +151,12 @@ export class IssuesResolver {
unassigned?: boolean;
releaseId?: string;
labels?: string[];
parent?: string;
sprintId?: string;
noSprint?: boolean;
dueFrom?: string;
dueTo?: string;
sort: 'RECENT' | 'RANK';
limit: number;
offset: number;
},
@@ -147,6 +178,12 @@ export class IssuesResolver {
unassigned: params.unassigned,
releaseId: params.releaseId,
label: params.labels,
parent: params.parent,
sprintId: params.sprintId,
noSprint: params.noSprint,
dueFrom: params.dueFrom,
dueTo: params.dueTo,
sort: params.sort,
limit: params.limit,
offset: params.offset,
},
@@ -160,7 +197,7 @@ export class IssuesResolver {
@Tool({
name: 'get_issue',
description:
'A ticket in full: description, links, allowed next statuses, and optionally its comments and history.',
'A ticket in full: description, parent, checklist (with item ids), links, commits and pull requests, followers, allowed next statuses, and optionally its comments and history.',
paramsSchema: z.object({
issue: issueRef,
withComments: z.boolean().default(true),
@@ -206,10 +243,21 @@ export class IssuesResolver {
description: issue.description,
reporter: issue.reporter.name,
figmaUrl: issue.figmaUrl,
storyPoints: issue.storyPoints,
dueDate: issue.dueDate,
startDate: issue.startDate,
allowedTransitions: issue.allowedTransitions,
parent: issue.parent,
checklist: issue.checklist,
links: issue.links,
devLinks: issue.devLinks.map((d) => ({
kind: d.kind,
ref: d.ref,
title: d.title,
state: d.state,
url: d.url,
})),
watchers: issue.watchers.map((w) => w.name),
attachments: issue.attachments.length,
trashedAt: issue.trashedAt,
createdAt: issue.createdAt,
comments,
history,
@@ -229,6 +277,10 @@ export class IssuesResolver {
title: z.string().min(1).max(255),
assigneeId: z.string().optional().describe('User id (a project member)'),
releaseId: z.string().optional(),
parent: z
.string()
.optional()
.describe('Epic or ticket of the same project it breaks down (key)'),
...issueFields,
}),
annotations: WRITE,
@@ -240,6 +292,7 @@ export class IssuesResolver {
title: string;
assigneeId?: string;
releaseId?: string;
parent?: string;
},
ctx: McpContext,
) {
@@ -399,4 +452,167 @@ export class IssuesResolver {
),
);
}
@Tool({
name: 'unlink_issues',
description:
'Remove a relation between two tickets (as link_issues made it).',
paramsSchema: z.object({
issue: issueRef,
type: LINK_TYPE,
target: issueRef,
}),
annotations: REMOVE,
})
unlink(
params: { issue: string; type: z.infer<typeof LINK_TYPE>; target: string },
ctx: McpContext,
) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).DELETE('/api/issues/{issue}/links/{type}/{target}', {
params: { path: params },
}),
),
);
}
@Tool({
name: 'watch_issue',
description:
'Follow a ticket (notifications of its changes and comments) or stop following it.',
paramsSchema: z.object({
issue: issueRef,
follow: z.boolean().default(true),
}),
annotations: { ...WRITE, idempotentHint: true },
})
watch(
{ issue, follow }: { issue: string; follow: boolean },
ctx: McpContext,
) {
const client = this.api.as(ctx);
const options = { params: { path: { issue } } };
return toolResult(() =>
this.api.data(
follow
? client.PUT('/api/issues/{issue}/watch', options)
: client.DELETE('/api/issues/{issue}/watch', options),
),
);
}
@Tool({
name: 'edit_comment',
description:
'Reword a comment (only its author). Comment ids come with get_issue.',
paramsSchema: z.object({
commentId: z.string().min(1),
body: z.string().min(1).max(20000),
}),
annotations: { ...WRITE, idempotentHint: true },
})
editComment(
{ commentId, body }: { commentId: string; body: string },
ctx: McpContext,
) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).PATCH('/api/comments/{commentId}', {
params: { path: { commentId } },
body: { body },
}),
),
);
}
@Tool({
name: 'delete_comment',
description: 'Delete a comment (its author or a project admin).',
paramsSchema: z.object({ commentId: z.string().min(1) }),
annotations: REMOVE,
})
deleteComment({ commentId }: { commentId: string }, ctx: McpContext) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).DELETE('/api/comments/{commentId}', {
params: { path: { commentId } },
}),
),
);
}
/* ---- Checklist ---- */
@Tool({
name: 'add_checklist_item',
description: "Add a step to a ticket's checklist.",
paramsSchema: z.object({
issue: issueRef,
text: z.string().min(1).max(300),
}),
annotations: WRITE,
})
addChecklistItem(
{ issue, text }: { issue: string; text: string },
ctx: McpContext,
) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).POST('/api/issues/{issue}/checklist', {
params: { path: { issue } },
body: { text },
}),
),
);
}
@Tool({
name: 'update_checklist_item',
description:
'Tick, untick or reword a checklist step. Item ids come with get_issue.',
paramsSchema: z.object({
issue: issueRef,
item: z.string().min(1).describe('Checklist item id'),
done: z.boolean().optional(),
text: z.string().min(1).max(300).optional(),
}),
annotations: { ...WRITE, idempotentHint: true },
})
updateChecklistItem(
params: { issue: string; item: string; done?: boolean; text?: string },
ctx: McpContext,
) {
const { issue, item, ...body } = params;
return toolResult(() =>
this.api.data(
this.api.as(ctx).PATCH('/api/issues/{issue}/checklist/{item}', {
params: { path: { issue, item } },
body,
}),
),
);
}
@Tool({
name: 'remove_checklist_item',
description: "Remove a step from a ticket's checklist.",
paramsSchema: z.object({
issue: issueRef,
item: z.string().min(1).describe('Checklist item id'),
}),
annotations: REMOVE,
})
removeChecklistItem(
{ issue, item }: { issue: string; item: string },
ctx: McpContext,
) {
return toolResult(() =>
this.api.data(
this.api.as(ctx).DELETE('/api/issues/{issue}/checklist/{item}', {
params: { path: { issue, item } },
}),
),
);
}
}