Skip to content

Commit 5f42bf7

Browse files
committed
feat(mcp): add repro_list_tickets with filters + cursor pagination
1 parent ef962e7 commit 5f42bf7

1 file changed

Lines changed: 121 additions & 1 deletion

File tree

apps/dashboard/server/mcp/tools/tickets.ts

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from "zod"
2-
import { eq } from "drizzle-orm"
2+
import { and, desc, eq, inArray, lt, or, sql } from "drizzle-orm"
3+
import { encodeCursor, decodeCursor } from "../cursor"
34
import { gunzipSync } from "node:zlib"
45
import { db } from "../../db"
56
import { reports, reportAttachments } from "../../db/schema"
@@ -126,3 +127,122 @@ export const getTicketTool = {
126127
}
127128
},
128129
}
130+
131+
export const listTicketsTool = {
132+
name: "repro_list_tickets",
133+
config: {
134+
description:
135+
"List Repro tickets (reports) in a project, newest first. Supports filtering by status, priority, tags, free-text search, and cursor pagination. Returns up to 50 items per page.",
136+
inputSchema: z.object({
137+
projectId: z.string().uuid(),
138+
status: z
139+
.array(z.enum(["open", "in_progress", "resolved", "closed"]))
140+
.optional()
141+
.describe("Filter to these statuses (default: any)."),
142+
priority: z
143+
.array(z.enum(["low", "normal", "high", "urgent"]))
144+
.optional()
145+
.describe("Filter to these priorities (default: any)."),
146+
tag: z
147+
.array(z.string())
148+
.optional()
149+
.describe("Filter to tickets containing ALL of these tags."),
150+
query: z
151+
.string()
152+
.optional()
153+
.describe("Case-insensitive substring match on title/description."),
154+
cursor: z.string().optional(),
155+
limit: z.number().int().min(1).max(50).optional(),
156+
}),
157+
},
158+
handler: async (
159+
input: {
160+
projectId: string
161+
status?: string[]
162+
priority?: string[]
163+
tag?: string[]
164+
query?: string
165+
cursor?: string
166+
limit?: number
167+
},
168+
ctx: McpRequestContext,
169+
) => {
170+
await requireProjectRoleByUser(ctx.userId, input.projectId, "viewer")
171+
const limit = input.limit ?? 25
172+
const decodedCursor = input.cursor ? decodeCursor(input.cursor) : null
173+
174+
const conditions = [eq(reports.projectId, input.projectId)]
175+
if (input.status?.length) {
176+
conditions.push(
177+
inArray(reports.status, input.status as ("open" | "in_progress" | "resolved" | "closed")[]),
178+
)
179+
}
180+
if (input.priority?.length) {
181+
conditions.push(
182+
inArray(reports.priority, input.priority as ("low" | "normal" | "high" | "urgent")[]),
183+
)
184+
}
185+
if (input.tag?.length) {
186+
// tags @> ARRAY[$tags] — Postgres array-contains.
187+
conditions.push(sql`${reports.tags} @> ${input.tag}::text[]`)
188+
}
189+
if (input.query) {
190+
const needle = `%${input.query.toLowerCase()}%`
191+
conditions.push(
192+
sql`(lower(${reports.title}) LIKE ${needle} OR lower(coalesce(${reports.description}, '')) LIKE ${needle})`,
193+
)
194+
}
195+
if (decodedCursor) {
196+
// Keyset pagination: rows strictly after the cursor in (createdAt DESC, id DESC).
197+
conditions.push(
198+
or(
199+
lt(reports.createdAt, decodedCursor.createdAt),
200+
and(eq(reports.createdAt, decodedCursor.createdAt), lt(reports.id, decodedCursor.id)),
201+
)!,
202+
)
203+
}
204+
205+
const rows = await db
206+
.select({
207+
id: reports.id,
208+
title: reports.title,
209+
status: reports.status,
210+
priority: reports.priority,
211+
tags: reports.tags,
212+
githubIssueNumber: reports.githubIssueNumber,
213+
githubIssueUrl: reports.githubIssueUrl,
214+
createdAt: reports.createdAt,
215+
updatedAt: reports.updatedAt,
216+
})
217+
.from(reports)
218+
.where(and(...conditions))
219+
.orderBy(desc(reports.createdAt), desc(reports.id))
220+
.limit(limit + 1)
221+
222+
const hasMore = rows.length > limit
223+
const page = rows.slice(0, limit)
224+
const nextCursor =
225+
hasMore && page.length > 0
226+
? encodeCursor({
227+
createdAt: page[page.length - 1]!.createdAt,
228+
id: page[page.length - 1]!.id,
229+
})
230+
: null
231+
232+
const items = page.map((r) => ({
233+
id: r.id,
234+
title: r.title,
235+
status: r.status,
236+
priority: r.priority,
237+
tags: r.tags,
238+
github: r.githubIssueNumber
239+
? { issueNumber: r.githubIssueNumber, issueUrl: r.githubIssueUrl ?? null }
240+
: null,
241+
createdAt: r.createdAt.toISOString(),
242+
updatedAt: r.updatedAt.toISOString(),
243+
}))
244+
return {
245+
content: [{ type: "text" as const, text: JSON.stringify({ items, nextCursor }, null, 2) }],
246+
}
247+
},
248+
}

0 commit comments

Comments
 (0)