Skip to content

Commit 5cf522d

Browse files
committed
restore
1 parent 7a0ea88 commit 5cf522d

3 files changed

Lines changed: 98 additions & 58 deletions

File tree

‎packages/mcp-core/src/internal/formatting.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,10 @@ export function formatEventOutput(
237237
user?: z.infer<typeof EventSchema>["user"];
238238
}
239239
).user;
240-
const eventToRender = options?.replaySummary
240+
const eventWithReplayMetadataStripped = options?.replaySummary
241241
? stripReplayMetadata(event)
242242
: event;
243+
const eventToRender = eventWithReplayMetadataStripped;
243244

244245
if (options?.replaySummary) {
245246
output += formatIssueReplayOutput({
@@ -1945,7 +1946,8 @@ function formatSeerSummary(autofixState: AutofixRunState | undefined): string {
19451946
parts.push("");
19461947
}
19471948

1948-
// Summarize the solution if available, otherwise the root cause.
1949+
// Summarize from the run's artifacts: the solution if available, otherwise
1950+
// the root cause if it has been identified.
19491951
const { rootCause, solution } = getAutofixArtifactSummaries(autofix);
19501952
if (solution) {
19511953
parts.push("**Summary:**");
@@ -2111,6 +2113,9 @@ export function formatIssueOutput({
21112113

21122114
output += "## Event Details\n\n";
21132115

2116+
// Check if this is an unsupported event type
2117+
// Event type union is: ErrorEvent | DefaultEvent | TransactionEvent | GenericEvent | CspEvent
2118+
// But in practice we may have other types returned as UnknownEvent
21142119
const eventType = event.type;
21152120
if (!isSupportedEventType(event)) {
21162121
// Log to Sentry for tracking new/unknown event types

‎packages/mcp-core/src/tools/catalog/get-issue-details.test.ts‎

Lines changed: 63 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2154,18 +2154,14 @@ describe("structuredContent", () => {
21542154
);
21552155
}
21562156

2157-
function payloadOf(result: unknown): Record<string, any> {
2158-
expect(result).toHaveProperty("structuredContent");
2159-
return (result as { structuredContent: Record<string, any> })
2160-
.structuredContent;
2161-
}
2162-
21632157
it("returns a structured payload in experimental mode", async () => {
21642158
mockLatestEvent();
21652159

2166-
const payload = payloadOf(
2167-
await getIssueDetails.handler(params, experimentalContext),
2168-
);
2160+
const result = await getIssueDetails.handler(params, experimentalContext);
2161+
2162+
expect(result).toHaveProperty("structuredContent");
2163+
const payload = (result as { structuredContent: Record<string, any> })
2164+
.structuredContent;
21692165

21702166
// the issue level fields the markdown used to assemble
21712167
expect(payload.issue.shortId).toBe("CLOUDFLARE-MCP-41");
@@ -2186,9 +2182,9 @@ describe("structuredContent", () => {
21862182
it("renders the event body itself rather than asking the api for one", async () => {
21872183
mockLatestEvent();
21882184

2189-
const payload = payloadOf(
2190-
await getIssueDetails.handler(params, experimentalContext),
2191-
);
2185+
const result = await getIssueDetails.handler(params, experimentalContext);
2186+
const payload = (result as { structuredContent: Record<string, any> })
2187+
.structuredContent;
21922188

21932189
expect(typeof payload.event.body).toBe("string");
21942190
expect(payload.event.body).toContain("### Error");
@@ -2199,9 +2195,9 @@ describe("structuredContent", () => {
21992195
it("produces a payload that satisfies the schema", async () => {
22002196
mockLatestEvent();
22012197

2202-
const payload = payloadOf(
2203-
await getIssueDetails.handler(params, experimentalContext),
2204-
);
2198+
const result = await getIssueDetails.handler(params, experimentalContext);
2199+
const payload = (result as { structuredContent: Record<string, any> })
2200+
.structuredContent;
22052201

22062202
// a tool that advertises a schema has to return something that satisfies it
22072203
expect(() => getIssueDetailsOutputSchema.parse(payload)).not.toThrow();
@@ -2210,9 +2206,9 @@ describe("structuredContent", () => {
22102206
it("carries the response notes, which say which tool to call next", async () => {
22112207
mockLatestEvent();
22122208

2213-
const payload = payloadOf(
2214-
await getIssueDetails.handler(params, experimentalContext),
2215-
);
2209+
const result = await getIssueDetails.handler(params, experimentalContext);
2210+
const payload = (result as { structuredContent: Record<string, any> })
2211+
.structuredContent;
22162212

22172213
expect(payload.responseNotes.length).toBeGreaterThan(0);
22182214
const notes = payload.responseNotes.join("\n");
@@ -2223,9 +2219,9 @@ describe("structuredContent", () => {
22232219
it("carries the top level message, which the body does not render", async () => {
22242220
mockLatestEvent({ message: "TOP-LEVEL-MESSAGE", entries: [] });
22252221

2226-
const payload = payloadOf(
2227-
await getIssueDetails.handler(params, experimentalContext),
2228-
);
2222+
const result = await getIssueDetails.handler(params, experimentalContext);
2223+
const payload = (result as { structuredContent: Record<string, any> })
2224+
.structuredContent;
22292225

22302226
expect(payload.event.message).toBe("TOP-LEVEL-MESSAGE");
22312227
});
@@ -2263,18 +2259,19 @@ describe("structuredContent", () => {
22632259
),
22642260
);
22652261

2266-
const payload = payloadOf(
2267-
await getIssueDetails.handler(
2268-
{ ...params, issueId: "PERF-N1-001" },
2269-
experimentalContext,
2270-
),
2262+
const result = await getIssueDetails.handler(
2263+
{ ...params, issueId: "PERF-N1-001" },
2264+
experimentalContext,
22712265
);
2266+
const payload = (result as { structuredContent: Record<string, any> })
2267+
.structuredContent;
22722268

22732269
expect(payload.event.type).toBe("transaction");
22742270
expect(payload.event.body).toContain("Span");
22752271
});
22762272

22772273
it("keeps the attached replay, which lives on the event not the related list", async () => {
2274+
// an issue whose only replay is attached would otherwise report no replays at all
22782275
mockLatestEvent({
22792276
contexts: {
22802277
replay: {
@@ -2284,9 +2281,9 @@ describe("structuredContent", () => {
22842281
},
22852282
});
22862283

2287-
const payload = payloadOf(
2288-
await getIssueDetails.handler(params, experimentalContext),
2289-
);
2284+
const result = await getIssueDetails.handler(params, experimentalContext);
2285+
const payload = (result as { structuredContent: Record<string, any> })
2286+
.structuredContent;
22902287

22912288
expect(payload.replays?.attached).toBe("1234567890abcdef1234567890abcdef");
22922289
// and the attached id is not repeated in the related list
@@ -2301,14 +2298,16 @@ describe("structuredContent", () => {
23012298
it("reports no replays when there are none", async () => {
23022299
mockLatestEvent();
23032300

2304-
const payload = payloadOf(
2305-
await getIssueDetails.handler(params, experimentalContext),
2306-
);
2301+
const result = await getIssueDetails.handler(params, experimentalContext);
2302+
const payload = (result as { structuredContent: Record<string, any> })
2303+
.structuredContent;
23072304

23082305
expect(payload.replays).toBeNull();
23092306
});
23102307

23112308
it("maps external issues field by field so upstream extras cannot leak", async () => {
2309+
// structuredContent is a product contract, not a view of the api response: several
2310+
// upstream schemas are passthrough, so anything not mapped must not appear
23122311
mockLatestEvent();
23132312
mswServer.use(
23142313
http.get(
@@ -2327,20 +2326,22 @@ describe("structuredContent", () => {
23272326
),
23282327
);
23292328

2330-
const payload = payloadOf(
2331-
await getIssueDetails.handler(params, experimentalContext),
2332-
);
2329+
const result = await getIssueDetails.handler(params, experimentalContext);
2330+
const payload = (result as { structuredContent: Record<string, any> })
2331+
.structuredContent;
23332332

23342333
expect(JSON.stringify(payload)).not.toContain("internalOnlyToken");
23352334
expect(JSON.stringify(payload)).not.toContain("must-not-leak");
23362335
});
23372336

23382337
it("carries every field the markdown output surfaces", async () => {
2338+
// greg's bar for this migration is "roughly the same content": anything the markdown
2339+
// renders and the payload drops is a regression for every MCP user
23392340
mockLatestEvent({ dateCreated: "2026-09-03T12:00:00.000Z" });
23402341

2341-
const payload = payloadOf(
2342-
await getIssueDetails.handler(params, experimentalContext),
2343-
);
2342+
const result = await getIssueDetails.handler(params, experimentalContext);
2343+
const payload = (result as { structuredContent: Record<string, any> })
2344+
.structuredContent;
23442345

23452346
// the issue header markdown builds before the event body
23462347
for (const field of [
@@ -2365,6 +2366,8 @@ describe("structuredContent", () => {
23652366
});
23662367

23672368
it("does not label an error's exception message as a query pattern", async () => {
2369+
// metadata.value is a query pattern for a performance issue and the exception message for
2370+
// an error, so reading it unconditionally puts error text under the wrong name
23682371
mswServer.use(
23692372
http.get(
23702373
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
@@ -2383,12 +2386,15 @@ describe("structuredContent", () => {
23832386
issueCategory: "error",
23842387
}),
23852388
),
2389+
http.get(
2390+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
2391+
() => HttpResponse.json(createDefaultEvent()),
2392+
),
23862393
);
2387-
mockLatestEvent();
23882394

2389-
const payload = payloadOf(
2390-
await getIssueDetails.handler(params, experimentalContext),
2391-
);
2395+
const result = await getIssueDetails.handler(params, experimentalContext);
2396+
const payload = (result as { structuredContent: Record<string, any> })
2397+
.structuredContent;
23922398

23932399
expect(payload.issue.queryPattern).toBeNull();
23942400
expect(payload.issue.location).toBeNull();
@@ -2401,8 +2407,8 @@ describe("structuredContent", () => {
24012407
http.get(
24022408
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/CLOUDFLARE-MCP-41/",
24032409
() =>
2404-
HttpResponse.json(
2405-
createPerformanceIssue({
2410+
HttpResponse.json({
2411+
...createPerformanceIssue({
24062412
shortId: "CLOUDFLARE-MCP-41",
24072413
issueType: "performance_n_plus_one_db_queries",
24082414
issueCategory: "performance",
@@ -2412,14 +2418,17 @@ describe("structuredContent", () => {
24122418
location: "/api/checkout",
24132419
},
24142420
}),
2415-
),
2421+
}),
2422+
),
2423+
http.get(
2424+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
2425+
() => HttpResponse.json(createDefaultEvent()),
24162426
),
24172427
);
2418-
mockLatestEvent();
24192428

2420-
const payload = payloadOf(
2421-
await getIssueDetails.handler(params, experimentalContext),
2422-
);
2429+
const result = await getIssueDetails.handler(params, experimentalContext);
2430+
const payload = (result as { structuredContent: Record<string, any> })
2431+
.structuredContent;
24232432

24242433
expect(payload.issue.title).toBe("N+1 Query");
24252434
expect(payload.issue.queryPattern).toBe("SELECT * FROM users WHERE id = ?");
@@ -2433,7 +2442,8 @@ describe("structuredContent", () => {
24332442
);
24342443
mockLatestEvent();
24352444
mswServer.use(
2436-
// echo back whichever issue id was asked for
2445+
// related ids come from replay-count, keyed by numeric issue id. Echo back whichever
2446+
// id was asked for: a preceding test can leave a different issue fixture registered.
24372447
http.get(
24382448
"https://sentry.io/api/0/organizations/sentry-mcp-evals/replay-count/",
24392449
({ request }) => {
@@ -2444,9 +2454,9 @@ describe("structuredContent", () => {
24442454
),
24452455
);
24462456

2447-
const payload = payloadOf(
2448-
await getIssueDetails.handler(params, experimentalContext),
2449-
);
2457+
const result = await getIssueDetails.handler(params, experimentalContext);
2458+
const payload = (result as { structuredContent: Record<string, any> })
2459+
.structuredContent;
24502460

24512461
expect(payload.replays).not.toBeNull();
24522462
expect(payload.replays.relatedCount).toBe(51);

‎packages/mcp-core/src/tools/catalog/get-issue-details.ts‎

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,14 @@ const MAX_AI_CONVERSATION_MATCHES = 3;
5757
const AI_CONVERSATION_LOOKUP_WINDOW_MS = 24 * 60 * 60 * 1000;
5858
const TRACE_ID_PATTERN = /^[0-9a-fA-F]{32}$/;
5959

60-
/** The issue payload as `structuredContent`, mapped field by field so api passthrough fields can't leak. */
60+
/**
61+
* The issue payload as `structuredContent`.
62+
*
63+
* Every field is mapped explicitly rather than spread from an api response, so a passthrough
64+
* upstream schema cannot leak backend-only fields into the public interface.
65+
*
66+
* `event.body` is this server's own markdown rendering of the event.
67+
*/
6168
export const getIssueDetailsOutputSchema = z.object({
6269
issue: z.object({
6370
shortId: z.string(),
@@ -144,7 +151,15 @@ export type GetIssueDetailsPayload = z.infer<
144151
typeof getIssueDetailsOutputSchema
145152
>;
146153

147-
// dateCreated isn't on every event type
154+
/**
155+
* The attached replay plus the related ones, derived the way the markdown output derives them.
156+
* The attached id lives on the event rather than in the related list, so passing that list
157+
* through alone loses the replay for an issue whose only one is attached.
158+
*/
159+
/**
160+
* ``dateCreated`` sits on the error, default, generic and csp event types rather than the base
161+
* union, and the markdown path normalizes it to ISO. Read it the same way and tolerate a bad value.
162+
*/
148163
function eventOccurredAt(event: Event): string | null {
149164
const raw = "dateCreated" in event ? event.dateCreated : null;
150165
if (typeof raw !== "string") {
@@ -199,12 +214,15 @@ function buildIssueDetailsPayload({
199214
directToolNames,
200215
}: IssueDetailsArgs): GetIssueDetailsPayload {
201216
const autofix = autofixState?.autofix;
217+
// the run's own artifacts, not the whole state: an AutofixRunState carries every step and
218+
// would dwarf the rest of the payload
202219
const summaries = autofix ? getAutofixArtifactSummaries(autofix) : undefined;
203220
const isPerf = isPerformanceIssueType(issue) && !!issue.metadata;
204221

205222
return {
206223
issue: {
207224
shortId: issue.shortId,
225+
// a performance issue's metadata carries the better title, as the markdown path prefers
208226
title: (isPerf ? issue.metadata?.title : null) || issue.title,
209227
culprit: issue.culprit,
210228
firstSeen: issue.firstSeen,
@@ -226,6 +244,8 @@ function buildIssueDetailsPayload({
226244
platform: issue.platform,
227245
project: issue.project?.name,
228246
url: apiService.getIssueUrl(organizationSlug, issue.shortId),
247+
// metadata.value is a query pattern only for a performance issue; on an error it is the
248+
// exception message, so reading it unconditionally would misname the error text
229249
location: isPerf ? issue.metadata?.location : null,
230250
queryPattern: isPerf ? issue.metadata?.value : null,
231251
},
@@ -257,6 +277,8 @@ function buildIssueDetailsPayload({
257277
: null,
258278
replays: buildReplays(event, relatedReplayIds),
259279
suspectCommit: getSuspectCommit(committers),
280+
// mapped field by field, not handed through: several upstream schemas are passthrough, and
281+
// structuredContent is a product contract rather than a view of the api response
260282
externalIssues: externalIssues?.length
261283
? externalIssues.map((issue) => ({
262284
id: String(issue.id),
@@ -337,7 +359,10 @@ export default defineTool({
337359
eventId: ParamEventId.optional(),
338360
issueUrl: ParamIssueUrl.optional(),
339361
},
340-
// no outputSchema until every success path returns structuredContent
362+
// outputSchema is deliberately not declared yet. tools/list would export it immediately,
363+
// while a session outside experimental mode still gets a markdown result with
364+
// no structuredContent -- advertising a schema that some success paths cannot satisfy. Wire
365+
// it up once every success path returns structuredContent.
341366
annotations: {
342367
readOnlyHint: true,
343368
destructiveHint: false,

0 commit comments

Comments
 (0)