From c4cfd4f8c8c2c89ccb76f656cf2b960d39608c3c Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:31:29 +0000 Subject: [PATCH 1/2] fix(mcp): Accept 1h and minute periods in search tools `search_issues` limits `period` to an enum without `1h`, and the shared `ParamPeriod` regex rejects minute windows such as `30m`. The MCP SDK rejects these before the handler runs. In `sentry/mcp-server` over the 7 days to 2026-10-26, about 26K `search_issues` calls failed on `period: "1h"`. A single user sent about 25K of those; the other ~50 users sent about 500. Minute periods (`30m`, `15m`, `10m`, ...) failed about 1.5K times for ~300 users on `search_errors`, `search_logs`, and `search_traces`. Sentry's `statsPeriod` already treats `m` as minutes: a read-only check returned 28 errors for `30m` against 1,269 for `24h`, and the issues endpoint accepts `statsPeriod=1h`. Add `1h` to the `search_issues` enum and accept `m` in `ParamPeriod`. The other `search_issues` windows stay bounded. Co-Authored-By: Burak Yigit Kaya <126780+BYK@users.noreply.github.com> --- packages/mcp-core/src/constants.ts | 1 + packages/mcp-core/src/schema.test.ts | 12 +++++ packages/mcp-core/src/schema.ts | 6 +-- packages/mcp-core/src/toolDefinitions.json | 48 +++++++++---------- .../src/tools/catalog/search-issues.test.ts | 4 ++ .../src/tools/catalog/search-issues.ts | 2 +- 6 files changed, 45 insertions(+), 28 deletions(-) create mode 100644 packages/mcp-core/src/schema.test.ts diff --git a/packages/mcp-core/src/constants.ts b/packages/mcp-core/src/constants.ts index 487bd04c5..85cef2495 100644 --- a/packages/mcp-core/src/constants.ts +++ b/packages/mcp-core/src/constants.ts @@ -14,6 +14,7 @@ export const MCP_SERVER_NAME = "Sentry MCP" as const; * Intentionally bounded for query performance; lower-level API methods still accept arbitrary Sentry statsPeriod strings. */ export const SEARCH_ISSUES_PERIOD_VALUES = [ + "1h", "24h", "7d", "14d", diff --git a/packages/mcp-core/src/schema.test.ts b/packages/mcp-core/src/schema.test.ts new file mode 100644 index 000000000..77151577a --- /dev/null +++ b/packages/mcp-core/src/schema.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from "vitest"; +import { ParamPeriod } from "./schema"; + +describe("ParamPeriod", () => { + it.each(["30m", "1h", "24h", "7d", "2w"])("accepts %s", (period) => { + expect(ParamPeriod.parse(period)).toBe(period); + }); + + it.each(["1mo", "last hour", "1.5h"])("rejects %s", (period) => { + expect(ParamPeriod.safeParse(period).success).toBe(false); + }); +}); diff --git a/packages/mcp-core/src/schema.ts b/packages/mcp-core/src/schema.ts index 6f5d4702c..7d957defe 100644 --- a/packages/mcp-core/src/schema.ts +++ b/packages/mcp-core/src/schema.ts @@ -142,11 +142,11 @@ export const ParamPeriod = z .string() .trim() .regex( - /^\d+[hdw]$/, - "Period must be a relative time window like `24h`, `7d`, `14d`, `30d`, or `90d`.", + /^\d+[mhdw]$/, + "Period must be a relative time window like `30m` (minutes), `1h`, `24h`, `7d`, or `90d`.", ) .describe( - "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", ); /** diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 50d8ec298..14cdea45f 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -5213,7 +5213,7 @@ "anyOf": [ { "type": "string", - "pattern": "^\\d+[hdw]$", + "pattern": "^\\d+[mhdw]$", "description": "Relative time range. Defaults to `24h` when `start` and `end` are omitted." }, { @@ -5321,12 +5321,12 @@ "default": "7d", "description": "Profiling time window (default: '7d'). Use '1h' for very recent profiling data.", "type": "string", - "pattern": "^\\d+[hdw]$" + "pattern": "^\\d+[mhdw]$" }, "compareAgainstPeriod": { "description": "Compare against this baseline profiling time window. Enables regression detection.", "type": "string", - "pattern": "^\\d+[hdw]$" + "pattern": "^\\d+[mhdw]$" }, "focusOnUserCode": { "default": true, @@ -5761,7 +5761,7 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", + "pattern": "^\\d+[mhdw]$", "description": "Relative time range for recent checks. Defaults to `24h` when `start` and `end` are omitted." }, "start": { @@ -6198,7 +6198,7 @@ "default": "30d", "description": "Relative time range. Defaults to 30d.", "type": "string", - "pattern": "^\\d+[hdw]$" + "pattern": "^\\d+[mhdw]$" }, "start": { "description": "Explicit start time for the search window.", @@ -6421,7 +6421,7 @@ "default": "30d", "description": "Relative time range. Defaults to 30d.", "type": "string", - "pattern": "^\\d+[hdw]$" + "pattern": "^\\d+[mhdw]$" }, "start": { "description": "Explicit start time for the search window.", @@ -6796,8 +6796,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, @@ -6898,8 +6898,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, @@ -6970,8 +6970,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "projectSlug": { "default": null, @@ -7064,9 +7064,9 @@ }, "period": { "default": "30d", - "description": "Time window for issue search results. Controls which issues are returned based on when they had activity. Default 30d is a balance between coverage and query performance; use 24h for very recent issues or 90d for broader historical searches.", + "description": "Time window for issue search results. Controls which issues are returned based on when they had activity. Default 30d is a balance between coverage and query performance; use 1h or 24h for very recent issues or 90d for broader historical searches.", "type": "string", - "enum": ["24h", "7d", "14d", "30d", "90d"] + "enum": ["1h", "24h", "7d", "14d", "30d", "90d"] }, "includeExplanation": { "default": false, @@ -7126,8 +7126,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, @@ -7206,8 +7206,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, @@ -7286,8 +7286,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, @@ -7369,8 +7369,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, @@ -7533,8 +7533,8 @@ }, "period": { "type": "string", - "pattern": "^\\d+[hdw]$", - "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + "pattern": "^\\d+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window." }, "regionUrl": { "default": null, diff --git a/packages/mcp-core/src/tools/catalog/search-issues.test.ts b/packages/mcp-core/src/tools/catalog/search-issues.test.ts index 73c740fd0..59f9cd772 100644 --- a/packages/mcp-core/src/tools/catalog/search-issues.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-issues.test.ts @@ -113,6 +113,10 @@ describe("search_issues", () => { }, ); + it("accepts a 1h period", () => { + expect(searchIssues.inputSchema.period.parse("1h")).toBe("1h"); + }); + it("should search issues with natural language query", async () => { mockGenerateText.mockResolvedValue(mockAIResponse("is:unresolved", "date")); diff --git a/packages/mcp-core/src/tools/catalog/search-issues.ts b/packages/mcp-core/src/tools/catalog/search-issues.ts index 93e8acc5f..749aa4681 100644 --- a/packages/mcp-core/src/tools/catalog/search-issues.ts +++ b/packages/mcp-core/src/tools/catalog/search-issues.ts @@ -128,7 +128,7 @@ export default defineTool({ .enum(SEARCH_ISSUES_PERIOD_VALUES) .default("30d") .describe( - "Time window for issue search results. Controls which issues are returned based on when they had activity. Default 30d is a balance between coverage and query performance; use 24h for very recent issues or 90d for broader historical searches.", + "Time window for issue search results. Controls which issues are returned based on when they had activity. Default 30d is a balance between coverage and query performance; use 1h or 24h for very recent issues or 90d for broader historical searches.", ), includeExplanation: z .boolean() From 117ef664da0b9ff8bcd73e2e51a7998818bf66e0 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2026 23:31:58 +0000 Subject: [PATCH 2/2] fix(mcp): Explain how to recover a missing organizationSlug Sessions without an organization constraint need `organizationSlug` on most tools. When a call omits it, the MCP SDK rejects the arguments before the handler runs and returns zod's generic message, such as `organizationSlug: Invalid input: expected string, received undefined`. In `sentry/mcp-server` over the 7 days to 2026-10-26, about 24K tool calls failed this way. A single user sent about 17.7K of them. The rest came from about 860 users, mostly on `search_issues`, `search_errors`, and `find_projects`. The SDK validates before the handler, so the zod issue message is the only place to add guidance. A missing or null slug now returns `Required. Call find_organizations() to list your organizations, then retry with organizationSlug set to one of their slugs.` Other invalid values keep their current messages. The JSON schema and token count do not change. Co-Authored-By: Burak Yigit Kaya <126780+BYK@users.noreply.github.com> --- packages/mcp-core/src/schema.test.ts | 20 +++++++++++++++++++- packages/mcp-core/src/schema.ts | 9 ++++++++- packages/mcp-core/src/server.test.ts | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/mcp-core/src/schema.test.ts b/packages/mcp-core/src/schema.test.ts index 77151577a..a3e7373de 100644 --- a/packages/mcp-core/src/schema.test.ts +++ b/packages/mcp-core/src/schema.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { ParamPeriod } from "./schema"; +import { ParamOrganizationSlug, ParamPeriod } from "./schema"; describe("ParamPeriod", () => { it.each(["30m", "1h", "24h", "7d", "2w"])("accepts %s", (period) => { @@ -10,3 +10,21 @@ describe("ParamPeriod", () => { expect(ParamPeriod.safeParse(period).success).toBe(false); }); }); + +describe("ParamOrganizationSlug", () => { + it.each([undefined, null])("explains how to recover a %s slug", (value) => { + const result = ParamOrganizationSlug.safeParse(value); + + expect(result.error?.issues[0]?.message).toBe( + "Required. Call `find_organizations()` to list your organizations, then retry with `organizationSlug` set to one of their slugs.", + ); + }); + + it("keeps the type message for non-string slugs", () => { + const result = ParamOrganizationSlug.safeParse(42); + + expect(result.error?.issues[0]?.message).toBe( + "Invalid input: expected string, received number", + ); + }); +}); diff --git a/packages/mcp-core/src/schema.ts b/packages/mcp-core/src/schema.ts index 7d957defe..fb86c4381 100644 --- a/packages/mcp-core/src/schema.ts +++ b/packages/mcp-core/src/schema.ts @@ -12,7 +12,14 @@ import { validateResourceId, validateSlug } from "./utils/slug-validation"; // Sentry slug lookups can be exact and case-sensitive on legacy instances. // Preserve caller casing for resource slugs; only trim and validate shape. export const ParamOrganizationSlug = z - .string() + .string({ + // The MCP SDK rejects a missing slug before the handler runs, so this + // message is the only recovery hint the caller gets. + error: (issue) => + issue.input == null + ? "Required. Call `find_organizations()` to list your organizations, then retry with `organizationSlug` set to one of their slugs." + : undefined, + }) .trim() .superRefine(validateSlug) .describe( diff --git a/packages/mcp-core/src/server.test.ts b/packages/mcp-core/src/server.test.ts index a6425f7d4..1256b4f09 100644 --- a/packages/mcp-core/src/server.test.ts +++ b/packages/mcp-core/src/server.test.ts @@ -252,6 +252,26 @@ describe("buildServer", () => { }); }); + it.each(["v1", "v2"] as const)( + "explains how to recover a missing organizationSlug on SDK %s", + async (sdkVersion) => { + const server = + sdkVersion === "v2" + ? buildServer({ context: baseContext, sdkVersion }) + : buildServer({ context: baseContext }); + + const result = await callRegisteredTool(server, "search_issues", { + query: "is:unresolved", + period: "1h", + }); + + expect(result).toMatchObject({ isError: true }); + const text = getTextContent(result); + expect(text).toContain("Required. Call `find_organizations()`"); + expect(text).not.toContain("period"); + }, + ); + describe("telemetry context", () => { it("generates compatibility text for structured-only tool output", async () => { const server = buildServer({