Skip to content

Commit 44bc95a

Browse files
Joe BecherCursordcramerclaude
authored
feat(tools): add get_issue_tag_values tool for tag distribution (#706)
## Summary Adds new MCP tool `get_issue_tag_values` to expose Sentry's Tag Distributions view. This addresses [CCMRG-1999](https://linear.app/getsentry/issue/CCMRG-1999/add-get-issue-tag-values-tool-for-tag-distribution). ### Context Currently, the Sentry MCP doesn't expose the Tag Distributions view (like `https://sentry.io/issues/{id}/distributions/url/`). This makes it impossible for AI agents to get aggregate counts of unique tag values for an issue (e.g., "how many unique repos are affected by this error"). ### Key Changes - **API Client**: Added `getIssueTagValues()` method calling `GET /api/0/issues/{issue_id}/tags/{tag_name}/` - **Schema**: Added `IssueTagValuesSchema` Zod schema for response validation - **New Tool**: Created `get_issue_tag_values` tool with full documentation, examples, and hints - **Testing**: Added MSW mock handler, fixture, and comprehensive unit tests - **Updated tool counts**: Fixed `use-sentry` handler tests for new tool count (21 tools) ### Example Usage ```typescript // Get URL distribution for an issue get_issue_tag_values(organizationSlug='my-org', issueId='PROJECT-123', tagKey='url') // Get browser distribution using issue URL get_issue_tag_values(issueUrl='https://sentry.io/issues/PROJECT-123/', tagKey='browser') ``` ### Breaking Changes None --------- Co-authored-by: Cursor <noreply@cursor.sh> Co-authored-by: David Cramer <dcramer@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 384fe45 commit 44bc95a

11 files changed

Lines changed: 520 additions & 9 deletions

File tree

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ReleaseListSchema,
1616
IssueListSchema,
1717
IssueSchema,
18+
IssueTagValuesSchema,
1819
EventSchema,
1920
EventAttachmentListSchema,
2021
ErrorsSearchResponseSchema,
@@ -42,6 +43,7 @@ import type {
4243
EventAttachmentList,
4344
Issue,
4445
IssueList,
46+
IssueTagValues,
4547
OrganizationList,
4648
Project,
4749
ProjectList,
@@ -1546,6 +1548,51 @@ export class SentryApiService {
15461548
return IssueSchema.parse(body);
15471549
}
15481550

1551+
/**
1552+
* Retrieves tag value distribution for a specific issue.
1553+
*
1554+
* Returns aggregate counts of unique tag values, useful for understanding
1555+
* how an issue is distributed across different tag values (e.g., URLs,
1556+
* browsers, environments).
1557+
*
1558+
* @param params Query parameters
1559+
* @param params.organizationSlug Organization identifier
1560+
* @param params.issueId Issue identifier (short ID or numeric ID)
1561+
* @param params.tagKey Tag key to get values for (e.g., "url", "browser", "environment")
1562+
* @param opts Request options
1563+
* @returns Tag value distribution with counts and percentages
1564+
*
1565+
* @example
1566+
* ```typescript
1567+
* const tagValues = await apiService.getIssueTagValues({
1568+
* organizationSlug: "my-org",
1569+
* issueId: "PROJECT-123",
1570+
* tagKey: "url"
1571+
* });
1572+
* console.log(`Total unique values: ${tagValues.totalValues}`);
1573+
* tagValues.topValues.forEach(v => console.log(`${v.value}: ${v.count}`));
1574+
* ```
1575+
*/
1576+
async getIssueTagValues(
1577+
{
1578+
organizationSlug,
1579+
issueId,
1580+
tagKey,
1581+
}: {
1582+
organizationSlug: string;
1583+
issueId: string;
1584+
tagKey: string;
1585+
},
1586+
opts?: RequestOptions,
1587+
): Promise<IssueTagValues> {
1588+
const body = await this.requestJSON(
1589+
`/organizations/${organizationSlug}/issues/${issueId}/tags/${tagKey}/`,
1590+
undefined,
1591+
opts,
1592+
);
1593+
return IssueTagValuesSchema.parse(body);
1594+
}
1595+
15491596
async getEventForIssue(
15501597
{
15511598
organizationSlug,

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,33 @@ export const EventAttachmentSchema = z.object({
666666

667667
export const EventAttachmentListSchema = z.array(EventAttachmentSchema);
668668

669+
/**
670+
* Schema for individual tag values within an issue's tag distribution.
671+
*
672+
* Represents a single value's occurrence count and percentage within a tag.
673+
*/
674+
export const IssueTagValueSchema = z.object({
675+
key: z.string().optional(),
676+
name: z.string().optional(),
677+
value: z.string(),
678+
count: z.number(),
679+
lastSeen: z.string().datetime().nullable().optional(),
680+
firstSeen: z.string().datetime().nullable().optional(),
681+
});
682+
683+
/**
684+
* Schema for Sentry issue tag values response.
685+
*
686+
* Contains aggregate counts of unique tag values for an issue,
687+
* useful for understanding the distribution of tags like URL, browser, etc.
688+
*/
689+
export const IssueTagValuesSchema = z.object({
690+
key: z.string(),
691+
name: z.string(),
692+
totalValues: z.number(),
693+
topValues: z.array(IssueTagValueSchema),
694+
});
695+
669696
/**
670697
* Schema for Sentry trace metadata response.
671698
*

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import type {
5555
EventAttachmentListSchema,
5656
IssueListSchema,
5757
IssueSchema,
58+
IssueTagValuesSchema,
5859
OrganizationListSchema,
5960
OrganizationSchema,
6061
ProjectListSchema,
@@ -116,3 +117,6 @@ export type TraceMeta = z.infer<typeof TraceMetaSchema>;
116117
export type TraceSpan = z.infer<typeof TraceSpanSchema>;
117118
export type TraceIssue = z.infer<typeof TraceIssueSchema>;
118119
export type Trace = z.infer<typeof TraceSchema>;
120+
121+
// Issue tag values
122+
export type IssueTagValues = z.infer<typeof IssueTagValuesSchema>;

‎packages/mcp-core/src/skillDefinitions.json‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"description": "Search for errors, analyze traces, and explore event details",
66
"defaultEnabled": true,
77
"order": 1,
8-
"toolCount": 11,
8+
"toolCount": 12,
99
"tools": [
1010
{
1111
"name": "find_organizations",
@@ -37,6 +37,11 @@
3737
"description": "Get detailed information about a specific Sentry issue by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific issue ID (e.g., 'CLOUDFLARE-MCP-41', 'PROJECT-123')\n- Ask to 'explain [ISSUE-ID]', 'tell me about [ISSUE-ID]'\n- Want details/stacktrace/analysis for a known issue\n- Provide a Sentry issue URL\n\nDO NOT USE for:\n- General searching or listing issues (use search_issues)\n- Root cause analysis (use analyze_issue_with_seer)\n\nTRIGGER PATTERNS:\n- 'Explain ISSUE-123' → use get_issue_details\n- 'Tell me about PROJECT-456' → use get_issue_details\n- 'What happened in [issue URL]' → use get_issue_details\n\n<examples>\n### With Sentry URL (recommended - simplest approach)\n```\nget_issue_details(issueUrl='https://sentry.sentry.io/issues/6916805731/?project=4509062593708032&query=is%3Aunresolved')\n```\n\n### With issue ID and organization\n```\nget_issue_details(organizationSlug='my-organization', issueId='CLOUDFLARE-MCP-41')\n```\n\n### With event ID and organization\n```\nget_issue_details(organizationSlug='my-organization', eventId='c49541c747cb4d8aa3efb70ca5aba243')\n```\n</examples>\n\n<hints>\n- **IMPORTANT**: If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- When using issueUrl, all other parameters are automatically extracted - don't provide them separately\n- If using issueId (not URL), then organizationSlug is required\n</hints>",
3838
"requiredScopes": ["event:read"]
3939
},
40+
{
41+
"name": "get_issue_tag_values",
42+
"description": "Get tag value distribution for a specific Sentry issue.\n\nUse this tool when you need to:\n- Understand how an issue is distributed across different tag values\n- Get aggregate counts of unique tag values (e.g., 'how many unique URLs are affected')\n- Analyze which browsers, environments, or URLs are most impacted by an issue\n- View the tag distributions page data programmatically\n\nCommon tag keys:\n- `url`: Request URLs affected by the issue\n- `browser`: Browser types and versions\n- `browser.name`: Browser names only\n- `os`: Operating systems\n- `environment`: Deployment environments (production, staging, etc.)\n- `release`: Software releases\n- `device`: Device types\n- `user`: Affected users\n\n<examples>\n### Get URL distribution for an issue\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='url')\n```\n\n### Get browser distribution using issue URL\n```\nget_issue_tag_values(issueUrl='https://sentry.io/issues/PROJECT-123/', tagKey='browser')\n```\n\n### Get environment distribution\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='environment')\n```\n</examples>\n\n<hints>\n- If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- Common tag keys: url, browser, browser.name, os, environment, release, device, user\n- Tag keys are case-sensitive\n</hints>",
43+
"requiredScopes": ["event:read"]
44+
},
4045
{
4146
"name": "get_trace_details",
4247
"description": "Get detailed information about a specific Sentry trace by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific trace ID (e.g., 'a4d1aae7216b47ff8117cf4e09ce9d0a')\n- Ask to 'show me trace [TRACE-ID]', 'explain trace [TRACE-ID]'\n- Want high-level overview and link to view trace details in Sentry\n- Need trace statistics and span breakdown\n\nDO NOT USE for:\n- General searching for traces (use search_events with trace queries)\n- Individual span details (this shows trace overview)\n\nTRIGGER PATTERNS:\n- 'Show me trace abc123' → use get_trace_details\n- 'Explain trace a4d1aae7216b47ff8117cf4e09ce9d0a' → use get_trace_details\n- 'What is trace [trace-id]' → use get_trace_details\n\n<examples>\n### Get trace overview\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a')\n```\n</examples>\n\n<hints>\n- Trace IDs are 32-character hexadecimal strings\n</hints>",

‎packages/mcp-core/src/toolDefinitions.json‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,50 @@
475475
},
476476
"requiredScopes": ["event:read"]
477477
},
478+
{
479+
"name": "get_issue_tag_values",
480+
"description": "Get tag value distribution for a specific Sentry issue.\n\nUse this tool when you need to:\n- Understand how an issue is distributed across different tag values\n- Get aggregate counts of unique tag values (e.g., 'how many unique URLs are affected')\n- Analyze which browsers, environments, or URLs are most impacted by an issue\n- View the tag distributions page data programmatically\n\nCommon tag keys:\n- `url`: Request URLs affected by the issue\n- `browser`: Browser types and versions\n- `browser.name`: Browser names only\n- `os`: Operating systems\n- `environment`: Deployment environments (production, staging, etc.)\n- `release`: Software releases\n- `device`: Device types\n- `user`: Affected users\n\n<examples>\n### Get URL distribution for an issue\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='url')\n```\n\n### Get browser distribution using issue URL\n```\nget_issue_tag_values(issueUrl='https://sentry.io/issues/PROJECT-123/', tagKey='browser')\n```\n\n### Get environment distribution\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='environment')\n```\n</examples>\n\n<hints>\n- If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- Common tag keys: url, browser, browser.name, os, environment, release, device, user\n- Tag keys are case-sensitive\n</hints>",
481+
"inputSchema": {
482+
"type": "object",
483+
"properties": {
484+
"organizationSlug": {
485+
"type": "string",
486+
"description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
487+
},
488+
"regionUrl": {
489+
"anyOf": [
490+
{
491+
"type": "string",
492+
"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."
493+
},
494+
{
495+
"type": "null"
496+
}
497+
],
498+
"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.",
499+
"default": null
500+
},
501+
"issueId": {
502+
"type": "string",
503+
"description": "The Issue ID. e.g. `PROJECT-1Z43`"
504+
},
505+
"issueUrl": {
506+
"type": "string",
507+
"format": "uri",
508+
"description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43"
509+
},
510+
"tagKey": {
511+
"type": "string",
512+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$",
513+
"description": "The tag key to get values for (e.g., 'url', 'browser', 'environment', 'release')."
514+
}
515+
},
516+
"required": ["tagKey"],
517+
"additionalProperties": false,
518+
"$schema": "http://json-schema.org/draft-07/schema#"
519+
},
520+
"requiredScopes": ["event:read"]
521+
},
478522
{
479523
"name": "get_trace_details",
480524
"description": "Get detailed information about a specific Sentry trace by ID.\n\nUSE THIS TOOL WHEN USERS:\n- Provide a specific trace ID (e.g., 'a4d1aae7216b47ff8117cf4e09ce9d0a')\n- Ask to 'show me trace [TRACE-ID]', 'explain trace [TRACE-ID]'\n- Want high-level overview and link to view trace details in Sentry\n- Need trace statistics and span breakdown\n\nDO NOT USE for:\n- General searching for traces (use search_events with trace queries)\n- Individual span details (this shows trace overview)\n\nTRIGGER PATTERNS:\n- 'Show me trace abc123' → use get_trace_details\n- 'Explain trace a4d1aae7216b47ff8117cf4e09ce9d0a' → use get_trace_details\n- 'What is trace [trace-id]' → use get_trace_details\n\n<examples>\n### Get trace overview\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a')\n```\n</examples>\n\n<hints>\n- Trace IDs are 32-character hexadecimal strings\n</hints>",
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { describe, it, expect } from "vitest";
2+
import getIssueTagValues from "./get-issue-tag-values.js";
3+
import { getServerContext } from "../test-setup.js";
4+
import { UserInputError } from "../errors.js";
5+
6+
describe("get_issue_tag_values", () => {
7+
it("returns tag value distribution for an issue", async () => {
8+
const result = await getIssueTagValues.handler(
9+
{
10+
organizationSlug: "sentry-mcp-evals",
11+
issueId: "CLOUDFLARE-MCP-41",
12+
tagKey: "url",
13+
regionUrl: null,
14+
issueUrl: undefined,
15+
},
16+
getServerContext(),
17+
);
18+
expect(result).toMatchInlineSnapshot(`
19+
"# Tag Distribution: Url
20+
21+
**Issue**: CLOUDFLARE-MCP-41
22+
**Tag Key**: \`url\`
23+
**Total Unique Values**: 156
24+
25+
## Top Values
26+
27+
| Value | Count | First Seen | Last Seen |
28+
|-------|-------|------------|----------|
29+
| \`/upload/github/org/repo/commit/abc123\` | 45 | 2024-01-10 | 2024-01-15 |
30+
| \`/api/v1/users/profile\` | 32 | 2024-01-11 | 2024-01-15 |
31+
| \`/dashboard/overview\` | 28 | 2024-01-12 | 2024-01-15 |
32+
| \`/settings/notifications\` | 21 | 2024-01-13 | 2024-01-14 |
33+
| \`/checkout/payment\` | 15 | 2024-01-14 | 2024-01-14 |
34+
35+
*Showing top 5 of 156 unique values*
36+
37+
## Using this information
38+
39+
- Use \`get_issue_details(organizationSlug='sentry-mcp-evals', issueId='CLOUDFLARE-MCP-41')\` to see the full issue details
40+
- Try other tag keys like: url, browser, environment, release, os, device, user
41+
"
42+
`);
43+
});
44+
45+
it("works with issue URL parameter", async () => {
46+
const result = await getIssueTagValues.handler(
47+
{
48+
organizationSlug: undefined,
49+
issueId: undefined,
50+
tagKey: "browser",
51+
regionUrl: null,
52+
issueUrl:
53+
"https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41/",
54+
},
55+
getServerContext(),
56+
);
57+
expect(result).toContain("# Tag Distribution: Browser");
58+
expect(result).toContain("**Tag Key**: `browser`");
59+
});
60+
61+
it("throws error when neither issueId nor issueUrl provided", async () => {
62+
await expect(
63+
getIssueTagValues.handler(
64+
{
65+
organizationSlug: "sentry-mcp-evals",
66+
issueId: undefined,
67+
tagKey: "url",
68+
regionUrl: null,
69+
issueUrl: undefined,
70+
},
71+
getServerContext(),
72+
),
73+
).rejects.toThrow(UserInputError);
74+
});
75+
76+
it("throws error when organizationSlug missing with issueId", async () => {
77+
await expect(
78+
getIssueTagValues.handler(
79+
{
80+
organizationSlug: undefined,
81+
issueId: "CLOUDFLARE-MCP-41",
82+
tagKey: "url",
83+
regionUrl: null,
84+
issueUrl: undefined,
85+
},
86+
getServerContext(),
87+
),
88+
).rejects.toThrow(UserInputError);
89+
});
90+
91+
it("throws error when tagKey is missing", async () => {
92+
await expect(
93+
getIssueTagValues.handler(
94+
{
95+
organizationSlug: "sentry-mcp-evals",
96+
issueId: "CLOUDFLARE-MCP-41",
97+
tagKey: "",
98+
regionUrl: null,
99+
issueUrl: undefined,
100+
},
101+
getServerContext(),
102+
),
103+
).rejects.toThrow(UserInputError);
104+
});
105+
106+
it("throws error when tagKey contains path traversal characters", async () => {
107+
await expect(
108+
getIssueTagValues.handler(
109+
{
110+
organizationSlug: "sentry-mcp-evals",
111+
issueId: "CLOUDFLARE-MCP-41",
112+
tagKey: "../../../admin",
113+
regionUrl: null,
114+
issueUrl: undefined,
115+
},
116+
getServerContext(),
117+
),
118+
).rejects.toThrow();
119+
});
120+
121+
it("throws error when tagKey contains slashes", async () => {
122+
await expect(
123+
getIssueTagValues.handler(
124+
{
125+
organizationSlug: "sentry-mcp-evals",
126+
issueId: "CLOUDFLARE-MCP-41",
127+
tagKey: "url/path",
128+
regionUrl: null,
129+
issueUrl: undefined,
130+
},
131+
getServerContext(),
132+
),
133+
).rejects.toThrow();
134+
});
135+
});

0 commit comments

Comments
 (0)