Skip to content

Commit 78dd35a

Browse files
committed
fix
1 parent 5cf522d commit 78dd35a

3 files changed

Lines changed: 111 additions & 138 deletions

File tree

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

Lines changed: 32 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -2117,7 +2117,14 @@ export function formatIssueOutput({
21172117
// Event type union is: ErrorEvent | DefaultEvent | TransactionEvent | GenericEvent | CspEvent
21182118
// But in practice we may have other types returned as UnknownEvent
21192119
const eventType = event.type;
2120-
if (!isSupportedEventType(event)) {
2120+
const isUnsupportedType =
2121+
eventType !== "error" &&
2122+
eventType !== "default" &&
2123+
eventType !== "transaction" &&
2124+
eventType !== "generic" &&
2125+
eventType !== "csp";
2126+
2127+
if (isUnsupportedType) {
21212128
// Log to Sentry for tracking new/unknown event types
21222129
const sentryEventId = logIssue(
21232130
`Unsupported event type encountered: ${String(eventType)}`,
@@ -2148,15 +2155,13 @@ export function formatIssueOutput({
21482155

21492156
if (aiConversations && aiConversations.length > 0) {
21502157
output += "\n## Response Notes\n\n";
2151-
for (const note of buildAIConversationResponseNotes({
2158+
output += formatAIConversationResponseNote({
21522159
aiConversations,
21532160
organizationSlug,
21542161
experimentalMode: experimentalMode ?? false,
21552162
availableToolNames,
21562163
directToolNames,
2157-
})) {
2158-
output += `- ${note}\n`;
2159-
}
2164+
});
21602165
}
21612166

21622167
// For unsupported event types, return early without trying to render event details
@@ -2211,67 +2216,27 @@ export function formatIssueOutput({
22112216
output += "\n";
22122217
}
22132218

2214-
output += "## Response Notes\n\n";
2215-
for (const note of buildIssueResponseNotes({
2216-
organizationSlug,
2217-
issue,
2218-
event,
2219-
apiService,
2220-
aiConversations,
2221-
experimentalMode,
2222-
availableToolNames,
2223-
directToolNames,
2224-
})) {
2225-
output += `- ${note}\n`;
2226-
}
2227-
return output;
2228-
}
2229-
2230-
export function buildIssueResponseNotes({
2231-
organizationSlug,
2232-
issue,
2233-
event,
2234-
apiService,
2235-
aiConversations,
2236-
experimentalMode,
2237-
availableToolNames,
2238-
directToolNames,
2239-
}: {
2240-
organizationSlug: string;
2241-
issue: Issue;
2242-
event: Event;
2243-
apiService: SentryApiService;
2244-
aiConversations?: AIConversationReference[];
2245-
experimentalMode?: boolean;
2246-
availableToolNames?: ReadonlySet<string>;
2247-
directToolNames?: ReadonlySet<string>;
2248-
}): string[] {
2249-
const notes: string[] = [];
22502219
const traceId =
22512220
typeof event.contexts?.trace?.trace_id === "string" &&
22522221
event.contexts.trace.trace_id.length > 0
22532222
? event.contexts.trace.trace_id
22542223
: undefined;
22552224

2225+
output += "## Response Notes\n\n";
22562226
const commitIssueReference = /^\d+$/.test(issue.shortId)
22572227
? apiService.getIssueUrl(organizationSlug, issue.shortId)
22582228
: issue.shortId;
2259-
notes.push(
2260-
`Commit message issue reference: \`Fixes ${commitIssueReference}\` automatically closes the issue when the commit is merged.`,
2261-
);
2262-
notes.push(
2263-
"The stacktrace includes first-party application code and third-party code. First-party frames are usually the best starting point for triage.",
2264-
);
2229+
output += `- Commit message issue reference: \`Fixes ${commitIssueReference}\` automatically closes the issue when the commit is merged.\n`;
2230+
output +=
2231+
"- The stacktrace includes first-party application code and third-party code. First-party frames are usually the best starting point for triage.\n";
22652232
if (aiConversations && aiConversations.length > 0) {
2266-
notes.push(
2267-
...buildAIConversationResponseNotes({
2268-
aiConversations,
2269-
organizationSlug,
2270-
experimentalMode: experimentalMode ?? false,
2271-
availableToolNames,
2272-
directToolNames,
2273-
}),
2274-
);
2233+
output += formatAIConversationResponseNote({
2234+
aiConversations,
2235+
organizationSlug,
2236+
experimentalMode: experimentalMode ?? false,
2237+
availableToolNames,
2238+
directToolNames,
2239+
});
22752240
}
22762241
const issueEventSearchInstruction = formatToolCallInstruction({
22772242
toolName: "search_issue_events",
@@ -2285,7 +2250,7 @@ export function buildIssueResponseNotes({
22852250
directToolNames,
22862251
fallbackInstruction: "Issue event search is not available in this session",
22872252
});
2288-
notes.push(`Issue event search: ${issueEventSearchInstruction}`);
2253+
output += `- Issue event search: ${issueEventSearchInstruction}\n`;
22892254
const hasMultipleThreads = event.entries?.some((entry) => {
22902255
if (entry.type !== "threads") {
22912256
return false;
@@ -2310,7 +2275,7 @@ export function buildIssueResponseNotes({
23102275
"to fetch a full thread stacktrace by numeric Thread ID or exact thread Name. Omit `thread` to use Sentry's default selected thread",
23112276
});
23122277
if (stacktraceInstruction) {
2313-
notes.push(`Thread stacktrace lookup: ${stacktraceInstruction}`);
2278+
output += `- Thread stacktrace lookup: ${stacktraceInstruction}\n`;
23142279
}
23152280
}
23162281
if (traceId) {
@@ -2351,11 +2316,9 @@ export function buildIssueResponseNotes({
23512316
fallbackInstruction:
23522317
"Related log search is not available in this session",
23532318
});
2354-
notes.push(
2355-
`Full distributed trace and span tree: ${traceDetailsInstruction}`,
2356-
);
2357-
notes.push(`Related span search: ${spanSearchInstruction}`);
2358-
notes.push(`Related log search: ${logSearchInstruction}`);
2319+
output += `- Full distributed trace and span tree: ${traceDetailsInstruction}\n`;
2320+
output += `- Related span search: ${spanSearchInstruction}\n`;
2321+
output += `- Related log search: ${logSearchInstruction}\n`;
23592322
}
23602323
if (experimentalMode) {
23612324
const breadcrumbsInstruction = formatToolCallInstruction({
@@ -2369,14 +2332,12 @@ export function buildIssueResponseNotes({
23692332
fallbackInstruction:
23702333
"Issue breadcrumbs are not available in this session",
23712334
});
2372-
notes.push(
2373-
`Breadcrumb trail leading up to this error: ${breadcrumbsInstruction}`,
2374-
);
2335+
output += `- Breadcrumb trail leading up to this error: ${breadcrumbsInstruction}\n`;
23752336
}
2376-
return notes;
2337+
return output;
23772338
}
23782339

2379-
function buildAIConversationResponseNotes({
2340+
function formatAIConversationResponseNote({
23802341
aiConversations,
23812342
organizationSlug,
23822343
experimentalMode,
@@ -2388,7 +2349,7 @@ function buildAIConversationResponseNotes({
23882349
experimentalMode: boolean;
23892350
availableToolNames?: ReadonlySet<string>;
23902351
directToolNames?: ReadonlySet<string>;
2391-
}): string[] {
2352+
}): string {
23922353
const instructions = formatAIConversationActionInstructions({
23932354
organizationSlug,
23942355
aiConversations,
@@ -2402,19 +2363,13 @@ function buildAIConversationResponseNotes({
24022363
const spanSuffix = conversation.spanId
24032364
? ` Matching span: \`${conversation.spanId}\`.`
24042365
: "";
2405-
return [
2406-
`Agent conversation found in this trace: \`${conversation.conversationId}\`.${spanSuffix}`,
2407-
...instructions,
2408-
];
2366+
return `- Agent conversation found in this trace: \`${conversation.conversationId}\`.${spanSuffix}\n${instructions.map((instruction) => `- ${instruction}`).join("\n")}\n`;
24092367
}
24102368

24112369
const conversationIds = aiConversations
24122370
.map((conversation) => `\`${conversation.conversationId}\``)
24132371
.join(", ");
2414-
return [
2415-
`Multiple agent conversations were found in this trace: ${conversationIds}.`,
2416-
...instructions,
2417-
];
2372+
return `- Multiple agent conversations were found in this trace: ${conversationIds}.\n${instructions.map((instruction) => `- ${instruction}`).join("\n")}\n`;
24182373
}
24192374

24202375
const MAX_DISPLAY_REPLAYS = 5;

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

Lines changed: 66 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,14 +2137,6 @@ describe("get_issue_details", () => {
21372137
describe("structuredContent", () => {
21382138
const experimentalContext = { ...baseContext, experimentalMode: true };
21392139

2140-
const params = {
2141-
organizationSlug: "sentry-mcp-evals",
2142-
issueId: "CLOUDFLARE-MCP-41",
2143-
eventId: undefined,
2144-
issueUrl: undefined,
2145-
regionUrl: null,
2146-
};
2147-
21482140
function mockLatestEvent(overrides: Record<string, unknown> = {}) {
21492141
mswServer.use(
21502142
http.get(
@@ -2154,6 +2146,14 @@ describe("structuredContent", () => {
21542146
);
21552147
}
21562148

2149+
const params = {
2150+
organizationSlug: "sentry-mcp-evals",
2151+
issueId: "CLOUDFLARE-MCP-41",
2152+
eventId: undefined,
2153+
issueUrl: undefined,
2154+
regionUrl: null,
2155+
};
2156+
21572157
it("returns a structured payload in experimental mode", async () => {
21582158
mockLatestEvent();
21592159

@@ -2170,6 +2170,17 @@ describe("structuredContent", () => {
21702170
expect(typeof payload.issue.usersImpacted).toBe("number");
21712171
});
21722172

2173+
it("produces a payload that satisfies the schema", async () => {
2174+
mockLatestEvent();
2175+
2176+
const result = await getIssueDetails.handler(params, experimentalContext);
2177+
const payload = (result as { structuredContent: unknown })
2178+
.structuredContent;
2179+
2180+
// a tool that advertises a schema has to return something that satisfies it
2181+
expect(() => getIssueDetailsOutputSchema.parse(payload)).not.toThrow();
2182+
});
2183+
21732184
it("returns markdown outside experimental mode", async () => {
21742185
mockLatestEvent();
21752186

@@ -2192,30 +2203,6 @@ describe("structuredContent", () => {
21922203
expect(payload.event.body).toContain("### Tags");
21932204
});
21942205

2195-
it("produces a payload that satisfies the schema", async () => {
2196-
mockLatestEvent();
2197-
2198-
const result = await getIssueDetails.handler(params, experimentalContext);
2199-
const payload = (result as { structuredContent: Record<string, any> })
2200-
.structuredContent;
2201-
2202-
// a tool that advertises a schema has to return something that satisfies it
2203-
expect(() => getIssueDetailsOutputSchema.parse(payload)).not.toThrow();
2204-
});
2205-
2206-
it("carries the response notes, which say which tool to call next", async () => {
2207-
mockLatestEvent();
2208-
2209-
const result = await getIssueDetails.handler(params, experimentalContext);
2210-
const payload = (result as { structuredContent: Record<string, any> })
2211-
.structuredContent;
2212-
2213-
expect(payload.responseNotes.length).toBeGreaterThan(0);
2214-
const notes = payload.responseNotes.join("\n");
2215-
expect(notes).toContain("Fixes CLOUDFLARE-MCP-41");
2216-
expect(notes).toContain("search_issue_events");
2217-
});
2218-
22192206
it("carries the top level message, which the body does not render", async () => {
22202207
mockLatestEvent({ message: "TOP-LEVEL-MESSAGE", entries: [] });
22212208

@@ -2272,14 +2259,21 @@ describe("structuredContent", () => {
22722259

22732260
it("keeps the attached replay, which lives on the event not the related list", async () => {
22742261
// an issue whose only replay is attached would otherwise report no replays at all
2275-
mockLatestEvent({
2276-
contexts: {
2277-
replay: {
2278-
type: "default",
2279-
replay_id: "1234567890abcdef1234567890abcdef",
2280-
},
2281-
},
2282-
});
2262+
mswServer.use(
2263+
http.get(
2264+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/",
2265+
() =>
2266+
HttpResponse.json({
2267+
...createDefaultEvent(),
2268+
contexts: {
2269+
replay: {
2270+
type: "default",
2271+
replay_id: "1234567890abcdef1234567890abcdef",
2272+
},
2273+
},
2274+
}),
2275+
),
2276+
);
22832277

22842278
const result = await getIssueDetails.handler(params, experimentalContext);
22852279
const payload = (result as { structuredContent: Record<string, any> })
@@ -2308,8 +2302,14 @@ describe("structuredContent", () => {
23082302
it("maps external issues field by field so upstream extras cannot leak", async () => {
23092303
// structuredContent is a product contract, not a view of the api response: several
23102304
// upstream schemas are passthrough, so anything not mapped must not appear
2311-
mockLatestEvent();
23122305
mswServer.use(
2306+
http.get(
2307+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/",
2308+
() =>
2309+
HttpResponse.json({
2310+
...createDefaultEvent(),
2311+
}),
2312+
),
23132313
http.get(
23142314
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/external-issues/",
23152315
() =>
@@ -2337,7 +2337,16 @@ describe("structuredContent", () => {
23372337
it("carries every field the markdown output surfaces", async () => {
23382338
// greg's bar for this migration is "roughly the same content": anything the markdown
23392339
// renders and the payload drops is a regression for every MCP user
2340-
mockLatestEvent({ dateCreated: "2026-09-03T12:00:00.000Z" });
2340+
mswServer.use(
2341+
http.get(
2342+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/",
2343+
() =>
2344+
HttpResponse.json({
2345+
...createDefaultEvent(),
2346+
dateCreated: "2026-09-03T12:00:00.000Z",
2347+
}),
2348+
),
2349+
);
23412350

23422351
const result = await getIssueDetails.handler(params, experimentalContext);
23432352
const payload = (result as { structuredContent: Record<string, any> })
@@ -2388,7 +2397,10 @@ describe("structuredContent", () => {
23882397
),
23892398
http.get(
23902399
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
2391-
() => HttpResponse.json(createDefaultEvent()),
2400+
() =>
2401+
HttpResponse.json({
2402+
...createDefaultEvent(),
2403+
}),
23922404
),
23932405
);
23942406

@@ -2422,7 +2434,10 @@ describe("structuredContent", () => {
24222434
),
24232435
http.get(
24242436
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/7890123456/events/latest/",
2425-
() => HttpResponse.json(createDefaultEvent()),
2437+
() =>
2438+
HttpResponse.json({
2439+
...createDefaultEvent(),
2440+
}),
24262441
),
24272442
);
24282443

@@ -2440,8 +2455,14 @@ describe("structuredContent", () => {
24402455
const many = Array.from({ length: 51 }, (_, i) =>
24412456
i.toString(16).padStart(32, "0"),
24422457
);
2443-
mockLatestEvent();
24442458
mswServer.use(
2459+
http.get(
2460+
"https://sentry.io/api/0/organizations/sentry-mcp-evals/issues/6507376925/events/latest/",
2461+
() =>
2462+
HttpResponse.json({
2463+
...createDefaultEvent(),
2464+
}),
2465+
),
24452466
// related ids come from replay-count, keyed by numeric issue id. Echo back whichever
24462467
// id was asked for: a preceding test can leave a different issue fixture registered.
24472468
http.get(

0 commit comments

Comments
 (0)