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..a3e7373de --- /dev/null +++ b/packages/mcp-core/src/schema.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { ParamOrganizationSlug, 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); + }); +}); + +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 6f5d4702c..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( @@ -142,11 +149,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/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({ 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()