Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .agents/skills/mcp-qa/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ behavior. For catalog tools, expect `search_sentry_tools` followed by
`execute_sentry_tool(name: <changed_tool>)`. For direct tools, expect the tool name in
the transcript. `--list-tools` alone is not QA.

For mutating catalog-only tools, avoid live prod changes unless there is a
disposable resource prepared for the test. Add or run a server-level
`execute_sentry_tool` dispatch test with MSW coverage to prove catalog
discovery, generated schema exposure, constraint injection, and tool dispatch
without changing real Sentry data.

For output-format changes, also inspect the raw MCP tool result when possible,
not only the LLM's final answer. The final answer can add model-specific text
that is not part of the tool response. Review raw tool output against
Expand Down
4 changes: 4 additions & 0 deletions docs/testing/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ Fast, focused tests of actual functionality:
formatted handler response with `toMatchInlineSnapshot()`. Supplemental
`toContain()` assertions are fine, but they do not replace a full-response
snapshot.
- For catalog-only tools, include server-level coverage that executes the tool
through `execute_sentry_tool` when the change affects discovery, generated
schemas, constraint injection, or dispatch. Prefer this MSW-backed route for
mutating tools when live QA would change real Sentry data.
- Review tool output snapshots against
[../contributing/tool-responses.md](../contributing/tool-responses.md) so
formatted output stays user-facing and avoids raw internals.
Expand Down
81 changes: 81 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,27 @@ type RequestOptions = {
};

export type TraceItemType = "spans" | "logs" | "tracemetrics";

type ClientKeyRateLimit = {
window: number;
count: number;
};

type ClientKeyDynamicSdkLoaderOptions = {
hasReplay?: boolean;
hasPerformance?: boolean;
hasDebug?: boolean;
hasFeedback?: boolean;
hasLogsAndMetrics?: boolean;
};

type UpdateClientKeyRequest = {
name?: string;
isActive?: boolean;
rateLimit?: ClientKeyRateLimit | null;
browserSdkVersion?: string;
dynamicSdkLoaderOptions?: ClientKeyDynamicSdkLoaderOptions;
};
export type TraceItemAttributeType = "string" | "number" | "boolean";
export type TraceItemAttributeSourceType = "sentry" | "user";

Expand Down Expand Up @@ -2061,6 +2082,66 @@ export class SentryApiService {
return ClientKeySchema.parse(body);
}

/**
* Updates a client key (DSN) for a project.
*
* @param params Key update parameters
* @param params.organizationSlug Organization identifier
* @param params.projectSlug Project identifier
* @param params.keyId The ID of the key to update
* @param params.name Human-readable name for the key (optional)
* @param params.isActive Activate or deactivate the client key (optional)
* @param params.rateLimit Applies a rate limit to cap the number of errors accepted during a given time window (optional, null to disable)
* @param params.browserSdkVersion Sentry Javascript SDK version to use (optional)
* @param params.dynamicSdkLoaderOptions Configures options for the Javascript Loader Script (optional)
* @param opts Request options
* @returns Updated client key with DSN information
*/
async updateClientKey(
{
organizationSlug,
projectSlug,
keyId,
name,
isActive,
rateLimit,
browserSdkVersion,
dynamicSdkLoaderOptions,
}: {
organizationSlug: string;
projectSlug: string;
keyId: string | number;
} & UpdateClientKeyRequest,
opts?: RequestOptions,
): Promise<ClientKey> {
const updateData: UpdateClientKeyRequest = {};
if (name !== undefined) {
updateData.name = name;
}
if (isActive !== undefined) {
updateData.isActive = isActive;
}
if (rateLimit !== undefined) {
updateData.rateLimit = rateLimit;
}
if (browserSdkVersion !== undefined) {
updateData.browserSdkVersion = browserSdkVersion;
}
if (dynamicSdkLoaderOptions !== undefined) {
updateData.dynamicSdkLoaderOptions = dynamicSdkLoaderOptions;
}

const body = await this.requestJSON(
`/projects/${organizationSlug}/${projectSlug}/keys/${keyId}/`,
{
method: "PUT",
body: JSON.stringify(updateData),
},
opts,
);
return ClientKeySchema.parse(body);
}

/**
* Lists all client keys (DSNs) for a project.
*
Expand Down
25 changes: 22 additions & 3 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,11 +419,30 @@ export const ClientKeySchema = z
.object({
id: z.union([z.string(), z.number()]),
name: z.string(),
dsn: z.object({
public: z.string(),
}),
dsn: z
.object({
public: z.string(),
})
.passthrough(),
isActive: z.boolean(),
dateCreated: z.string().datetime().nullable(),
rateLimit: z
.object({
window: z.number(),
count: z.number(),
})
.nullable()
.optional(),
browserSdkVersion: z.string().optional(),
dynamicSdkLoaderOptions: z
.object({
hasReplay: z.boolean(),
hasPerformance: z.boolean(),
hasDebug: z.boolean(),
hasFeedback: z.boolean().optional(),
hasLogsAndMetrics: z.boolean().optional(),
})
.optional(),
})
.passthrough();

Expand Down
25 changes: 25 additions & 0 deletions packages/mcp-core/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,31 @@ describe("buildServer", () => {
);
});

it("execute_sentry_tool dispatches to catalog-only update_dsn", async () => {
const server = buildServer({
context: baseContext,
});

const toolNames = getRegisteredToolNames(server);
expect(toolNames).not.toContain("update_dsn");

const result = await callRegisteredTool(server, "execute_sentry_tool", {
name: "update_dsn",
arguments: {
organizationSlug: "sentry-mcp-evals",
projectSlug: "cloudflare-mcp",
keyId: "d20df0a1ab5031c7f3c7edca9c02814d",
rateLimitWindow: 3600,
rateLimitCount: 0,
},
});

expect(getTextContent(result)).toContain(
"# Updated DSN in **sentry-mcp-evals/cloudflare-mcp**",
);
expect(getTextContent(result)).toContain("**Rate Limit**: Disabled");
});

it("execute_sentry_tool dispatches to catalog-only whoami", async () => {
const server = buildServer({
context: {
Expand Down
7 changes: 6 additions & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@
"description": "Create and modify projects, teams, and DSNs",
"defaultEnabled": false,
"order": 5,
"toolCount": 9,
"toolCount": 10,
"tools": [
{
"name": "create_dsn",
Expand Down Expand Up @@ -368,6 +368,11 @@
"description": "Find teams in an organization in Sentry.\n\nUse this tool when you need to:\n- View teams in a Sentry organization\n- Find a team's slug and numeric ID to aid other tool requests\n- Search for specific teams by name or slug\n\nReturns up to 25 results. If you hit this limit, use the query parameter to narrow down results.",
"requiredScopes": ["team:read"]
},
{
"name": "update_dsn",
"description": "Update settings for an existing DSN (client key) in a project, such as name, active status, rate limit, and loader script options.\n\nUSE THIS TOOL WHEN:\n- Deactivating or activating a DSN/client key\n- Setting or removing DSN rate limits ('set rate limit of 1000 per hour on DSN X')\n- Renaming a DSN ('rename DSN X to Production')\n- Configuring Javascript SDK loader script options (session replay, performance, debug, feedback, etc.)\n\nBe careful when using this tool!\n\n<examples>\n### Rename DSN and set rate limit\n```\nupdate_dsn(organizationSlug='my-organization', projectSlug='my-project', keyId='d20df0a1ab5031c7f3c7edca9c02814d', name='Production Key', rateLimitWindow=3600, rateLimitCount=500)\n```\n\n### Deactivate a DSN\n```\nupdate_dsn(organizationSlug='my-organization', projectSlug='my-project', keyId='d20df0a1ab5031c7f3c7edca9c02814d', isActive=false)\n```\n\n### Disable rate limit entirely\n```\nupdate_dsn(organizationSlug='my-organization', projectSlug='my-project', keyId='d20df0a1ab5031c7f3c7edca9c02814d', disableRateLimit=true)\n```\n</examples>\n\n<hints>\n- Use `find_dsns()` first to find the `keyId` for the DSN you want to update.\n- Both `rateLimitWindow` (seconds) and `rateLimitCount` (error cap) must be provided together to set a rate limit.\n</hints>",
"requiredScopes": ["project:write"]
},
{
"name": "update_project",
"description": "Update project settings in Sentry, such as name, slug, platform, and team assignment.\n\nBe careful when using this tool!\n\nUse this tool when you need to:\n- Update a project's name or slug to fix onboarding mistakes\n- Change the platform assigned to a project\n- Update team assignment for a project\n\n<examples>\n### Update a project's name and slug\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='old-project', name='New Project Name', slug='new-project-slug')\n```\n\n### Assign a project to a different team\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', teamSlug='backend-team')\n```\n\n### Update platform\n\n```\nupdate_project(organizationSlug='my-organization', projectSlug='my-project', platform='python')\n```\n\n</examples>\n\n<hints>\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of <organizationSlug>/<projectSlug>.\n- Team assignment is handled separately from other project settings\n- If any parameter is ambiguous, you should clarify with the user what they meant.\n- When updating the slug, the project will be accessible at the new slug after the update\n</hints>",
Expand Down
88 changes: 88 additions & 0 deletions packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,94 @@
"skills": ["inspect", "seer", "docs", "triage", "project-management"],
"surface": "direct"
},
{
"name": "update_dsn",
"description": "Update settings for an existing DSN (client key) in a project, such as name, active status, rate limit, and loader script options.\n\nUSE THIS TOOL WHEN:\n- Deactivating or activating a DSN/client key\n- Setting or removing DSN rate limits ('set rate limit of 1000 per hour on DSN X')\n- Renaming a DSN ('rename DSN X to Production')\n- Configuring Javascript SDK loader script options (session replay, performance, debug, feedback, etc.)\n\nBe careful when using this tool!\n\n<examples>\n### Rename DSN and set rate limit\n```\nupdate_dsn(organizationSlug='my-organization', projectSlug='my-project', keyId='d20df0a1ab5031c7f3c7edca9c02814d', name='Production Key', rateLimitWindow=3600, rateLimitCount=500)\n```\n\n### Deactivate a DSN\n```\nupdate_dsn(organizationSlug='my-organization', projectSlug='my-project', keyId='d20df0a1ab5031c7f3c7edca9c02814d', isActive=false)\n```\n\n### Disable rate limit entirely\n```\nupdate_dsn(organizationSlug='my-organization', projectSlug='my-project', keyId='d20df0a1ab5031c7f3c7edca9c02814d', disableRateLimit=true)\n```\n</examples>\n\n<hints>\n- Use `find_dsns()` first to find the `keyId` for the DSN you want to update.\n- Both `rateLimitWindow` (seconds) and `rateLimitCount` (error cap) must be provided together to set a rate limit.\n</hints>",
"inputSchema": {
"type": "object",
"properties": {
"organizationSlug": {
"type": "string",
"description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
},
"regionUrl": {
"anyOf": [
{
"type": "string",
"description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
},
{
"type": "null"
}
],
"description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
"default": null
},
"projectSlug": {
"type": "string",
"description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool."
},
"keyId": {
"type": "string",
"description": "The ID of the DSN (client key) to update. Use find_dsns() to retrieve this ID first."
},
"name": {
"type": "string",
"maxLength": 64,
"description": "The new name for the DSN."
},
"isActive": {
"type": "boolean",
"description": "Activate or deactivate the DSN."
},
"rateLimitWindow": {
"type": "integer",
"minimum": 0,
"maximum": 86400,
"description": "The time window in seconds for the rate limit."
},
"rateLimitCount": {
"type": "integer",
"minimum": 0,
"description": "The maximum number of errors allowed within the rate limit window."
},
"disableRateLimit": {
"type": "boolean",
"description": "Set to true to disable the rate limit entirely (removes the cap)."
},
"browserSdkVersion": {
"type": "string",
"description": "The Sentry Javascript SDK version to use (e.g. 'latest', '7.x')."
},
"loaderHasReplay": {
"type": "boolean",
"description": "Configure Session Replay for the Javascript Loader Script."
},
"loaderHasPerformance": {
"type": "boolean",
"description": "Configure Performance Monitoring for the Javascript Loader Script."
},
"loaderHasDebug": {
"type": "boolean",
"description": "Configure Debug Bundles & Logging for the Javascript Loader Script."
},
"loaderHasFeedback": {
"type": "boolean",
"description": "Configure User Feedback for the Javascript Loader Script."
},
"loaderHasLogsAndMetrics": {
"type": "boolean",
"description": "Configure Logs and Metrics for the Javascript Loader Script (requires SDK >= 10.0.0)."
}
},
"required": ["organizationSlug", "projectSlug", "keyId"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"requiredScopes": ["project:write"],
"skills": ["project-management"],
"surface": "catalog"
},
{
"name": "update_issue",
"description": "Update a Sentry issue's status or assignment.\n\nUse this to resolve, reopen, assign, or ignore an issue.\n\n<examples>\n```\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='resolved')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', assignedTo='user:123456')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='forever')\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', ignoreMode='untilOccurrenceCount', ignoreCount=100, ignoreWindowMinutes=60)\nupdate_issue(organizationSlug='my-org', issueId='PROJECT-123', status='ignored', reason='Ignoring because this is expected noise from the staging deploy')\n```\n</examples>\n\n<hints>\n- Provide `issueUrl` or `organizationSlug` + `issueId`.\n- At least one of `status` or `assignedTo` is required.\n- `assignedTo` format: `user:ID` or `team:ID_OR_SLUG`.\n- Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID for self-assignment.\n- Status values: `resolved`, `resolvedInNextRelease`, `unresolved`, `ignored`.\n- `status='ignored'` defaults to `ignoreMode='untilEscalating'`.\n- Ignore modes: `untilEscalating`, `forever`, `forDuration`, `untilOccurrenceCount`, `untilUserCount`.\n- Matching ignore inputs are `ignoreDurationMinutes`, `ignoreCount` + optional `ignoreWindowMinutes`, or `ignoreUserCount` + optional `ignoreUserWindowMinutes`.\n- To switch an already ignored issue between `untilEscalating`, `forever`, and condition-based ignore modes, first set `status='unresolved'`, then ignore it again with the new rule.\n- `reason` is optional. When provided, it will be posted as a comment on the issue's activity feed explaining why the action was taken.\n</hints>",
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp-core/src/tools/catalog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import createProject from "./create-project";
import updateProject from "./update-project";
import createDsn from "./create-dsn";
import findDsns from "./find-dsns";
import updateDsn from "./update-dsn";
import analyzeIssueWithSeer from "./analyze-issue-with-seer";
import searchDocs from "./search-docs";
import getDoc from "./get-doc";
Expand Down Expand Up @@ -73,6 +74,7 @@ const catalogTools = {
update_project: updateProject,
create_dsn: createDsn,
find_dsns: findDsns,
update_dsn: updateDsn,
analyze_issue_with_seer: analyzeIssueWithSeer,
search_docs: searchDocs,
get_doc: getDoc,
Expand Down
Loading
Loading