From 6cf06b405b5622d12c9a4a364472383e6f9982dd Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:11:38 +0000 Subject: [PATCH 1/2] feat(tools): Return reporter name, email, and IP from get_issue_user_reports The tool collapsed the report and event user into one display string. Return the report name and email, plus the event user's id, username, name, email, and IP address, as separate fields. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com> --- packages/mcp-core/src/toolDefinitions.json | 49 +++++++++++++- .../catalog/get-issue-user-reports.test.ts | 35 +++++++--- .../tools/catalog/get-issue-user-reports.ts | 64 +++++++++++++------ 3 files changed, 118 insertions(+), 30 deletions(-) diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 83613f8d5..2f084ea50 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -5312,8 +5312,49 @@ "eventId": { "type": "string" }, - "reporter": { - "type": "string" + "name": { + "description": "Name on the report, falling back to the event user.", + "type": ["string", "null"] + }, + "email": { + "description": "Email on the report, falling back to the event user.", + "type": ["string", "null"] + }, + "user": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": ["string", "null"] + }, + "username": { + "type": ["string", "null"] + }, + "name": { + "type": ["string", "null"] + }, + "email": { + "type": ["string", "null"] + }, + "ipAddress": { + "type": ["string", "null"] + } + }, + "required": [ + "id", + "username", + "name", + "email", + "ipAddress" + ], + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "The user on the linked event. Null when the event has none." }, "comments": { "type": "string" @@ -5325,7 +5366,9 @@ "required": [ "id", "eventId", - "reporter", + "name", + "email", + "user", "comments", "dateCreated" ], 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 index fb7251637..2e99b4c2e 100644 --- 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 @@ -51,30 +51,42 @@ describe("get_issue_user_reports", () => { { "comments": "i am currently testing it", "dateCreated": "2026-06-22T02:37:27.878Z", + "email": "reporter@example.com", "eventId": "957c1ee8f31e4e68a57b637dad557b32", "id": "11974227", - "reporter": "Example Reporter", + "name": "Example Reporter", + "user": { + "email": "reporter@example.com", + "id": "user-123", + "ipAddress": "203.0.113.10", + "name": "Example Reporter", + "username": "example-reporter", + }, }, { "comments": "anonymous report with no event user", "dateCreated": "2026-06-22T03:10:11.123Z", + "email": null, "eventId": "057c1ee8f31e4e68a57b637dad557b33", "id": "11974228", - "reporter": "anonymous", + "name": null, + "user": null, }, { "comments": "event user shell without identity", "dateCreated": "2026-06-22T03:45:12.456Z", + "email": null, "eventId": "157c1ee8f31e4e68a57b637dad557b34", "id": "11974229", - "reporter": "anonymous", + "name": null, + "user": null, }, ], } `); }); - it("returns the next cursor without leaking IP address or avatar URL", async () => { + it("returns the event user's identity and IP address with the next cursor", async () => { mswServer.use( http.get( "https://sentry.io/api/0/organizations/:org/issues/:issueId/user-reports/", @@ -136,15 +148,22 @@ describe("get_issue_user_reports", () => { { "comments": "reported by event user", "dateCreated": "2026-06-22T04:20:21.987Z", + "email": null, "eventId": "157c1ee8f31e4e68a57b637dad557b34", "id": "11974229", - "reporter": "event-user", + "name": null, + "user": { + "email": null, + "id": "user-456", + "ipAddress": "203.0.113.7", + "name": null, + "username": "event-user", + }, }, ], } `); - const serialized = JSON.stringify(structuredContent); - expect(serialized).not.toContain("203.0.113.7"); - expect(serialized).not.toContain("avatar.png"); + // The avatar URL is presentation data, not user data the report stored. + expect(JSON.stringify(structuredContent)).not.toContain("avatar.png"); }); }); 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 index 7993e0fa8..0a260fec9 100644 --- a/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts +++ b/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts @@ -17,24 +17,39 @@ import { setTargetTagsAndAttributes } from "../../telem/scope"; import type { ServerContext } from "../../types"; 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" - ); +type UserReport = UserReportList[number]; + +function orNull(value: string | null | undefined): string | null { + return value?.trim() || null; } +function mapEventUser(user: UserReport["user"]) { + if (!user) { + return null; + } + const id = orNull(user.id); + const mapped = { + // Older Sentry serializers can emit sentinel strings when no user ID exists. + id: id && !["none", "null"].includes(id.toLowerCase()) ? id : null, + username: orNull(user.username), + name: orNull(user.name), + email: orNull(user.email), + ipAddress: orNull(user.ipAddress), + }; + return Object.values(mapped).some((value) => value !== null) ? mapped : null; +} + +const eventUserSchema = z + .object({ + id: z.string().nullable(), + username: z.string().nullable(), + name: z.string().nullable(), + email: z.string().nullable(), + ipAddress: z.string().nullable(), + }) + .nullable() + .describe("The user on the linked event. Null when the event has none."); + export const getIssueUserReportsOutputSchema = z.object({ organizationSlug: z.string(), issueId: z.string(), @@ -42,7 +57,15 @@ export const getIssueUserReportsOutputSchema = z.object({ z.object({ id: z.string(), eventId: z.string(), - reporter: z.string(), + name: z + .string() + .nullable() + .describe("Name on the report, falling back to the event user."), + email: z + .string() + .nullable() + .describe("Email on the report, falling back to the event user."), + user: eventUserSchema, comments: z.string(), dateCreated: z.string().nullable(), }), @@ -53,11 +76,14 @@ export const getIssueUserReportsOutputSchema = z.object({ .describe("Pass as cursor with the same issue scope for the next page."), }); -function mapUserReport(report: UserReportList[number]) { +function mapUserReport(report: UserReport) { + const user = mapEventUser(report.user); return { id: report.id, eventId: report.eventID, - reporter: getReporter(report), + name: orNull(report.name) ?? user?.name ?? null, + email: orNull(report.email) ?? user?.email ?? null, + user, comments: report.comments, dateCreated: formatDate(report.dateCreated), }; From 9c115ecd3c2f5894b7058c5c8eac324e8b7c72ea Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 5 Nov 2026 22:59:57 +0000 Subject: [PATCH 2/2] fix(tools): Null get_issue_user_reports names that only repeat the email or username Sentry fills report and event-user names from the email or username when no real name exists. Return null for those so name holds only a real name. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com> --- packages/mcp-core/src/toolDefinitions.json | 3 +- .../catalog/get-issue-user-reports.test.ts | 62 ++++++++++++++++++- .../tools/catalog/get-issue-user-reports.ts | 47 +++++++++++--- 3 files changed, 102 insertions(+), 10 deletions(-) diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 2f084ea50..568333fb4 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -5313,7 +5313,7 @@ "type": "string" }, "name": { - "description": "Name on the report, falling back to the event user.", + "description": "Name the reporter entered, falling back to the event user's name. Null when Sentry only has an email or username.", "type": ["string", "null"] }, "email": { @@ -5332,6 +5332,7 @@ "type": ["string", "null"] }, "name": { + "description": "Null when Sentry only has an email or username.", "type": ["string", "null"] }, "email": { 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 index 2e99b4c2e..ad829f513 100644 --- 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 @@ -99,7 +99,9 @@ describe("get_issue_user_reports", () => { { id: "11974229", eventID: "157c1ee8f31e4e68a57b637dad557b34", - name: "", + // Sentry fills both names from the username when no real + // name exists. + name: "event-user", email: "", comments: "reported by event user", dateCreated: "2026-06-22T04:20:21.987654Z", @@ -107,7 +109,7 @@ describe("get_issue_user_reports", () => { id: "user-456", username: "event-user", email: null, - name: null, + name: "event-user", ipAddress: "203.0.113.7", avatarUrl: "https://example.com/avatar.png", }, @@ -166,4 +168,60 @@ describe("get_issue_user_reports", () => { // The avatar URL is presentation data, not user data the report stored. expect(JSON.stringify(structuredContent)).not.toContain("avatar.png"); }); + + it("returns null names when Sentry only copied the reporter email", async () => { + mswServer.use( + http.get( + "https://sentry.io/api/0/organizations/:org/issues/:issueId/user-reports/", + () => + HttpResponse.json([ + { + id: "11974230", + eventID: "257c1ee8f31e4e68a57b637dad557b35", + name: "jane@example.com", + email: "jane@example.com", + comments: "no name given", + dateCreated: "2026-06-22T05:00:00.000000Z", + user: { + id: "user-789", + username: null, + email: "jane@example.com", + name: "jane@example.com", + ipAddress: "198.51.100.4", + avatarUrl: null, + }, + event: { + id: "257c1ee8f31e4e68a57b637dad557b35", + eventID: "257c1ee8f31e4e68a57b637dad557b35", + }, + }, + ]), + ), + ); + + const result = await getIssueUserReports.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + issueId: "CLOUDFLARE-MCP-41", + cursor: null, + limit: 25, + }, + context, + ); + + const [report] = getIssueUserReportsOutputSchema.parse( + getStructuredContent(result), + ).reports; + expect(report).toMatchObject({ + name: null, + email: "jane@example.com", + user: { + id: "user-789", + name: null, + email: "jane@example.com", + ipAddress: "198.51.100.4", + }, + }); + }); }); 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 index 0a260fec9..4c197ea1d 100644 --- a/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts +++ b/packages/mcp-core/src/tools/catalog/get-issue-user-reports.ts @@ -23,17 +23,37 @@ function orNull(value: string | null | undefined): string | null { return value?.trim() || null; } +/** + * Sentry fills display names from the email or username when no real name + * exists, so treat a name that just repeats one of those as missing. + */ +function realName( + value: string | null | undefined, + identities: Array, +): string | null { + const name = orNull(value); + if (!name) { + return null; + } + const lowered = name.toLowerCase(); + return identities.some((identity) => identity?.toLowerCase() === lowered) + ? null + : name; +} + function mapEventUser(user: UserReport["user"]) { if (!user) { return null; } const id = orNull(user.id); + const username = orNull(user.username); + const email = orNull(user.email); const mapped = { // Older Sentry serializers can emit sentinel strings when no user ID exists. id: id && !["none", "null"].includes(id.toLowerCase()) ? id : null, - username: orNull(user.username), - name: orNull(user.name), - email: orNull(user.email), + username, + name: realName(user.name, [email, username]), + email, ipAddress: orNull(user.ipAddress), }; return Object.values(mapped).some((value) => value !== null) ? mapped : null; @@ -43,7 +63,10 @@ const eventUserSchema = z .object({ id: z.string().nullable(), username: z.string().nullable(), - name: z.string().nullable(), + name: z + .string() + .nullable() + .describe("Null when Sentry only has an email or username."), email: z.string().nullable(), ipAddress: z.string().nullable(), }) @@ -60,7 +83,9 @@ export const getIssueUserReportsOutputSchema = z.object({ name: z .string() .nullable() - .describe("Name on the report, falling back to the event user."), + .describe( + "Name the reporter entered, falling back to the event user's name. Null when Sentry only has an email or username.", + ), email: z .string() .nullable() @@ -78,11 +103,19 @@ export const getIssueUserReportsOutputSchema = z.object({ function mapUserReport(report: UserReport) { const user = mapEventUser(report.user); + const email = orNull(report.email) ?? user?.email ?? null; return { id: report.id, eventId: report.eventID, - name: orNull(report.name) ?? user?.name ?? null, - email: orNull(report.email) ?? user?.email ?? null, + name: + realName(report.name, [ + email, + user?.email ?? null, + user?.username ?? null, + ]) ?? + user?.name ?? + null, + email, user, comments: report.comments, dateCreated: formatDate(report.dateCreated),