diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 724832e61..5883c79f1 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -79,6 +79,7 @@ import { ReplayRecordingSegmentsSchema, AIConversationSummaryListSchema, AIConversationSpanListSchema, + UserReportListSchema, } from "./schema"; import { ConfigurationError } from "../errors"; import { createApiError, ApiNotFoundError, ApiValidationError } from "./errors"; @@ -130,6 +131,7 @@ import type { ReplayRecordingSegments, AIConversationSummary, AIConversationSpanList, + UserReportList, } from "./types"; // TODO: this is shared - so ideally, for safety, it uses @sentry/core, but currently // logger isnt exposed (or rather, it is, but its not the right logger) @@ -3140,6 +3142,44 @@ export class SentryApiService { return ExternalIssueListSchema.parse(body); } + /** + * Retrieves issue user reports and returns the next Sentry cursor when another page exists. + */ + async getIssueUserReports( + { + organizationSlug, + issueId, + cursor, + limit, + }: { + organizationSlug: string; + issueId: string; + cursor?: string | null; + limit?: number; + }, + opts?: RequestOptions, + ): Promise<{ reports: UserReportList; nextCursor: string | null }> { + const searchQuery = new URLSearchParams(); + if (cursor) { + searchQuery.set("cursor", cursor); + } + if (limit !== undefined) { + searchQuery.set("per_page", String(limit)); + } + + const path = `/organizations/${organizationSlug}/issues/${issueId}/user-reports/`; + const response = await this.request( + searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path, + undefined, + opts, + ); + const body = await this.parseJsonResponse(response); + return { + reports: UserReportListSchema.parse(body), + nextCursor: getNextCursor(response.headers.get("link")), + }; + } + async getEventForIssue( { organizationSlug, diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index 664ffd129..6f0e0a4f4 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -1255,6 +1255,31 @@ export const ExternalIssueSchema = z.object({ webUrl: z.string(), }); +export const UserReportSchema = z.object({ + id: z.string(), + eventID: z.string(), + name: z.string().nullable(), + email: z.string().nullable(), + comments: z.string(), + dateCreated: z.string(), + user: z + .object({ + id: z.string(), + username: z.string().nullable(), + email: z.string().nullable(), + name: z.string().nullable(), + ipAddress: z.string().nullable(), + avatarUrl: z.string().nullable(), + }) + .nullable(), + event: z.object({ + id: z.string(), + eventID: z.string(), + }), +}); + +export const UserReportListSchema = z.array(UserReportSchema); + export const ExternalIssueListSchema = z.array(ExternalIssueSchema); /** diff --git a/packages/mcp-core/src/api-client/types.ts b/packages/mcp-core/src/api-client/types.ts index 58caa88c9..a700560cd 100644 --- a/packages/mcp-core/src/api-client/types.ts +++ b/packages/mcp-core/src/api-client/types.ts @@ -114,6 +114,7 @@ import type { TransactionProfileSchema, UnknownEventSchema, UserSchema, + UserReportListSchema, } from "./schema"; export type User = z.infer; @@ -224,3 +225,6 @@ export type IssueTagValues = z.infer; // External issue links (Jira, GitHub, etc.) export type ExternalIssue = z.infer; export type ExternalIssueList = z.infer; + +// User Report +export type UserReportList = z.infer; diff --git a/packages/mcp-core/src/server.test.ts b/packages/mcp-core/src/server.test.ts index 7591a9305..1c27f7947 100644 --- a/packages/mcp-core/src/server.test.ts +++ b/packages/mcp-core/src/server.test.ts @@ -1312,6 +1312,27 @@ describe("buildServer", () => { ); }); + it("execute_sentry_tool dispatches to catalog-only issue user reports", async () => { + const server = buildServer({ + context: baseContext, + }); + + const toolNames = getRegisteredToolNames(server); + expect(toolNames).not.toContain("get_issue_user_reports"); + + const result = await callRegisteredTool(server, "execute_sentry_tool", { + name: "get_issue_user_reports", + arguments: { + organizationSlug: "sentry-mcp-evals", + issueId: "CLOUDFLARE-MCP-41", + }, + }); + + expect(getTextContent(result)).toContain( + "# Issue User Reports for Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals**", + ); + }); + it("execute_sentry_tool dispatches to catalog-only update_dsn", async () => { const server = buildServer({ context: baseContext, diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index c1d9f80f2..4addc7972 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -5,7 +5,7 @@ "description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, monitors, profiles, documentation, and project metadata", "defaultEnabled": true, "order": 1, - "toolCount": 32, + "toolCount": 33, "tools": [ { "name": "find_alert_rules", @@ -87,6 +87,11 @@ "description": "Get tag value distribution for a specific Sentry issue.\n\nUse this tool when you need to:\n- Understand how an issue is distributed across different tag values\n- Get aggregate counts of unique tag values (e.g., 'how many unique URLs are affected')\n- Analyze which browsers, environments, or URLs are most impacted by an issue\n- View the tag distributions page data programmatically\n\nCommon tag keys:\n- `url`: Request URLs affected by the issue\n- `browser`: Browser types and versions\n- `browser.name`: Browser names only\n- `os`: Operating systems\n- `environment`: Deployment environments (production, staging, etc.)\n- `release`: Software releases\n- `device`: Device types\n- `user`: Affected users\n\n\n### Get URL distribution for an issue\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='url')\n```\n\n### Get browser distribution using issue URL\n```\nget_issue_tag_values(issueUrl='https://sentry.io/issues/PROJECT-123/', tagKey='browser')\n```\n\n### Get environment distribution\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='environment')\n```\n\n\n\n- If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- Common tag keys: url, browser, browser.name, os, environment, release, device, user\n- Tag keys are case-sensitive\n", "requiredScopes": ["event:read"] }, + { + "name": "get_issue_user_reports", + "description": "Get legacy User Reports or crash-report feedback attached to a Sentry issue.\n\nUse this tool when you need to:\n- See what a user said happened when an error occurred\n- Check if any legacy bug reports or crash-report feedback were submitted for this issue\n- Get the human-provided context behind a crash, beyond the stack trace\n\n\nget_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')\nget_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')\n\n\n\n- For standalone User Feedback Widget submissions, use `search_issues(query='issue.category:feedback')`.\n- Reuse pagination cursors only with the same issue scope.\n", + "requiredScopes": ["event:read"] + }, { "name": "get_latest_base_snapshot", "description": "Get the latest UI screenshots/images for an app from the preprod snapshot system.\n\nThis is the primary tool for retrieving app screenshots — not search_events or search_issues.\n\nUse this tool when you need to:\n- Get screenshots, screens, golden images, or reference images for an app\n- Find what the current UI looks like (latest screenshots from the main/default branch)\n- List available snapshots or browse images before requesting specific ones\n- Look up dark mode, light mode, or other variant screenshots\n- Understand what baseline images exist when investigating snapshot test or visual regression CI failures\n\nThe appId parameter is the app identifier (e.g. 'sentry-frontend', 'com.emergetools.hackernews').\nReturns compact image metadata (display_name, image_file_name, group, description) for every image.\n\n\n### Get the latest screenshots for an app\n\n```\nget_latest_base_snapshot(organizationSlug=\"sentry\", appId=\"sentry-frontend\", project=\"frontend\")\n```\n\n### Get the latest screenshots for a specific branch\n\n```\nget_latest_base_snapshot(organizationSlug=\"sentry\", appId=\"sentry-frontend\", project=\"frontend\", branch=\"main\")\n```\n\n\n\n- The response includes compact metadata per image. Scan the list to find images matching what you need (e.g. filter by group or name containing 'button').\n- To view a specific image, use get_sentry_resource(url='?selectedSnapshot=').\n- If you need to investigate a specific snapshot comparison, use get_sentry_resource with the snapshot URL.\n", @@ -276,7 +281,7 @@ "description": "Resolve, assign, and update issues", "defaultEnabled": false, "order": 4, - "toolCount": 15, + "toolCount": 16, "tools": [ { "name": "add_issue_note", @@ -318,6 +323,11 @@ "description": "Get detailed information about a specific Sentry issue by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific issue ID (e.g., 'CLOUDFLARE-MCP-41', 'PROJECT-123')\n- Ask to 'explain [ISSUE-ID]', 'tell me about [ISSUE-ID]'\n- Want details/stacktrace/analysis for a known issue\n- Provide a Sentry issue URL\n\nDO NOT USE for:\n- General searching or listing issues (use search_issues)\n\nTRIGGER PATTERNS:\n- 'Explain ISSUE-123' → use get_issue_details\n- 'Tell me about PROJECT-456' → use get_issue_details\n- 'What happened in [issue URL]' → use get_issue_details\n\n\n### With Sentry URL (recommended - simplest approach)\n```\nget_issue_details(issueUrl='https://sentry.sentry.io/issues/6916805731/?project=4509062593708032&query=is%3Aunresolved')\n```\n\n### With issue ID and organization\n```\nget_issue_details(organizationSlug='my-organization', issueId='CLOUDFLARE-MCP-41')\n```\n\n### With event ID and organization\n```\nget_issue_details(organizationSlug='my-organization', eventId='c49541c747cb4d8aa3efb70ca5aba243')\n```\n\n\n\n- **IMPORTANT**: If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- When using issueUrl, all other parameters are automatically extracted - don't provide them separately\n- If using issueId (not URL), then organizationSlug is required\n", "requiredScopes": ["event:read"] }, + { + "name": "get_issue_user_reports", + "description": "Get legacy User Reports or crash-report feedback attached to a Sentry issue.\n\nUse this tool when you need to:\n- See what a user said happened when an error occurred\n- Check if any legacy bug reports or crash-report feedback were submitted for this issue\n- Get the human-provided context behind a crash, beyond the stack trace\n\n\nget_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')\nget_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')\n\n\n\n- For standalone User Feedback Widget submissions, use `search_issues(query='issue.category:feedback')`.\n- Reuse pagination cursors only with the same issue scope.\n", + "requiredScopes": ["event:read"] + }, { "name": "get_sentry_resource", "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, AI conversations, breadcrumbs, replays, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAI Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_ai_conversations. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: :\n- snapshot: \n- snapshotImage: :\n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='span', organizationSlug='my-org', resourceId=':')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index bb63f18d3..c4604cb1d 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -1522,6 +1522,66 @@ "skills": ["inspect"], "surface": "catalog" }, + { + "name": "get_issue_user_reports", + "description": "Get legacy User Reports or crash-report feedback attached to a Sentry issue.\n\nUse this tool when you need to:\n- See what a user said happened when an error occurred\n- Check if any legacy bug reports or crash-report feedback were submitted for this issue\n- Get the human-provided context behind a crash, beyond the stack trace\n\n\nget_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')\nget_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')\n\n\n\n- For standalone User Feedback Widget submissions, use `search_issues(query='issue.category:feedback')`.\n- Reuse pagination cursors only with the same issue scope.\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ], + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.", + "default": null + }, + "issueId": { + "type": "string", + "description": "The Issue ID. e.g. `PROJECT-1Z43`" + }, + "issueUrl": { + "type": "string", + "format": "uri", + "description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43" + }, + "cursor": { + "anyOf": [ + { + "type": "string", + "description": "Optional pagination cursor from a previous response." + }, + { + "type": "null" + } + ], + "description": "Optional pagination cursor from a previous response.", + "default": null + }, + "limit": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 100, + "description": "Maximum number of user reports to return.", + "default": 25 + } + }, + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "requiredScopes": ["event:read"], + "skills": ["inspect", "triage"], + "surface": "catalog" + }, { "name": "get_latest_base_snapshot", "description": "Get the latest UI screenshots/images for an app from the preprod snapshot system.\n\nThis is the primary tool for retrieving app screenshots — not search_events or search_issues.\n\nUse this tool when you need to:\n- Get screenshots, screens, golden images, or reference images for an app\n- Find what the current UI looks like (latest screenshots from the main/default branch)\n- List available snapshots or browse images before requesting specific ones\n- Look up dark mode, light mode, or other variant screenshots\n- Understand what baseline images exist when investigating snapshot test or visual regression CI failures\n\nThe appId parameter is the app identifier (e.g. 'sentry-frontend', 'com.emergetools.hackernews').\nReturns compact image metadata (display_name, image_file_name, group, description) for every image.\n\n\n### Get the latest screenshots for an app\n\n```\nget_latest_base_snapshot(organizationSlug=\"sentry\", appId=\"sentry-frontend\", project=\"frontend\")\n```\n\n### Get the latest screenshots for a specific branch\n\n```\nget_latest_base_snapshot(organizationSlug=\"sentry\", appId=\"sentry-frontend\", project=\"frontend\", branch=\"main\")\n```\n\n\n\n- The response includes compact metadata per image. Scan the list to find images matching what you need (e.g. filter by group or name containing 'button').\n- To view a specific image, use get_sentry_resource(url='?selectedSnapshot=').\n- If you need to investigate a specific snapshot comparison, use get_sentry_resource with the snapshot URL.\n", diff --git a/packages/mcp-core/src/tools/catalog/get-issue-user-reports.test.ts b/packages/mcp-core/src/tools/catalog/get-issue-user-reports.test.ts new file mode 100644 index 000000000..eb2a02766 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/get-issue-user-reports.test.ts @@ -0,0 +1,99 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { http, HttpResponse } from "msw"; +import { afterEach, describe, expect, it } from "vitest"; +import getIssueUserReports from "./get-issue-user-reports.js"; + +const context = { + constraints: { + organizationSlug: null, + }, + accessToken: "access-token", + userId: "1", +}; + +afterEach(() => { + mswServer.resetHandlers(); +}); + +describe("get_issue_user_reports", () => { + it("serializes user feedback for an issue", async () => { + const result = await getIssueUserReports.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + issueId: "CLOUDFLARE-MCP-41", + cursor: null, + limit: 25, + }, + context, + ); + + expect(result).toMatchInlineSnapshot(` + "# Issue User Reports for Issue CLOUDFLARE-MCP-41 in **sentry-mcp-evals** + + - 2026-06-22T02:37:27.878Z by Example Reporter: + - "i am currently testing it" + - 2026-06-22T03:10:11.123Z by anonymous: + - "anonymous report with no event user" + - 2026-06-22T03:45:12.456Z by anonymous: + - "event user shell without identity" + " + `); + }); + + it("returns pagination guidance when more reports are available", async () => { + mswServer.use( + http.get( + "*/api/0/organizations/:org/issues/:issueId/user-reports/", + ({ request }) => { + const url = new URL(request.url); + expect(url.searchParams.get("per_page")).toBe("1"); + expect(url.searchParams.get("cursor")).toBe("page-1"); + return HttpResponse.json( + [ + { + id: "11974229", + eventID: "157c1ee8f31e4e68a57b637dad557b34", + name: "", + email: "", + comments: "reported by event user", + dateCreated: "2026-06-22T04:20:21.987654Z", + user: { + id: "user-456", + username: "event-user", + email: null, + name: null, + ipAddress: null, + avatarUrl: null, + }, + event: { + id: "157c1ee8f31e4e68a57b637dad557b34", + eventID: "157c1ee8f31e4e68a57b637dad557b34", + }, + }, + ], + { + headers: { + Link: '; rel="next"; results="true"; cursor="page-2"', + }, + }, + ); + }, + ), + ); + + const result = await getIssueUserReports.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + issueId: "CLOUDFLARE-MCP-41", + cursor: "page-1", + limit: 1, + }, + context, + ); + + expect(result).toContain("by event-user"); + expect(result).toContain('cursor: "page-2"'); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts b/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts new file mode 100644 index 000000000..754a3f474 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts @@ -0,0 +1,135 @@ +import { z } from "zod"; +import { setTag } from "@sentry/core"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { + ensureIssueWithinProjectConstraint, + parseIssueParams, +} from "../../internal/tool-helpers/issue"; +import type { UserReportList } from "../../api-client/types"; +import type { ServerContext } from "../../types"; +import { + ParamIssueShortId, + ParamIssueUrl, + ParamOrganizationSlug, + ParamRegionUrl, +} from "../../schema"; +import { formatDate } from "./support/api-formatting"; + +function getReporter(report: UserReportList[number]): string { + const userId = report.user?.id?.trim(); + // Older Sentry serializers can emit sentinel strings when no user ID exists. + const userIdReporter = + userId && !["none", "null"].includes(userId.toLowerCase()) + ? `user:${userId}` + : null; + return ( + report.name?.trim() || + report.email?.trim() || + report.user?.name?.trim() || + report.user?.email?.trim() || + report.user?.username?.trim() || + userIdReporter || + "anonymous" + ); +} + +function formatUserReport(report: UserReportList[number]): string { + const date = formatDate(report.dateCreated) ?? "unknown time"; + const reporter = getReporter(report); + return `- ${date} by ${reporter}:\n - "${report.comments}"`; +} + +export default defineTool({ + name: "get_issue_user_reports", + skills: ["inspect", "triage"], + requiredScopes: ["event:read"], + description: [ + "Get legacy User Reports or crash-report feedback attached to a Sentry issue.", + "", + "Use this tool when you need to:", + "- See what a user said happened when an error occurred", + "- Check if any legacy bug reports or crash-report feedback were submitted for this issue", + "- Get the human-provided context behind a crash, beyond the stack trace", + "", + "", + "get_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')", + "get_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')", + "", + "", + "", + "- For standalone User Feedback Widget submissions, use `search_issues(query='issue.category:feedback')`.", + "- Reuse pagination cursors only with the same issue scope.", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug.optional(), + regionUrl: ParamRegionUrl.nullable().default(null), + issueId: ParamIssueShortId.optional(), + issueUrl: ParamIssueUrl.optional(), + cursor: z + .string() + .trim() + .describe("Optional pagination cursor from a previous response.") + .nullable() + .default(null), + limit: z + .number() + .int() + .positive() + .max(100) + .describe("Maximum number of user reports to return.") + .default(25), + }, + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + async handler(params, context: ServerContext) { + const parsed = parseIssueParams({ + issueUrl: params.issueUrl, + issueId: params.issueId, + organizationSlug: + params.organizationSlug ?? context.constraints.organizationSlug, + }); + + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? context.constraints.regionUrl ?? undefined, + }); + setTag("organization.slug", parsed.organizationSlug); + setTag("issue.id", parsed.issueId); + + await ensureIssueWithinProjectConstraint({ + apiService, + organizationSlug: parsed.organizationSlug, + issueId: parsed.issueId, + projectSlug: context.constraints.projectSlug, + }); + + const { reports, nextCursor } = await apiService.getIssueUserReports({ + organizationSlug: parsed.organizationSlug, + issueId: parsed.issueId, + cursor: params.cursor, + limit: params.limit, + }); + + const output = [ + `# Issue User Reports for Issue ${parsed.issueId} in **${parsed.organizationSlug}**`, + "", + reports.length === 0 + ? "No issue user reports found for this issue." + : reports.map(formatUserReport).join("\n"), + ]; + + if (nextCursor) { + output.push( + "", + "## Response Notes", + "", + `- More user reports are available. Pass \`cursor: "${nextCursor}"\` with the same issue scope to fetch the next page.`, + ); + } + + return `${output.join("\n")}\n`; + }, +}); diff --git a/packages/mcp-core/src/tools/catalog/index.ts b/packages/mcp-core/src/tools/catalog/index.ts index 7bc16ab6a..80594e97a 100644 --- a/packages/mcp-core/src/tools/catalog/index.ts +++ b/packages/mcp-core/src/tools/catalog/index.ts @@ -14,6 +14,7 @@ import getIssueDetails from "./get-issue-details"; import getEventStacktrace from "./get-event-stacktrace"; import getIssueActivity from "./get-issue-activity"; import getIssueTagValues from "./get-issue-tag-values"; +import getIssueUserReports from "./get-issue-user-reports"; import getTraceDetails from "./get-trace-details"; import getReplayDetails from "./get-replay-details"; import getEventAttachment from "./get-event-attachment"; @@ -66,6 +67,7 @@ const catalogTools = { get_issue_details: getIssueDetails, get_event_stacktrace: getEventStacktrace, get_issue_activity: getIssueActivity, + get_issue_user_reports: getIssueUserReports, get_issue_tag_values: getIssueTagValues, get_trace_details: getTraceDetails, get_replay_details: getReplayDetails, diff --git a/packages/mcp-server-mocks/src/fixtures/issue-user-reports.json b/packages/mcp-server-mocks/src/fixtures/issue-user-reports.json new file mode 100644 index 000000000..59c281b48 --- /dev/null +++ b/packages/mcp-server-mocks/src/fixtures/issue-user-reports.json @@ -0,0 +1,55 @@ +[ + { + "id": "11974227", + "eventID": "957c1ee8f31e4e68a57b637dad557b32", + "name": "Example Reporter", + "email": "reporter@example.com", + "comments": "i am currently testing it", + "dateCreated": "2026-06-22T02:37:27.878947Z", + "user": { + "id": "user-123", + "username": "example-reporter", + "email": "reporter@example.com", + "name": "Example Reporter", + "ipAddress": "203.0.113.10", + "avatarUrl": "https://example.com/avatar.png" + }, + "event": { + "id": "957c1ee8f31e4e68a57b637dad557b32", + "eventID": "957c1ee8f31e4e68a57b637dad557b32" + } + }, + { + "id": "11974228", + "eventID": "057c1ee8f31e4e68a57b637dad557b33", + "name": "", + "email": "", + "comments": "anonymous report with no event user", + "dateCreated": "2026-06-22T03:10:11.123456Z", + "user": null, + "event": { + "id": "057c1ee8f31e4e68a57b637dad557b33", + "eventID": "057c1ee8f31e4e68a57b637dad557b33" + } + }, + { + "id": "11974229", + "eventID": "157c1ee8f31e4e68a57b637dad557b34", + "name": "", + "email": "", + "comments": "event user shell without identity", + "dateCreated": "2026-06-22T03:45:12.456789Z", + "user": { + "id": "None", + "username": null, + "email": null, + "name": null, + "ipAddress": null, + "avatarUrl": null + }, + "event": { + "id": "157c1ee8f31e4e68a57b637dad557b34", + "eventID": "157c1ee8f31e4e68a57b637dad557b34" + } + } +] diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index 1deb769be..d26ff1dd0 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -83,6 +83,9 @@ import issueActivityFixture from "./fixtures/issue-activity.json" with { import issueCommentsFixture from "./fixtures/issue-comments.json" with { type: "json", }; +import issueUserReportsFixture from "./fixtures/issue-user-reports.json" with { + type: "json", +}; import issueFixture from "./fixtures/issue.json" with { type: "json" }; import issueNullCulpritFixture from "./fixtures/issue-null-culprit.json" with { type: "json", @@ -1264,6 +1267,12 @@ export const restHandlers = buildHandlers([ path: "/api/0/organizations/:org/issues/:issueId/external-issues/", fetch: () => HttpResponse.json([]), }, + // User reports endpoints + { + method: "get", + path: "/api/0/organizations/:org/issues/:issueId/user-reports/", + fetch: () => HttpResponse.json(issueUserReportsFixture), + }, // Issue tag values endpoints { method: "get",