diff --git a/docs/contributing/api-patterns.md b/docs/contributing/api-patterns.md index ecbb46607..58e592be4 100644 --- a/docs/contributing/api-patterns.md +++ b/docs/contributing/api-patterns.md @@ -63,6 +63,11 @@ await api.issues.update({ }); ``` +### Resolved Issue IDs + +Use `String(issue.id)` for API requests after resolving an issue. Short IDs can +fail to resolve for legacy mixed-case project slugs; keep them for display. + ### Multi-Region Support Sentry uses region-specific URLs: diff --git a/packages/mcp-core/src/tools/catalog/get-issue-details.numeric-id.test.ts b/packages/mcp-core/src/tools/catalog/get-issue-details.numeric-id.test.ts new file mode 100644 index 000000000..506553756 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/get-issue-details.numeric-id.test.ts @@ -0,0 +1,95 @@ +import { + autofixStateFixture, + eventsFixture, + issueFixture, + mswServer, +} from "@sentry/mcp-server-mocks"; +import { http, HttpResponse } from "msw"; +import { describe, expect, it } from "vitest"; +import getIssueDetails from "./get-issue-details"; +import getSentryResource from "./get-sentry-resource"; + +const organizationSlug = "sentry-mcp-evals"; +const issueId = String(issueFixture.id); +const eventId = eventsFixture.id; +const issueUrl = `https://${organizationSlug}.sentry.io/issues/${issueId}/`; +const context = { + constraints: { organizationSlug: undefined }, + accessToken: "access-token", + userId: "1", +}; + +describe("issue details with an unresolvable short ID", () => { + it.each([ + { + name: "numeric issue ID", + call: () => + getIssueDetails.handler( + { organizationSlug, issueId, regionUrl: null }, + context, + ), + }, + { + name: "issue URL", + call: () => + getIssueDetails.handler({ issueUrl, regionUrl: null }, context), + }, + { + name: "numeric issue ID and explicit event ID", + call: () => + getIssueDetails.handler( + { organizationSlug, issueId, eventId, regionUrl: null }, + context, + ), + }, + { + name: "event ID resolved through issue search", + call: () => + getIssueDetails.handler( + { organizationSlug, eventId, regionUrl: null }, + context, + ), + }, + { + name: "resource issue URL", + call: () => getSentryResource.handler({ url: issueUrl }, context), + }, + { + name: "resource event URL", + call: () => + getSentryResource.handler( + { url: `${issueUrl}events/${eventId}/` }, + context, + ), + }, + ])("loads $name and enrichment by numeric ID", async ({ call }) => { + const base = `https://sentry.io/api/0/organizations/${organizationSlug}`; + mswServer.use( + http.get(`${base}/issues/`, () => HttpResponse.json([issueFixture])), + http.get(`${base}/issues/${issueFixture.shortId}/*`, () => + HttpResponse.json({ detail: "Not found" }, { status: 404 }), + ), + http.get(`${base}/issues/${issueId}/autofix/`, () => + HttpResponse.json(autofixStateFixture), + ), + http.get(`${base}/issues/${issueId}/external-issues/`, () => + HttpResponse.json([ + { + id: "123", + issueId, + serviceType: "github", + displayName: "example/app#123", + webUrl: "https://github.com/example/app/issues/123", + }, + ]), + ), + ); + + const result = await call(); + + expect(result).toContain(`Issue ${issueFixture.shortId}`); + expect(result).toContain(eventsFixture.id); + expect(result).toContain("## Seer Analysis"); + expect(result).toContain("**example/app#123** (github)"); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/get-issue-details.test.ts b/packages/mcp-core/src/tools/catalog/get-issue-details.test.ts index 8715c80ed..abbb465dd 100644 --- a/packages/mcp-core/src/tools/catalog/get-issue-details.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-issue-details.test.ts @@ -370,7 +370,7 @@ describe("get_issue_details", () => { } mswServer.use( http.get( - `${base}/events/latest/`, + `https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/${issue ? issue().id : "6507376925"}/events/latest/`, () => HttpResponse.json({ ...event(), @@ -411,7 +411,7 @@ describe("get_issue_details", () => { { once: true }, ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/", () => HttpResponse.json({ ...createPerformanceEvent(), @@ -448,7 +448,7 @@ describe("get_issue_details", () => { it("keeps the replay note when error events use formatted.content", async () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -485,7 +485,7 @@ describe("get_issue_details", () => { it("embeds the shared formatter's analysis in the Seer section when present", async () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => HttpResponse.json({ autofix: { run_id: 7, status: "completed", blocks: [] }, @@ -536,7 +536,7 @@ describe("get_issue_details", () => { async ({ status, expected }) => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => HttpResponse.json({ autofix: { run_id: 7, status, blocks: [] }, @@ -580,7 +580,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json(event), { once: true }, ), @@ -712,7 +712,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json(event), { once: true }, ), @@ -752,7 +752,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json(event), { once: true }, ), @@ -816,7 +816,7 @@ describe("get_issue_details", () => { { once: true }, ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/TEAM-ISSUE-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/123456789/events/latest/", () => HttpResponse.json(createDefaultEvent()), { once: true }, ), @@ -840,7 +840,7 @@ describe("get_issue_details", () => { it("lists threads and stacktrace lookup guidance only for multi-thread events", async () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json( createDefaultEvent({ @@ -936,7 +936,7 @@ describe("get_issue_details", () => { it("omits thread list and stacktrace lookup guidance for single-thread events", async () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json( createDefaultEvent({ @@ -1005,7 +1005,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json(event), { once: true }, ), @@ -1175,7 +1175,7 @@ describe("get_issue_details", () => { { once: true }, ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/", () => { // Create event with specific evidence data for this test const event = createPerformanceEvent(); @@ -1265,7 +1265,7 @@ describe("get_issue_details", () => { { once: true }, ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/PERF-N1-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/", () => { // Create event with specific evidence data for this test const event = createPerformanceEvent(); @@ -1556,7 +1556,7 @@ describe("get_issue_details", () => { let autofixRequested = false; mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => { autofixRequested = true; return HttpResponse.json({ autofix: null }); @@ -1586,7 +1586,7 @@ describe("get_issue_details", () => { let autofixRequested = false; mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => { autofixRequested = true; return HttpResponse.json({ autofix: null }); @@ -1641,7 +1641,7 @@ describe("get_issue_details", () => { // Use mswServer.use to prepend a handler - MSW uses LIFO order mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => HttpResponse.json(inProgressFixture), { once: true }, // Ensure this handler is only used once for this test ), @@ -1684,7 +1684,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => HttpResponse.json(failedFixture), { once: true }, ), @@ -1739,7 +1739,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/autofix/", () => HttpResponse.json(needsInfoFixture), { once: true }, ), @@ -1778,7 +1778,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/DEFAULT-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/123456/events/latest/", () => HttpResponse.json(defaultEvent), ), http.get( @@ -1842,7 +1842,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/BLOG-CSP-4XC/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/4256774711/events/latest/", () => HttpResponse.json(cspEvent), ), http.get( @@ -1924,7 +1924,7 @@ describe("get_issue_details", () => { }), ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/MALFORMED-TAGS-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/123456/events/latest/", () => HttpResponse.json(eventWithMalformedTags), ), ); @@ -2014,7 +2014,7 @@ describe("get_issue_details", () => { }, ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CONTEXT-001/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/123456/events/latest/", () => { return HttpResponse.json(eventWithContext); }, @@ -2070,7 +2070,7 @@ describe("get_issue_details", () => { { once: true }, ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/MCP-SERVER-EQE/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6898891101/events/latest/", () => HttpResponse.json(regressedEventFixture), { once: true }, ), @@ -2152,7 +2152,7 @@ describe("get_issue_details", () => { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/external-issues/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/external-issues/", () => HttpResponse.json(mockExternalIssues), { once: true }, ), @@ -2216,7 +2216,7 @@ describe("get_issue_details", () => { mswServer.use( // More specific pattern for events (must come first to match before the issue pattern) http.get( - "https://sentry.io/api/0/organizations/*/issues/FUTURE-TYPE-001/events/latest/", + "https://sentry.io/api/0/organizations/*/issues/7777777777/events/latest/", () => { return HttpResponse.json(unsupportedEventFixture); }, @@ -2342,7 +2342,7 @@ describe("structuredContent", () => { function mockLatestEventWithFormatted(formatted: unknown) { mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), formatted }), ), ); @@ -2411,7 +2411,7 @@ describe("structuredContent", () => { // rendered for transactions, so a transaction must not take the structured path mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -2431,7 +2431,7 @@ describe("structuredContent", () => { // an issue whose only replay is attached would otherwise report no replays at all mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -2472,7 +2472,7 @@ describe("structuredContent", () => { // upstream schemas are passthrough, so anything not mapped must not appear mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -2480,7 +2480,7 @@ describe("structuredContent", () => { }), ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/external-issues/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/external-issues/", () => HttpResponse.json([ { @@ -2508,7 +2508,7 @@ describe("structuredContent", () => { // renders and the payload drops is a regression for every MCP user mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -2566,7 +2566,7 @@ describe("structuredContent", () => { }), ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -2604,7 +2604,7 @@ describe("structuredContent", () => { }), ), http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), @@ -2629,7 +2629,7 @@ describe("structuredContent", () => { ); mswServer.use( http.get( - "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/events/latest/", + "https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/", () => HttpResponse.json({ ...createDefaultEvent(), diff --git a/packages/mcp-core/src/tools/catalog/get-issue-details.ts b/packages/mcp-core/src/tools/catalog/get-issue-details.ts index 8eb9178b7..6d2f910c1 100644 --- a/packages/mcp-core/src/tools/catalog/get-issue-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-issue-details.ts @@ -406,7 +406,7 @@ export default defineTool({ apiService .getEventForIssue({ organizationSlug: orgSlug, - issueId: issue.shortId, + issueId: String(issue.id), eventId, }) // Optionally enhance 404 errors with parameter context @@ -414,7 +414,7 @@ export default defineTool({ if (error instanceof ApiNotFoundError) { throw enhanceNotFoundError(error, { organizationSlug: orgSlug, - issueId: issue.shortId, + issueId: String(issue.id), eventId, }); } @@ -524,7 +524,7 @@ export default defineTool({ apiService .getLatestEventForIssue({ organizationSlug: orgSlug, - issueId: issue.shortId, + issueId: String(issue.id), }) .then(async (event) => ({ event, @@ -648,7 +648,7 @@ async function fetchIssueEnrichmentData({ seerEnabled, }), apiService - .getIssueExternalLinks({ organizationSlug, issueId: issue.shortId }) + .getIssueExternalLinks({ organizationSlug, issueId }) .catch(() => undefined), apiService .listReplayIdsForIssue({ @@ -691,7 +691,7 @@ async function maybeFetchAutofixState({ } return apiService - .getAutofixState({ organizationSlug, issueId: issue.shortId }) + .getAutofixState({ organizationSlug, issueId: String(issue.id) }) .catch(() => undefined); } diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index b0629e08a..2c505f596 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -1334,6 +1334,11 @@ export const restHandlers = buildHandlers([ path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/autofix/", fetch: () => HttpResponse.json({ autofix: null }), }, + { + method: "get", + path: "/api/0/organizations/sentry-mcp-evals/issues/:issueId(\\d+)/autofix/", + fetch: () => HttpResponse.json({ autofix: null }), + }, { method: "post", path: "/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-42/autofix/",