Skip to content

Commit d6bfafc

Browse files
authored
fix(mcp-core): Allow string 'count' in EventsStatsResponseSchema
1 parent 486de4b commit d6bfafc

3 files changed

Lines changed: 65 additions & 3 deletions

File tree

‎packages/mcp-core/src/api-client/schema.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2447,7 +2447,9 @@ export const EventsStatsResponseSchema = z
24472447
data: z.array(
24482448
z.tuple([
24492449
z.number(),
2450-
z.array(z.object({ count: z.number().nullish() }).passthrough()),
2450+
z.array(
2451+
z.object({ count: z.union([z.string(), z.number()]).nullish() }).passthrough(),
2452+
),
24512453
]),
24522454
),
24532455
start: z.number().optional(),

‎packages/mcp-core/src/tools/catalog/search-events.test.ts‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,66 @@ describe("search_events", () => {
306306
expect(result).toContain("**Peak**: 8");
307307
});
308308

309+
it("handles timeseries when the API returns count as a string (e.g. max(timestamp))", async () => {
310+
const output = {
311+
dataset: "errors" as const,
312+
query: "",
313+
fields: [] as string[],
314+
sort: "-timestamp",
315+
environment: null,
316+
timeSeries: { yAxis: "max(timestamp)", interval: "1d" },
317+
timeRange: { statsPeriod: "45d" },
318+
explanation: "Max timestamp per day",
319+
};
320+
mockGenerateText.mockResolvedValueOnce({
321+
text: JSON.stringify(output),
322+
experimental_output: output,
323+
finishReason: "stop" as const,
324+
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
325+
warnings: [] as const,
326+
} as any);
327+
328+
// The Sentry API returns `count` as a string for non-count aggregates like max(timestamp).
329+
mswServer.use(
330+
http.get(
331+
"https://sentry.io/api/0/organizations/test-org/events-stats/",
332+
() =>
333+
HttpResponse.json({
334+
data: [
335+
[1757548800, [{ count: "1757548800" }]],
336+
[1757635200, [{ count: "1757635200" }]],
337+
[1757721600, [{ count: "1757721600" }]],
338+
],
339+
}),
340+
),
341+
);
342+
343+
const result = await searchEvents.handler(
344+
{
345+
organizationSlug: "test-org",
346+
regionUrl: null,
347+
projectSlug: null,
348+
dataset: "errors",
349+
query: "latest event timestamp per day",
350+
fields: null,
351+
sort: null,
352+
period: "45d",
353+
limit: 10,
354+
includeExplanation: false,
355+
},
356+
{
357+
accessToken: "test-token",
358+
userId: "user-123",
359+
clientId: "client-123",
360+
grantedSkills: new Set(),
361+
constraints: {},
362+
sentryHost: "sentry.io",
363+
},
364+
);
365+
366+
expect(result).toContain("max(timestamp) over time");
367+
});
368+
309369
it("should handle spans dataset queries", async () => {
310370
// Mock AI response for spans dataset
311371
mockGenerateText.mockResolvedValueOnce(

‎packages/mcp-core/src/tools/support/search-events/formatters.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -943,7 +943,7 @@ function isAdditiveAggregate(yAxis: string): boolean {
943943
* `interval` is null when Sentry chose the bucket size for the range.
944944
*/
945945
export function formatTimeSeriesResults(params: {
946-
series: { data: Array<[number, Array<{ count?: number | null }>]> };
946+
series: { data: Array<[number, Array<{ count?: string | number | null }>]> };
947947
yAxis: string;
948948
interval: string | null;
949949
inputQuery: string;
@@ -965,7 +965,7 @@ export function formatTimeSeriesResults(params: {
965965

966966
const points = series.data.map(([ts, values]) => ({
967967
time: new Date(ts * 1000).toISOString().slice(0, 16).replace("T", " "),
968-
value: values[0]?.count ?? 0,
968+
value: Number(values[0]?.count ?? 0),
969969
}));
970970

971971
// Total is only meaningful for additive aggregates; summing count_unique /

0 commit comments

Comments
 (0)