diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 1622d3104..adbe50466 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -15,6 +15,7 @@ import { ReleaseListSchema, IssueListSchema, IssueSchema, + IssueTagValuesSchema, EventSchema, EventAttachmentListSchema, ErrorsSearchResponseSchema, @@ -42,6 +43,7 @@ import type { EventAttachmentList, Issue, IssueList, + IssueTagValues, OrganizationList, Project, ProjectList, @@ -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 { + const body = await this.requestJSON( + `/organizations/${organizationSlug}/issues/${issueId}/tags/${tagKey}/`, + undefined, + opts, + ); + return IssueTagValuesSchema.parse(body); + } + async getEventForIssue( { organizationSlug, diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index de23c85d2..a35a6a0cc 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -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().nullable().optional(), + firstSeen: z.string().datetime().nullable().optional(), +}); + +/** + * 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. * diff --git a/packages/mcp-core/src/api-client/types.ts b/packages/mcp-core/src/api-client/types.ts index ecb5bae2d..7adced4a1 100644 --- a/packages/mcp-core/src/api-client/types.ts +++ b/packages/mcp-core/src/api-client/types.ts @@ -55,6 +55,7 @@ import type { EventAttachmentListSchema, IssueListSchema, IssueSchema, + IssueTagValuesSchema, OrganizationListSchema, OrganizationSchema, ProjectListSchema, @@ -116,3 +117,6 @@ export type TraceMeta = z.infer; export type TraceSpan = z.infer; export type TraceIssue = z.infer; export type Trace = z.infer; + +// Issue tag values +export type IssueTagValues = z.infer; diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index d222d1ffc..e1b720e8b 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -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", @@ -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\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\n\n\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", "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\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\n\n\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", + "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\n### Get trace overview\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a')\n```\n\n\n\n- Trace IDs are 32-character hexadecimal strings\n", diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index b98b18525..dcc345e2a 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -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\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\n\n\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", + "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\n### Get trace overview\n```\nget_trace_details(organizationSlug='my-organization', traceId='a4d1aae7216b47ff8117cf4e09ce9d0a')\n```\n\n\n\n- Trace IDs are 32-character hexadecimal strings\n", diff --git a/packages/mcp-core/src/tools/get-issue-tag-values.test.ts b/packages/mcp-core/src/tools/get-issue-tag-values.test.ts new file mode 100644 index 000000000..197170568 --- /dev/null +++ b/packages/mcp-core/src/tools/get-issue-tag-values.test.ts @@ -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(organizationSlug='sentry-mcp-evals', 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(); + }); +}); diff --git a/packages/mcp-core/src/tools/get-issue-tag-values.ts b/packages/mcp-core/src/tools/get-issue-tag-values.ts new file mode 100644 index 000000000..1a866b7fc --- /dev/null +++ b/packages/mcp-core/src/tools/get-issue-tag-values.ts @@ -0,0 +1,183 @@ +import { z } from "zod"; +import { setTag } from "@sentry/core"; +import { defineTool } from "../internal/tool-helpers/define"; +import { apiServiceFromContext } from "../internal/tool-helpers/api"; +import { parseIssueParams } from "../internal/tool-helpers/issue"; +import { enhanceNotFoundError } from "../internal/tool-helpers/enhance-error"; +import { ApiNotFoundError } from "../api-client"; +import { UserInputError } from "../errors"; +import type { ServerContext } from "../types"; +import { + ParamOrganizationSlug, + ParamRegionUrl, + ParamIssueShortId, + ParamIssueUrl, +} from "../schema"; + +export default defineTool({ + name: "get_issue_tag_values", + skills: ["inspect"], // Available in inspect skill for understanding issue distribution + requiredScopes: ["event:read"], + description: [ + "Get tag value distribution for a specific Sentry issue.", + "", + "Use this tool when you need to:", + "- Understand how an issue is distributed across different tag values", + "- Get aggregate counts of unique tag values (e.g., 'how many unique URLs are affected')", + "- Analyze which browsers, environments, or URLs are most impacted by an issue", + "- View the tag distributions page data programmatically", + "", + "Common tag keys:", + "- `url`: Request URLs affected by the issue", + "- `browser`: Browser types and versions", + "- `browser.name`: Browser names only", + "- `os`: Operating systems", + "- `environment`: Deployment environments (production, staging, etc.)", + "- `release`: Software releases", + "- `device`: Device types", + "- `user`: Affected users", + "", + "", + "### Get URL distribution for an issue", + "```", + "get_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='url')", + "```", + "", + "### Get browser distribution using issue URL", + "```", + "get_issue_tag_values(issueUrl='https://sentry.io/issues/PROJECT-123/', tagKey='browser')", + "```", + "", + "### Get environment distribution", + "```", + "get_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='environment')", + "```", + "", + "", + "", + "- If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged", + "- Common tag keys: url, browser, browser.name, os, environment, release, device, user", + "- Tag keys are case-sensitive", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug.optional(), + regionUrl: ParamRegionUrl.nullable().default(null), + issueId: ParamIssueShortId.optional(), + issueUrl: ParamIssueUrl.optional(), + tagKey: z + .string() + .trim() + .regex( + /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/, + "Tag key must contain only alphanumeric characters, dots, hyphens, and underscores, and must start with an alphanumeric character", + ) + .describe( + "The tag key to get values for (e.g., 'url', 'browser', 'environment', 'release').", + ), + }, + annotations: { + readOnlyHint: true, + openWorldHint: true, + }, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? undefined, + }); + + // Validate that we have the minimum required parameters + if (!params.issueUrl && !params.issueId) { + throw new UserInputError( + "Either `issueId` or `issueUrl` must be provided", + ); + } + + if (!params.issueUrl && !params.organizationSlug) { + throw new UserInputError( + "`organizationSlug` is required when providing `issueId`", + ); + } + + if (!params.tagKey) { + throw new UserInputError( + "`tagKey` is required. Common values: url, browser, environment, release, os, device, user", + ); + } + + const { organizationSlug: orgSlug, issueId: parsedIssueId } = + parseIssueParams({ + organizationSlug: params.organizationSlug, + issueId: params.issueId, + issueUrl: params.issueUrl, + }); + + setTag("organization.slug", orgSlug); + + // Fetch the tag values for the issue + let tagValues: Awaited>; + try { + tagValues = await apiService.getIssueTagValues({ + organizationSlug: orgSlug, + issueId: parsedIssueId!, + tagKey: params.tagKey, + }); + } catch (error) { + if (error instanceof ApiNotFoundError) { + throw enhanceNotFoundError(error, { + organizationSlug: orgSlug, + issueId: parsedIssueId, + tagKey: params.tagKey, + }); + } + throw error; + } + + // Format the output + let output = `# Tag Distribution: ${tagValues.name}\n\n`; + output += `**Issue**: ${parsedIssueId}\n`; + output += `**Tag Key**: \`${tagValues.key}\`\n`; + output += `**Total Unique Values**: ${tagValues.totalValues}\n\n`; + + if (tagValues.topValues.length === 0) { + output += "No values found for this tag.\n"; + return output; + } + + output += "## Top Values\n\n"; + output += "| Value | Count | First Seen | Last Seen |\n"; + output += "|-------|-------|------------|----------|\n"; + + for (const value of tagValues.topValues) { + const firstSeen = value.firstSeen + ? new Date(value.firstSeen).toISOString().split("T")[0] + : "-"; + const lastSeen = value.lastSeen + ? new Date(value.lastSeen).toISOString().split("T")[0] + : "-"; + // Truncate long values for readability + let displayValue = + value.value.length > 60 + ? `${value.value.substring(0, 57)}...` + : value.value; + // Escape markdown table special characters (backslashes first) + displayValue = displayValue + .replace(/\\/g, "\\\\") + .replace(/\|/g, "\\|") + .replace(/`/g, "\\`") + .replace(/\n/g, " "); + output += `| \`${displayValue}\` | ${value.count} | ${firstSeen} | ${lastSeen} |\n`; + } + + if (tagValues.topValues.length > 0 && tagValues.totalValues > 0) { + const shownCount = tagValues.topValues.length; + output += `\n*Showing top ${shownCount} of ${tagValues.totalValues} unique values*\n`; + } + + // Add usage hints + output += "\n## Using this information\n\n"; + output += `- Use \`get_issue_details(organizationSlug='${orgSlug}', issueId='${parsedIssueId}')\` to see the full issue details\n`; + output += `- Try other tag keys like: url, browser, environment, release, os, device, user\n`; + + return output; + }, +}); diff --git a/packages/mcp-core/src/tools/index.ts b/packages/mcp-core/src/tools/index.ts index 6d1e97805..4e7e45f45 100644 --- a/packages/mcp-core/src/tools/index.ts +++ b/packages/mcp-core/src/tools/index.ts @@ -4,6 +4,7 @@ import findTeams from "./find-teams"; import findProjects from "./find-projects"; import findReleases from "./find-releases"; import getIssueDetails from "./get-issue-details"; +import getIssueTagValues from "./get-issue-tag-values"; import getTraceDetails from "./get-trace-details"; import getEventAttachment from "./get-event-attachment"; import updateIssue from "./update-issue"; @@ -28,6 +29,7 @@ export default { find_projects: findProjects, find_releases: findReleases, get_issue_details: getIssueDetails, + get_issue_tag_values: getIssueTagValues, get_trace_details: getTraceDetails, get_event_attachment: getEventAttachment, update_issue: updateIssue, diff --git a/packages/mcp-core/src/tools/use-sentry/handler.test.ts b/packages/mcp-core/src/tools/use-sentry/handler.test.ts index e6162c6fc..fe23f3789 100644 --- a/packages/mcp-core/src/tools/use-sentry/handler.test.ts +++ b/packages/mcp-core/src/tools/use-sentry/handler.test.ts @@ -58,9 +58,9 @@ describe("use_sentry handler", () => { }), }); - // Verify all 20 tools were provided (21 total - use_sentry itself) + // Verify all 21 tools were provided (22 total - use_sentry itself) const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools; - expect(Object.keys(toolsArg)).toHaveLength(20); + expect(Object.keys(toolsArg)).toHaveLength(21); // Verify result is returned expect(result).toBe("Agent executed tools successfully"); @@ -107,8 +107,8 @@ describe("use_sentry handler", () => { // Verify use_sentry is NOT in the list expect(toolNames).not.toContain("use_sentry"); - // Verify we have exactly 20 tools (21 total - 1 use_sentry) - expect(toolNames).toHaveLength(20); + // Verify we have exactly 21 tools (22 total - 1 use_sentry) + expect(toolNames).toHaveLength(21); }); it("filters find_organizations when organizationSlug constraint is set", async () => { @@ -134,8 +134,8 @@ describe("use_sentry handler", () => { const toolsArg = mockUseSentryAgent.mock.calls[0][0].tools; expect(toolsArg).toBeDefined(); - // With only org constraint, find_organizations is filtered (20 - 1 = 19) - expect(Object.keys(toolsArg)).toHaveLength(19); + // With only org constraint, find_organizations is filtered (21 - 1 = 20) + expect(Object.keys(toolsArg)).toHaveLength(20); // Verify find_organizations is filtered but find_projects remains expect(toolsArg.find_organizations).toBeUndefined(); @@ -167,8 +167,8 @@ describe("use_sentry handler", () => { expect(toolsArg).toBeDefined(); // When both org and project constraints are present, - // find_organizations and find_projects are filtered out (20 - 2 = 18) - expect(Object.keys(toolsArg)).toHaveLength(18); + // find_organizations and find_projects are filtered out (21 - 2 = 19) + expect(Object.keys(toolsArg)).toHaveLength(19); // Verify both find tools are filtered expect(toolsArg.find_organizations).toBeUndefined(); diff --git a/packages/mcp-server-mocks/src/fixtures/issue-tag-values.json b/packages/mcp-server-mocks/src/fixtures/issue-tag-values.json new file mode 100644 index 000000000..2c2925f6c --- /dev/null +++ b/packages/mcp-server-mocks/src/fixtures/issue-tag-values.json @@ -0,0 +1,47 @@ +{ + "key": "url", + "name": "URL", + "totalValues": 156, + "topValues": [ + { + "key": "url", + "name": "/upload/github/org/repo/commit/abc123", + "value": "/upload/github/org/repo/commit/abc123", + "count": 45, + "lastSeen": "2024-01-15T10:30:00.000Z", + "firstSeen": "2024-01-10T08:00:00.000Z" + }, + { + "key": "url", + "name": "/api/v1/users/profile", + "value": "/api/v1/users/profile", + "count": 32, + "lastSeen": "2024-01-15T09:45:00.000Z", + "firstSeen": "2024-01-11T14:20:00.000Z" + }, + { + "key": "url", + "name": "/dashboard/overview", + "value": "/dashboard/overview", + "count": 28, + "lastSeen": "2024-01-15T08:15:00.000Z", + "firstSeen": "2024-01-12T11:30:00.000Z" + }, + { + "key": "url", + "name": "/settings/notifications", + "value": "/settings/notifications", + "count": 21, + "lastSeen": "2024-01-14T22:00:00.000Z", + "firstSeen": "2024-01-13T16:45:00.000Z" + }, + { + "key": "url", + "name": "/checkout/payment", + "value": "/checkout/payment", + "count": 15, + "lastSeen": "2024-01-14T18:30:00.000Z", + "firstSeen": "2024-01-14T10:00:00.000Z" + } + ] +} diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index 9447e64a3..0fb7e5e31 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -64,6 +64,9 @@ import traceMixedFixture from "./fixtures/trace-mixed.json" with { import traceEventFixture from "./fixtures/trace-event.json" with { type: "json", }; +import issueTagValuesFixture from "./fixtures/issue-tag-values.json" with { + type: "json", +}; /** * Standard organization payload for mock responses. @@ -1040,6 +1043,20 @@ export const restHandlers = buildHandlers([ }, }), }, + // Issue tag values endpoints + { + method: "get", + path: "/api/0/organizations/:org/issues/:issueId/tags/:tagKey/", + fetch: ({ params }) => { + const tagKey = params.tagKey as string; + // Return fixture with the requested tag key + return HttpResponse.json({ + ...issueTagValuesFixture, + key: tagKey, + name: tagKey.charAt(0).toUpperCase() + tagKey.slice(1), + }); + }, + }, { method: "post", path: "/api/0/projects/sentry-mcp-evals/cloudflare-mcp/teams/:teamSlug/",