From 2d431e8c1d8564f976bd774034ebb34e410771a3 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:30:34 +0000 Subject: [PATCH 1/2] feat(tools): Return DSN status and rate limit from find_dsns find_dsns only returned the DSN id, name, and public DSN, so there was no way to read a DSN's rate limit through MCP even though update_dsn can set it. The Sentry project keys API already returns isActive and rateLimit; map both into the structured output. rateLimit is null when no limit is set, matching what Sentry returns. Co-Authored-By: Saad Khan --- packages/mcp-core/src/skillDefinitions.json | 2 +- packages/mcp-core/src/toolDefinitions.json | 30 +++++++++++++++++-- .../src/tools/catalog/find-dsns.test.ts | 11 ++++++- .../mcp-core/src/tools/catalog/find-dsns.ts | 20 ++++++++++++- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 1f3a1d245..e1684d3ac 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -580,7 +580,7 @@ }, { "name": "find_dsns", - "description": "List all Sentry DSNs for a specific project.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n", + "description": "List all Sentry DSNs for a specific project, including each DSN's status and rate limit.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n- Check whether a DSN is active or what rate limit it has\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n", "requiredScopes": ["project:read"] }, { diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 3165f01c0..87235371f 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -2191,7 +2191,7 @@ }, { "name": "find_dsns", - "description": "List all Sentry DSNs for a specific project.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n", + "description": "List all Sentry DSNs for a specific project, including each DSN's status and rate limit.\n\nUse this tool when you need to:\n- Retrieve a SENTRY_DSN for a specific project\n- Check whether a DSN is active or what rate limit it has\n\n\n- If the user passes a parameter in the form of name/otherName, its likely in the format of /.\n- If only one parameter is provided, and it could be either `organizationSlug` or `projectSlug`, its probably `organizationSlug`, but if you're really uncertain you might want to call `find_organizations()` first.\n", "inputSchema": { "type": "object", "properties": { @@ -2234,9 +2234,35 @@ }, "dsn": { "type": "string" + }, + "isActive": { + "type": "boolean" + }, + "rateLimit": { + "anyOf": [ + { + "type": "object", + "properties": { + "window": { + "type": "number", + "description": "Rate limit window in seconds." + }, + "count": { + "type": "number", + "description": "Maximum errors accepted within the window." + } + }, + "required": ["window", "count"], + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "The DSN's rate limit, or null when no limit is set." } }, - "required": ["id", "name", "dsn"], + "required": ["id", "name", "dsn", "isActive", "rateLimit"], "additionalProperties": false } } diff --git a/packages/mcp-core/src/tools/catalog/find-dsns.test.ts b/packages/mcp-core/src/tools/catalog/find-dsns.test.ts index 6f914e3c0..2c3d37a33 100644 --- a/packages/mcp-core/src/tools/catalog/find-dsns.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-dsns.test.ts @@ -9,7 +9,7 @@ import { import findDsns from "./find-dsns.js"; describe("find_dsns", () => { - it("returns all project DSNs as structured content", async () => { + it("returns all project DSNs with status and rate limit as structured content", async () => { mswServer.use( http.get( "https://sentry.io/api/0/projects/test-org/test-project/keys/", @@ -24,6 +24,7 @@ describe("find_dsns", () => { }, isActive: true, dateCreated: "2026-07-28T12:00:00.000Z", + rateLimit: { window: 3600, count: 1000 }, secretKey: "do-not-leak", backendOnlyField: "do-not-leak", }, @@ -35,6 +36,7 @@ describe("find_dsns", () => { }, isActive: false, dateCreated: "2026-07-28T12:00:00.000Z", + rateLimit: null, }, ]), ), @@ -56,12 +58,19 @@ describe("find_dsns", () => { { "dsn": "https://public@example.ingest.sentry.io/1", "id": "123", + "isActive": true, "name": "Production", + "rateLimit": { + "count": 1000, + "window": 3600, + }, }, { "dsn": "https://development@example.ingest.sentry.io/2", "id": "456", + "isActive": false, "name": "Development", + "rateLimit": null, }, ], } diff --git a/packages/mcp-core/src/tools/catalog/find-dsns.ts b/packages/mcp-core/src/tools/catalog/find-dsns.ts index f9886986a..97704d44a 100644 --- a/packages/mcp-core/src/tools/catalog/find-dsns.ts +++ b/packages/mcp-core/src/tools/catalog/find-dsns.ts @@ -16,6 +16,16 @@ export const findDsnsOutputSchema = z.object({ id: z.string(), name: z.string(), dsn: z.string(), + isActive: z.boolean(), + rateLimit: z + .object({ + window: z.number().describe("Rate limit window in seconds."), + count: z + .number() + .describe("Maximum errors accepted within the window."), + }) + .nullable() + .describe("The DSN's rate limit, or null when no limit is set."), }), ), }); @@ -25,10 +35,11 @@ export default defineTool({ skills: ["project-management"], // DSN management is part of project setup requiredScopes: ["project:read"], description: [ - "List all Sentry DSNs for a specific project.", + "List all Sentry DSNs for a specific project, including each DSN's status and rate limit.", "", "Use this tool when you need to:", "- Retrieve a SENTRY_DSN for a specific project", + "- Check whether a DSN is active or what rate limit it has", "", "", "- If the user passes a parameter in the form of name/otherName, its likely in the format of /.", @@ -64,6 +75,13 @@ export default defineTool({ id: String(clientKey.id), name: clientKey.name, dsn: clientKey.dsn.public, + isActive: clientKey.isActive, + rateLimit: clientKey.rateLimit + ? { + window: clientKey.rateLimit.window, + count: clientKey.rateLimit.count, + } + : null, })), }); }, From 0934ea27040dd8cb9151bef2efd10279c44411fb Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 07:27:57 +0000 Subject: [PATCH 2/2] fix(tools): Match find_dsns rate limit shape to update_dsn update_dsn returns rateLimit as { count, windowSeconds }. Use the same field names in find_dsns so agents see one shape across both tools. --- packages/mcp-core/src/toolDefinitions.json | 14 ++++++-------- .../mcp-core/src/tools/catalog/find-dsns.test.ts | 2 +- packages/mcp-core/src/tools/catalog/find-dsns.ts | 10 ++++------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 5a0f2b64c..aa0474092 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -2518,23 +2518,21 @@ { "type": "object", "properties": { - "window": { - "type": "number", - "description": "Rate limit window in seconds." - }, "count": { - "type": "number", - "description": "Maximum errors accepted within the window." + "type": "number" + }, + "windowSeconds": { + "type": "number" } }, - "required": ["window", "count"], + "required": ["count", "windowSeconds"], "additionalProperties": false }, { "type": "null" } ], - "description": "The DSN's rate limit, or null when no limit is set." + "description": "Null when rate limiting is disabled." } }, "required": ["id", "name", "dsn", "isActive", "rateLimit"], diff --git a/packages/mcp-core/src/tools/catalog/find-dsns.test.ts b/packages/mcp-core/src/tools/catalog/find-dsns.test.ts index 2c3d37a33..608d84366 100644 --- a/packages/mcp-core/src/tools/catalog/find-dsns.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-dsns.test.ts @@ -62,7 +62,7 @@ describe("find_dsns", () => { "name": "Production", "rateLimit": { "count": 1000, - "window": 3600, + "windowSeconds": 3600, }, }, { diff --git a/packages/mcp-core/src/tools/catalog/find-dsns.ts b/packages/mcp-core/src/tools/catalog/find-dsns.ts index a42392197..b6425182b 100644 --- a/packages/mcp-core/src/tools/catalog/find-dsns.ts +++ b/packages/mcp-core/src/tools/catalog/find-dsns.ts @@ -19,13 +19,11 @@ export const findDsnsOutputSchema = z.object({ isActive: z.boolean(), rateLimit: z .object({ - window: z.number().describe("Rate limit window in seconds."), - count: z - .number() - .describe("Maximum errors accepted within the window."), + count: z.number(), + windowSeconds: z.number(), }) .nullable() - .describe("The DSN's rate limit, or null when no limit is set."), + .describe("Null when rate limiting is disabled."), }), ), }); @@ -80,8 +78,8 @@ export default defineTool({ isActive: clientKey.isActive, rateLimit: clientKey.rateLimit ? { - window: clientKey.rateLimit.window, count: clientKey.rateLimit.count, + windowSeconds: clientKey.rateLimit.window, } : null, })),