Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import {
ReplayRecordingSegmentsSchema,
AIConversationSummaryListSchema,
AIConversationSpanListSchema,
UserReportListSchema,
} from "./schema";
import { ConfigurationError } from "../errors";
import { createApiError, ApiNotFoundError, ApiValidationError } from "./errors";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-core/src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import type {
TransactionProfileSchema,
UnknownEventSchema,
UserSchema,
UserReportListSchema,
} from "./schema";

export type User = z.infer<typeof UserSchema>;
Expand Down Expand Up @@ -224,3 +225,6 @@ export type IssueTagValues = z.infer<typeof IssueTagValuesSchema>;
// External issue links (Jira, GitHub, etc.)
export type ExternalIssue = z.infer<typeof ExternalIssueSchema>;
export type ExternalIssueList = z.infer<typeof ExternalIssueListSchema>;

// User Report
export type UserReportList = z.infer<typeof UserReportListSchema>;
21 changes: 21 additions & 0 deletions packages/mcp-core/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
14 changes: 12 additions & 2 deletions packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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<examples>\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</examples>\n\n<hints>\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</hints>",
"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<examples>\nget_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')\nget_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')\n</examples>\n\n<hints>\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</hints>",
"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<examples>\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</examples>\n\n<hints>\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='<snapshot_url>?selectedSnapshot=<image_file_name>').\n- If you need to investigate a specific snapshot comparison, use get_sentry_resource with the snapshot URL.\n</hints>",
Expand Down Expand Up @@ -276,7 +281,7 @@
"description": "Resolve, assign, and update issues",
"defaultEnabled": false,
"order": 4,
"toolCount": 15,
"toolCount": 16,
"tools": [
{
"name": "add_issue_note",
Expand Down Expand Up @@ -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<examples>\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</examples>\n\n<hints>\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</hints>",
"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<examples>\nget_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')\nget_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')\n</examples>\n\n<hints>\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</hints>",
"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=<image_file_name>: returns the image preview and metadata. Full-resolution snapshot image bytes are not available in this session.\n\nResource IDs:\n- span: <traceId>:<spanId>\n- snapshot: <snapshotId>\n- snapshotImage: <snapshotId>:<image_file_name>\n\n<examples>\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='<traceId>:<spanId>')\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</examples>",
Expand Down
60 changes: 60 additions & 0 deletions packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -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<examples>\nget_issue_user_reports(organizationSlug='my-organization', issueId='PROJECT-123')\nget_issue_user_reports(issueUrl='https://my-organization.sentry.io/issues/PROJECT-123/')\n</examples>\n\n<hints>\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</hints>",
"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<examples>\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</examples>\n\n<hints>\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='<snapshot_url>?selectedSnapshot=<image_file_name>').\n- If you need to investigate a specific snapshot comparison, use get_sentry_resource with the snapshot URL.\n</hints>",
Expand Down
Loading
Loading