Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
47 changes: 47 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
ReleaseListSchema,
IssueListSchema,
IssueSchema,
IssueTagValuesSchema,
EventSchema,
EventAttachmentListSchema,
ErrorsSearchResponseSchema,
Expand Down Expand Up @@ -42,6 +43,7 @@ import type {
EventAttachmentList,
Issue,
IssueList,
IssueTagValues,
OrganizationList,
Project,
ProjectList,
Expand Down Expand Up @@ -1546,6 +1548,51 @@ export class SentryApiService {
return IssueSchema.parse(body);
}

/**
* Retrieves tag value distribution for a specific issue.
*
* Returns aggregate counts of unique tag values, useful for understanding
* how an issue is distributed across different tag values (e.g., URLs,
* browsers, environments).
*
* @param params Query parameters
* @param params.organizationSlug Organization identifier
* @param params.issueId Issue identifier (short ID or numeric ID)
* @param params.tagKey Tag key to get values for (e.g., "url", "browser", "environment")
* @param opts Request options
* @returns Tag value distribution with counts and percentages
*
* @example
* ```typescript
* const tagValues = await apiService.getIssueTagValues({
* organizationSlug: "my-org",
* issueId: "PROJECT-123",
* tagKey: "url"
* });
* console.log(`Total unique values: ${tagValues.totalValues}`);
* tagValues.topValues.forEach(v => console.log(`${v.value}: ${v.count}`));
* ```
*/
async getIssueTagValues(
{
organizationSlug,
issueId,
tagKey,
}: {
organizationSlug: string;
issueId: string;
tagKey: string;
},
opts?: RequestOptions,
): Promise<IssueTagValues> {
const body = await this.requestJSON(
`/organizations/${organizationSlug}/issues/${issueId}/tags/${tagKey}/`,
undefined,
opts,
);
return IssueTagValuesSchema.parse(body);
}

async getEventForIssue(
{
organizationSlug,
Expand Down
27 changes: 27 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,33 @@ export const EventAttachmentSchema = z.object({

export const EventAttachmentListSchema = z.array(EventAttachmentSchema);

/**
* Schema for individual tag values within an issue's tag distribution.
*
* Represents a single value's occurrence count and percentage within a tag.
*/
export const IssueTagValueSchema = z.object({
key: z.string().optional(),
name: z.string().optional(),
value: z.string(),
count: z.number(),
lastSeen: z.string().datetime().optional(),
firstSeen: z.string().datetime().optional(),
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
});

/**
* Schema for Sentry issue tag values response.
*
* Contains aggregate counts of unique tag values for an issue,
* useful for understanding the distribution of tags like URL, browser, etc.
*/
export const IssueTagValuesSchema = z.object({
key: z.string(),
name: z.string(),
totalValues: z.number(),
topValues: z.array(IssueTagValueSchema),
});

/**
* Schema for Sentry trace metadata response.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/mcp-core/src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import type {
EventAttachmentListSchema,
IssueListSchema,
IssueSchema,
IssueTagValuesSchema,
OrganizationListSchema,
OrganizationSchema,
ProjectListSchema,
Expand Down Expand Up @@ -116,3 +117,6 @@ export type TraceMeta = z.infer<typeof TraceMetaSchema>;
export type TraceSpan = z.infer<typeof TraceSpanSchema>;
export type TraceIssue = z.infer<typeof TraceIssueSchema>;
export type Trace = z.infer<typeof TraceSchema>;

// Issue tag values
export type IssueTagValues = z.infer<typeof IssueTagValuesSchema>;
7 changes: 6 additions & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "Search for errors, analyze traces, and explore event details",
"defaultEnabled": true,
"order": 1,
"toolCount": 11,
"toolCount": 12,
"tools": [
{
"name": "find_organizations",
Expand Down Expand Up @@ -37,6 +37,11 @@
"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>",
"requiredScopes": ["event:read"]
},
{
"name": "get_issue_tag_values",
"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>",
"requiredScopes": ["event:read"]
},
{
"name": "get_trace_details",
"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>",
Expand Down
44 changes: 44 additions & 0 deletions packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,50 @@
},
"requiredScopes": ["event:read"]
},
{
"name": "get_issue_tag_values",
"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>",
"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
},
"issueId": {
"type": "string",
"description": "The Issue ID. e.g. `PROJECT-1Z43`"
},
"issueUrl": {
"type": "string",
"format": "uri",
"description": "The URL of the issue. e.g. https://my-organization.sentry.io/issues/PROJECT-1Z43"
},
"tagKey": {
"type": "string",
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]*$",
"description": "The tag key to get values for (e.g., 'url', 'browser', 'environment', 'release')."
}
},
"required": ["tagKey"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"requiredScopes": ["event:read"]
},
{
"name": "get_trace_details",
"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>",
Expand Down
135 changes: 135 additions & 0 deletions packages/mcp-core/src/tools/get-issue-tag-values.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { describe, it, expect } from "vitest";
import getIssueTagValues from "./get-issue-tag-values.js";
import { getServerContext } from "../test-setup.js";
import { UserInputError } from "../errors.js";

describe("get_issue_tag_values", () => {
it("returns tag value distribution for an issue", async () => {
const result = await getIssueTagValues.handler(
{
organizationSlug: "sentry-mcp-evals",
issueId: "CLOUDFLARE-MCP-41",
tagKey: "url",
regionUrl: null,
issueUrl: undefined,
},
getServerContext(),
);
expect(result).toMatchInlineSnapshot(`
"# Tag Distribution: Url

**Issue**: CLOUDFLARE-MCP-41
**Tag Key**: \`url\`
**Total Unique Values**: 156

## Top Values

| Value | Count | First Seen | Last Seen |
|-------|-------|------------|----------|
| \`/upload/github/org/repo/commit/abc123\` | 45 | 2024-01-10 | 2024-01-15 |
| \`/api/v1/users/profile\` | 32 | 2024-01-11 | 2024-01-15 |
| \`/dashboard/overview\` | 28 | 2024-01-12 | 2024-01-15 |
| \`/settings/notifications\` | 21 | 2024-01-13 | 2024-01-14 |
| \`/checkout/payment\` | 15 | 2024-01-14 | 2024-01-14 |

*Showing top 5 of 156 unique values*

## Using this information

- Use \`get_issue_details(issueId='CLOUDFLARE-MCP-41')\` to see the full issue details
- Try other tag keys like: url, browser, environment, release, os, device, user
"
`);
});

it("works with issue URL parameter", async () => {
const result = await getIssueTagValues.handler(
{
organizationSlug: undefined,
issueId: undefined,
tagKey: "browser",
regionUrl: null,
issueUrl:
"https://sentry-mcp-evals.sentry.io/issues/CLOUDFLARE-MCP-41/",
},
getServerContext(),
);
expect(result).toContain("# Tag Distribution: Browser");
expect(result).toContain("**Tag Key**: `browser`");
});

it("throws error when neither issueId nor issueUrl provided", async () => {
await expect(
getIssueTagValues.handler(
{
organizationSlug: "sentry-mcp-evals",
issueId: undefined,
tagKey: "url",
regionUrl: null,
issueUrl: undefined,
},
getServerContext(),
),
).rejects.toThrow(UserInputError);
});

it("throws error when organizationSlug missing with issueId", async () => {
await expect(
getIssueTagValues.handler(
{
organizationSlug: undefined,
issueId: "CLOUDFLARE-MCP-41",
tagKey: "url",
regionUrl: null,
issueUrl: undefined,
},
getServerContext(),
),
).rejects.toThrow(UserInputError);
});

it("throws error when tagKey is missing", async () => {
await expect(
getIssueTagValues.handler(
{
organizationSlug: "sentry-mcp-evals",
issueId: "CLOUDFLARE-MCP-41",
tagKey: "",
regionUrl: null,
issueUrl: undefined,
},
getServerContext(),
),
).rejects.toThrow(UserInputError);
});

it("throws error when tagKey contains path traversal characters", async () => {
await expect(
getIssueTagValues.handler(
{
organizationSlug: "sentry-mcp-evals",
issueId: "CLOUDFLARE-MCP-41",
tagKey: "../../../admin",
regionUrl: null,
issueUrl: undefined,
},
getServerContext(),
),
).rejects.toThrow();
});

it("throws error when tagKey contains slashes", async () => {
await expect(
getIssueTagValues.handler(
{
organizationSlug: "sentry-mcp-evals",
issueId: "CLOUDFLARE-MCP-41",
tagKey: "url/path",
regionUrl: null,
issueUrl: undefined,
},
getServerContext(),
),
).rejects.toThrow();
});
});
Loading
Loading