Skip to content

Commit 68b5e14

Browse files
committed
feat(mcp): add repro_list_ticket_comments with cursor pagination
1 parent 5f42bf7 commit 68b5e14

1 file changed

Lines changed: 90 additions & 0 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { z } from "zod"
2+
import { and, desc, eq, isNull, lt, or } from "drizzle-orm"
3+
import { db } from "../../db"
4+
import { reports, reportComments } from "../../db/schema"
5+
import { requireProjectRoleByUser } from "../../lib/permissions"
6+
import { mcpError } from "../errors"
7+
import { decodeCursor, encodeCursor } from "../cursor"
8+
import type { McpRequestContext } from "../context"
9+
10+
export const listTicketCommentsTool = {
11+
name: "repro_list_ticket_comments",
12+
config: {
13+
description:
14+
"List comments on a Repro ticket, newest first. Returns up to 50 per page. Comments may originate from the dashboard or be mirrored from GitHub (see the `source` field).",
15+
inputSchema: z.object({
16+
ticketId: z.string().uuid(),
17+
cursor: z.string().optional(),
18+
limit: z.number().int().min(1).max(50).optional(),
19+
}),
20+
},
21+
handler: async (
22+
input: { ticketId: string; cursor?: string; limit?: number },
23+
ctx: McpRequestContext,
24+
) => {
25+
const [report] = await db
26+
.select({ projectId: reports.projectId })
27+
.from(reports)
28+
.where(eq(reports.id, input.ticketId))
29+
.limit(1)
30+
if (!report) throw mcpError("NOT_FOUND", `ticket ${input.ticketId} not found`)
31+
await requireProjectRoleByUser(ctx.userId, report.projectId, "viewer")
32+
33+
const limit = input.limit ?? 25
34+
const decodedCursor = input.cursor ? decodeCursor(input.cursor) : null
35+
36+
const conditions = [
37+
eq(reportComments.reportId, input.ticketId),
38+
isNull(reportComments.deletedAt),
39+
]
40+
if (decodedCursor) {
41+
conditions.push(
42+
or(
43+
lt(reportComments.createdAt, decodedCursor.createdAt),
44+
and(
45+
eq(reportComments.createdAt, decodedCursor.createdAt),
46+
lt(reportComments.id, decodedCursor.id),
47+
),
48+
)!,
49+
)
50+
}
51+
52+
const rows = await db
53+
.select({
54+
id: reportComments.id,
55+
body: reportComments.body,
56+
userId: reportComments.userId,
57+
githubLogin: reportComments.githubLogin,
58+
source: reportComments.source,
59+
createdAt: reportComments.createdAt,
60+
updatedAt: reportComments.updatedAt,
61+
})
62+
.from(reportComments)
63+
.where(and(...conditions))
64+
.orderBy(desc(reportComments.createdAt), desc(reportComments.id))
65+
.limit(limit + 1)
66+
67+
const hasMore = rows.length > limit
68+
const page = rows.slice(0, limit)
69+
const nextCursor =
70+
hasMore && page.length > 0
71+
? encodeCursor({
72+
createdAt: page[page.length - 1]!.createdAt,
73+
id: page[page.length - 1]!.id,
74+
})
75+
: null
76+
77+
const items = page.map((c) => ({
78+
id: c.id,
79+
body: c.body,
80+
source: c.source,
81+
authorUserId: c.userId,
82+
authorGithubLogin: c.githubLogin,
83+
createdAt: c.createdAt.toISOString(),
84+
updatedAt: c.updatedAt.toISOString(),
85+
}))
86+
return {
87+
content: [{ type: "text" as const, text: JSON.stringify({ items, nextCursor }, null, 2) }],
88+
}
89+
},
90+
}

0 commit comments

Comments
 (0)