diff --git a/backend/__tests__/fixtures/hostedMcp/sentry-tools-list-2026-10-19.json b/backend/__tests__/fixtures/hostedMcp/sentry-tools-list-2026-10-19.json new file mode 100644 index 0000000000..1c29416116 --- /dev/null +++ b/backend/__tests__/fixtures/hostedMcp/sentry-tools-list-2026-10-19.json @@ -0,0 +1,1416 @@ +{ + "server": "https://mcp.sentry.dev/mcp", + "capturedAt": "2026-10-19T07:02:33.119Z", + "scopeRequested": "org:read project:write team:write event:write alerts:write", + "tokenShape": { + "keys": [ + "access_token", + "expires_in", + "issuer", + "refresh_token", + "scope", + "token_type" + ], + "tokenType": "bearer", + "grantedScope": "org:read project:write team:write event:write alerts:write", + "expiresIn": 3600, + "hasRefreshToken": true + }, + "toolCount": 15, + "tools": [ + { + "name": "find_organizations", + "description": "Find organizations that the user has access to in Sentry.\n\nUse this tool when you need to:\n- View organizations in Sentry\n- Find an organization's slug to aid other tool requests\n- Search for specific organizations by name or slug\n\nReturns up to 25 results per page. When hasMore is true, pass the returned nextCursor with the same filters and scope to fetch the next page.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items." + }, + { + "type": "null" + } + ] + }, + "cursor": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page." + }, + { + "type": "null" + } + ] + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "type": "object", + "properties": { + "organizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "slug": { + "type": "string" + }, + "webUrl": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + }, + "regionUrl": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "slug", + "webUrl", + "regionUrl" + ], + "additionalProperties": false + } + }, + "hasMore": { + "type": "boolean" + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "organizations", + "hasMore", + "nextCursor" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "find_projects", + "description": "Find projects in Sentry.\n\nUse this tool when you need to:\n- View projects in a Sentry organization\n- Find a project's slug to aid other tool requests\n- Search for specific projects by name or slug\n\nReturns up to 25 results per page. When hasMore is true, pass the returned nextCursor with the same filters and scope to fetch the next page.", + "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": { + "default": null, + "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" + } + ] + }, + "query": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Search query to filter results by name or slug. Use this to narrow down results when there are many items." + }, + { + "type": "null" + } + ] + }, + "cursor": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page." + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "slug": { + "type": "string" + } + }, + "required": [ + "slug" + ], + "additionalProperties": false + } + }, + "hasMore": { + "type": "boolean" + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "projects", + "hasMore", + "nextCursor" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "update_issue", + "description": "Update a Sentry issue's status or assignment.\n\nUse this to resolve, reopen, assign, unassign, or ignore an issue.\n\n\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', assignedTo=null)\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\n\n\n- Provide `issueUrl` or `organizationSlug` + `issueId`.\n- At least one of `status` or `assignedTo` is required.\n- Omit `assignedTo` to leave assignment unchanged. Pass `null` to unassign; otherwise use `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", + "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": { + "default": null, + "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" + } + ] + }, + "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" + }, + "status": { + "type": "string", + "enum": [ + "resolved", + "resolvedInNextRelease", + "unresolved", + "ignored" + ], + "description": "The new status for the issue. Valid values are 'resolved', 'resolvedInNextRelease', 'unresolved', and 'ignored'." + }, + "assignedTo": { + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "type": "null" + } + ], + "description": "The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Pass null to unassign the issue. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID." + }, + "ignoreMode": { + "type": "string", + "enum": [ + "untilEscalating", + "forever", + "forDuration", + "untilOccurrenceCount", + "untilUserCount" + ], + "description": "How ignored issues should behave. Use 'untilEscalating' to match the Sentry UI default, 'forever' for a permanent ignore, 'forDuration' with ignoreDurationMinutes, 'untilOccurrenceCount' with ignoreCount and optional ignoreWindowMinutes, or 'untilUserCount' with ignoreUserCount and optional ignoreUserWindowMinutes." + }, + "ignoreDurationMinutes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "How many minutes to ignore the issue when ignoreMode is 'forDuration'." + }, + "ignoreCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "How many times the issue must occur before it stops being ignored when ignoreMode is 'untilOccurrenceCount'." + }, + "ignoreWindowMinutes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 10080, + "description": "Optional time window in minutes for ignoreCount. If omitted, Sentry counts all future occurrences." + }, + "ignoreUserCount": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991, + "description": "How many users must be affected before the issue stops being ignored when ignoreMode is 'untilUserCount'." + }, + "ignoreUserWindowMinutes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 10080, + "description": "Optional time window in minutes for ignoreUserCount. If omitted, Sentry counts all future affected users." + }, + "reason": { + "type": "string", + "minLength": 1, + "description": "Optional reason for taking this action. When provided, it will be posted as a comment on the issue's activity feed." + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "search_errors", + "description": "Search Sentry error events: exceptions and crashes with stack traces. Use for error counts, statistics, trends, and individual error events.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('how many errors today', 'top error types'), individual events ('latest TypeErrors in checkout'), and time series ('errors per hour last 24h').\n\nNOT for log messages, including error/warning logs (use search_logs), requests or latency (use search_traces), or grouped issue lists (use search_issues).\n\n\nsearch_errors(organizationSlug='my-org', query='how many errors today')\nsearch_errors(organizationSlug='my-org', query='most common error types in production this week')\nsearch_errors(organizationSlug='my-org', query='errors per hour last 24h')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\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." + }, + "query": { + "description": "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + "type": "string" + }, + "fields": { + "description": "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "sort": { + "description": "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "period": { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of results to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_logs", + "description": "Search Sentry logs: application log entries, including error- and warning-severity log messages. Use for log counts, statistics, trends, and individual log lines.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('warning logs by service'), individual entries ('error logs from the last hour'), and time series ('error logs per hour').\n\nNOT for exceptions/crashes (use search_errors). For requests or spans whose trace also has a matching log, use search_traces.\n\n\nsearch_logs(organizationSlug='my-org', query='error logs from the last hour')\nsearch_logs(organizationSlug='my-org', query='logs mentioning payment timeout in production')\nsearch_logs(organizationSlug='my-org', query='count warning logs by service over 7 days')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\n- Sentry search syntax for logs includes RE2 regex filters, never quoted: `message://^Timeout after \\d+ms//`.\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." + }, + "query": { + "description": "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + "type": "string" + }, + "fields": { + "description": "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "sort": { + "description": "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "period": { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of results to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_traces", + "description": "Search Sentry spans and traces: requests, API/HTTP calls, endpoints, DB queries, AI/LLM calls, and other operations. Use for latency, throughput, slowness, and performance questions.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nAlso use for spans whose trace contains a matching log, metric, or other span, e.g. 'slow checkout requests that also logged an error'.\n\nSupports aggregations ('p95 duration by span.op'), individual spans ('slowest API calls today'), and time series ('requests per minute last hour').\n\nNOT for exceptions/crashes (use search_errors), standalone log lines (use search_logs), or a single trace's span tree (use get_sentry_resource with the trace).\n\n\nsearch_traces(organizationSlug='my-org', query='slowest API calls in the last 24 hours')\nsearch_traces(organizationSlug='my-org', query='p95 duration of db spans grouped by span.op over 7 days')\nsearch_traces(organizationSlug='my-org', query='checkout requests that also have an error log')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\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." + }, + "query": { + "description": "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + "type": "string" + }, + "fields": { + "description": "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "sort": { + "description": "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "period": { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of results to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_metrics", + "description": "Search Sentry metrics: counters, gauges, and distributions, as rows or aggregates. Use for metric values, percentiles, totals, and trends.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nSupports aggregations ('p95 http.request.duration by environment'), individual metric rows, and time series ('total tokens per day this week').\n\nNOT for span or request latency recorded on traces (use search_traces).\n\n\nsearch_metrics(organizationSlug='my-org', query='p95 http.request.duration grouped by environment over the last 24 hours')\nsearch_metrics(organizationSlug='my-org', query='total tokens used per day this week')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\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." + }, + "query": { + "description": "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + "type": "string" + }, + "fields": { + "description": "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "sort": { + "description": "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "period": { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of results to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_profiles", + "description": "Search Sentry profiles: transaction and continuous profile results, profile IDs, and profiled transactions. Use to find profiles to inspect.\n\n`query` is natural language (preferred) or Sentry search syntax; a configured agent translates it into query, fields, and sort.\n\nUse get_profile or get_profile_details on a result for flamegraph and hotspot analysis.\n\n\nsearch_profiles(organizationSlug='my-org', query='recent profiles for the /checkout transaction')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\n- Natural language is usually enough. Only pass fields/sort when you need exact columns or ordering.\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." + }, + "query": { + "description": "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + "type": "string" + }, + "fields": { + "description": "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + "anyOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "null" + } + ] + }, + "sort": { + "description": "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "period": { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of results to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_replays", + "description": "Search Sentry session replays: rage clicks, dead clicks, visited pages, errors seen, and replay users.\n\n`query` is natural language (preferred) or replay search syntax; a configured agent translates it into a replay search.\n\nReturns replay lists only; count()/avg()/sum() are not supported. Use get_replay_details on a result for one replay.\n\n\nsearch_replays(organizationSlug='my-org', query='replays with rage clicks on checkout in the last day')\nsearch_replays(organizationSlug='my-org', query='count_errors:>0', sort='-count_errors')\n\n\n\n- name/otherName notation means /; parse it directly, don't call find_organizations/find_projects.\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." + }, + "query": { + "description": "What to find, in natural language (preferred) or replay search syntax.", + "type": "string" + }, + "sort": { + "description": "Replay sort (prefix with - for descending): -started_at (default), -count_errors, -count_rage_clicks, or -duration.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "environment": { + "description": "Optional environment filter. Use a string for one environment or an array for multiple. Omit when unused.", + "anyOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + ] + }, + "period": { + "type": "string", + "pattern": "^\\d+[hdw]$", + "description": "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window." + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of results to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "analyze_issue_with_seer", + "description": "Use Seer to analyze production errors and get detailed root cause analysis with specific code fixes.\n\nUse this tool when:\n- The user explicitly asks for root cause analysis, Seer analysis, or help fixing/debugging an issue\n- You are unable to accurately determine the root cause from the issue details alone\n\nDo NOT call this tool as an automatic follow-up to get_sentry_resource.\n\nWhat this tool provides:\n- Root cause analysis with code-level explanations\n- Specific file locations and line numbers where errors occur\n- Concrete code fixes you can apply\n- Step-by-step implementation guidance\n\nThis tool automatically:\n1. Checks if analysis already exists (instant results)\n2. Starts new AI analysis if needed (~2-5 minutes)\n3. Returns complete fix recommendations\n\n\n### User: \"Run Seer on this issue\"\n\n```\nanalyze_issue_with_seer(issueUrl='https://my-org.sentry.io/issues/PROJECT-1Z43')\n```\n\n### User: \"Analyze this issue and suggest a fix\"\n\n```\nanalyze_issue_with_seer(organizationSlug='my-organization', issueId='ERROR-456')\n```\n\n\n\n- Only use when the user explicitly requests analysis or you cannot determine the root cause from issue details alone\n- Seer Autofix does not support metric alert issues (issueCategory: metric); use get_issue_details and search_metrics or search_traces instead\n- If the user provides an issueUrl, extract it and use that parameter alone\n- The analysis includes actual code snippets and fixes, not just error descriptions\n- Results are cached - subsequent calls return instantly\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": { + "default": null, + "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" + } + ] + }, + "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" + }, + "instruction": { + "type": "string", + "description": "Optional custom instruction for the AI analysis" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_issues", + "description": "Search for grouped issues/problems in Sentry - returns a LIST of issues, NOT counts or aggregations.\n\nProvide `query` as natural language or Sentry issue search syntax. When an embedded agent is configured, it fixes query and sort before running while preserving explicit Sentry search syntax.\n\nReturns grouped issues with metadata like title, status, and user count.\n\nCommon Query Syntax:\n- is:unresolved / is:resolved / is:ignored / is:for_review / is:new / is:regressed / is:escalating\n- level:error / level:warning\n- firstSeen:-24h / lastSeen:-7d\n- assigned:me / assigned_or_suggested:me\n- release:latest\n- issue.category:feedback\n- issue.priority:high\n- environment:production\n- userCount:>100\n\nDO NOT USE FOR COUNTS/AGGREGATIONS → use search_errors\nDO NOT USE FOR individual events with timestamps → use search_errors, search_logs, or search_traces\nDO NOT USE FOR details about a specific issue → use get_sentry_resource\n\n\nsearch_issues(organizationSlug='my-org', query='critical bugs from last week')\nsearch_issues(organizationSlug='my-org', query='is:unresolved is:unassigned', sort='freq')\nsearch_issues(organizationSlug='my-org', query='level:error firstSeen:-24h', projectSlugOrId='my-project')\n\n\n\n- If the user passes a parameter in the form of name/otherName, it's likely in the format of /.\n- Parse org/project notation directly without calling find_organizations or find_projects.\n- The projectSlugOrId parameter accepts both project slugs (e.g., 'my-project') and numeric IDs (e.g., '123456').\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." + }, + "query": { + "default": "is:unresolved", + "description": "Natural language or Sentry issue search query syntax.", + "type": "string" + }, + "sort": { + "default": "date", + "description": "Sort order: date (last seen), freq (frequency), new (first seen), user (user count), recommended (Sentry's prioritized ranking)", + "type": "string", + "enum": [ + "date", + "freq", + "new", + "user", + "recommended" + ] + }, + "projectSlugOrId": { + "default": null, + "description": "The project's slug or numeric ID (optional)", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "regionUrl": { + "default": null, + "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" + } + ] + }, + "limit": { + "default": 10, + "description": "Maximum number of issues to return (1-100)", + "type": "number", + "minimum": 1, + "maximum": 100 + }, + "period": { + "default": "30d", + "description": "Time window for issue search results. Controls which issues are returned based on when they had activity. Default 30d is a balance between coverage and query performance; use 24h for very recent issues or 90d for broader historical searches.", + "type": "string", + "enum": [ + "24h", + "7d", + "14d", + "30d", + "90d" + ] + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "find_dropped_events", + "description": "Find events dropped before they were stored in Sentry — ground-truth data-fidelity information about what was and wasn't captured.\n\nEvents can be dropped client-side in the SDK (sample_rate, before_send) or\nserver-side at ingest (rate limited, over quota, filtered, invalid, abuse/spike\nprotection, cardinality limited). Accepted-only views (searches, aggregates,\ncharts) can't show this, so the data may be incomplete in ways they don't reveal\n— for example, a flat or spiky chart caused entirely by drops.\n\nUse this tool when you need to:\n- Explain why a chart is flat, lower than expected, or doesn't match what the user is sending\n- Confirm the data you need is actually in Sentry (not dropped) before trusting a query, aggregate, or dashboard\n- Attribute a volume anomaly to a specific drop reason (quota, spike protection, sampling, filters)\n- Tell the user why their data is missing and what to do about it (raise quota, fix sampling, etc.)\n\nReturns dropped event volume bucketed over time, with the drop `outcome` and `reason`\nfor each bucket, plus the accepted volume per bucket so you can compute the dropped share.\n\n\nfind_dropped_events(organizationSlug='my-org', dataset='spans', projectSlug='my-project')\nfind_dropped_events(organizationSlug='my-org', dataset='logs', statsPeriod='30d')\nfind_dropped_events(organizationSlug='my-org', dataset='errors', outcome='rate_limited')\n\n\n\n- This is independent of any search query — it reports drops for the whole project/time range.\n- `outcome` is the drop kind (e.g. rate_limited, filtered); `reason` is the sub-cause (e.g. key_quota, sample_rate).\n- Pass `outcome` and/or `reason` to scope the dropped side to one classification; the accepted volume is always returned in full.\n- An empty `droppedEvents` list means no drops in the window — the data can be trusted.\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": { + "default": null, + "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" + } + ] + }, + "dataset": { + "default": "spans", + "type": "string", + "enum": [ + "spans", + "logs", + "metrics", + "errors" + ], + "description": "Which data type to report drops for." + }, + "projectSlug": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool." + }, + { + "type": "null" + } + ] + }, + "statsPeriod": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Relative time range, e.g. '24h', '7d', '30d'. Mutually exclusive with start/end." + }, + { + "type": "null" + } + ] + }, + "start": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Absolute start (ISO 8601). Must be paired with end." + }, + { + "type": "null" + } + ] + }, + "end": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Absolute end (ISO 8601). Must be paired with start." + }, + { + "type": "null" + } + ] + }, + "interval": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Bucket size, e.g. '1h', '1d'. Omit to let Sentry pick for the range." + }, + { + "type": "null" + } + ] + }, + "outcome": { + "default": null, + "anyOf": [ + { + "type": "string", + "enum": [ + "rate_limited", + "filtered", + "invalid", + "abuse", + "client_discard", + "cardinality_limited" + ], + "description": "Scope the dropped side to one top-level drop classification. Accepted volume is still returned in full." + }, + { + "type": "null" + } + ] + }, + "reason": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "Scope the dropped side to one reason (sub-classification within an outcome, e.g. 'spike_protection'). Combine with `outcome`." + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "organizationSlug" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "type": "object", + "properties": { + "dataset": { + "type": "string" + }, + "interval": { + "type": "number" + }, + "droppedEvents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "outcome": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "category": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "count": { + "type": "number" + } + }, + "required": [ + "outcome", + "reason", + "category", + "start", + "end", + "count" + ], + "additionalProperties": false + } + }, + "acceptedEvents": { + "type": "array", + "items": { + "type": "object", + "properties": { + "outcome": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "category": { + "type": "string" + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "count": { + "type": "number" + } + }, + "required": [ + "outcome", + "reason", + "category", + "start", + "end", + "count" + ], + "additionalProperties": false + } + } + }, + "required": [ + "dataset", + "interval", + "droppedEvents", + "acceptedEvents" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "get_sentry_resource", + "description": "Fetch a Sentry resource by URL, or by resourceType plus resourceId.\nPass a Sentry URL directly when possible; the resource type is auto-detected.\n\nSupports issues, events, traces, spans, agent conversations, replays, monitors, dashboards, preprod snapshots, and snapshot images.\nTrace lookups return a condensed overview by default.\n\nAgent Conversations: A conversation is a set of spans sharing the same gen_ai.conversation.id. Use resourceType='ai_conversation' with a conversation ID, or pass a Sentry conversation URL, to fetch the transcript/details. To discover or list conversations, use search_agent_conversations. Conversations are NOT issues — do not use search_issues for conversation queries.\n\nFor preprod snapshot URLs (matching 'sentry.io/preprod/snapshots/'):\n- Without ?selectedSnapshot=: returns the snapshot diff summary (changed, added, removed images)\n- With ?selectedSnapshot=: returns the image preview and metadata. Use the Sentry tool `get_snapshot_image` for full-resolution image bytes.\n\nResource IDs:\n- monitor: \n- dashboard: \n- snapshot: \n\n\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\nget_sentry_resource(resourceType='ai_conversation', organizationSlug='my-org', resourceId='conversation-123')\nget_sentry_resource(url='https://sentry.sentry.io/dashboard/542438/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/')\nget_sentry_resource(url='https://sentry.sentry.io/preprod/snapshots/123/?selectedSnapshot=login_screen.png')\n", + "inputSchema": { + "type": "object", + "properties": { + "url": { + "description": "Sentry URL. The resource type is auto-detected from the URL pattern.", + "type": "string", + "format": "uri" + }, + "resourceType": { + "description": "Resource type. With a URL, can override a span-focused trace URL with `trace`. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, or `dashboard` with a dashboard ID or exact title when inspect dashboard tools are available.", + "type": "string", + "enum": [ + "issue", + "event", + "trace", + "ai_conversation", + "replay", + "monitor", + "snapshot", + "dashboard" + ] + }, + "resourceId": { + "description": "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, agent conversation ID, replay ID, monitor slug when inspect monitor tools are available, dashboard ID or title when inspect dashboard tools are available, or snapshot artifact ID. Required when not using a URL.", + "type": "string" + }, + "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." + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + } + }, + { + "name": "search_sentry_tools", + "description": "Search the available Sentry MCP tool catalog by name and description.\n\nMany Sentry operations are intentionally not exposed as top-level tools. Use this for any Sentry-related task when you do not see an obvious direct tool, including long-tail inspection, project management, documentation lookup, preprod snapshots, attachments, DSNs, releases, teams, and issue-specific pivots.\n\nUse this tool when you need to:\n- Find the right Sentry operation for a task\n- Discover catalog tools and their schemas for a task\n- Inspect the executable JSON input schema for an available tool\n\n\nsearch_sentry_tools(query='list projects')\nsearch_sentry_tools(query='issue details')\nsearch_sentry_tools(query='find dsn', limit=5)\nsearch_sentry_tools(query='snapshot image')\n\n\n\n- Results only include tools available in the current session.\n- If a Sentry operation is not listed as a direct tool, search here before deciding it is unavailable.\n- Returned schemas already account for active organization, project, and region constraints.\n- Use the returned name and schema when executing a catalog result.\n- This tool returns structured JSON. Do not parse markdown from its text content.\n", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "description": "Natural language keywords describing the Sentry operation, resource, or workflow to find." + }, + "limit": { + "default": 8, + "description": "Maximum number of matching tools to return, up to 20.", + "anyOf": [ + { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 20 + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "query" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "outputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string" + }, + "results": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "inputSchema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "JSON Schema for the matching tool's arguments. Session-constrained parameters are omitted." + }, + "annotations": { + "type": "object", + "properties": { + "readOnlyHint": { + "type": "boolean" + }, + "destructiveHint": { + "type": "boolean" + }, + "idempotentHint": { + "type": "boolean" + }, + "openWorldHint": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": [ + "name", + "description", + "inputSchema", + "annotations" + ], + "additionalProperties": false + } + } + }, + "required": [ + "query", + "results" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema", + "additionalProperties": false + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": false + } + }, + { + "name": "execute_sentry_tool", + "description": "Execute an available Sentry MCP tool discovered through search_sentry_tools.\n\nUse this tool when you need to:\n- Call a Sentry operation returned by search_sentry_tools\n- Execute a tool by name using arguments that match its returned schema\n\n\nexecute_sentry_tool(name='find_projects', arguments={ organizationSlug: 'my-org' })\nexecute_sentry_tool(name='whoami', arguments={})\n\n\n\n- Use search_sentry_tools first if you are not sure which name or arguments to pass.\n- Arguments are validated against the target tool's schema before execution.\n- Active organization, project, and region constraints are injected automatically.\n", + "inputSchema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "description": "The name of the available tool to execute." + }, + "arguments": { + "default": {}, + "description": "Arguments for the target tool, matching the schema returned by search_sentry_tools.", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": [ + "name" + ], + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "openWorldHint": true + } + } + ] +} diff --git a/backend/__tests__/unit/services/hostedMcpEntry.test.js b/backend/__tests__/unit/services/hostedMcpEntry.test.js index b6fcb64c46..9bae416b3b 100644 --- a/backend/__tests__/unit/services/hostedMcpEntry.test.js +++ b/backend/__tests__/unit/services/hostedMcpEntry.test.js @@ -22,6 +22,7 @@ const LINEAR_FIRST_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list const LINEAR_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026-10-14.json'); const ATLASSIAN_CAPTURE = require('../../fixtures/hostedMcp/atlassian-tools-list-2026-10-09.json'); const AIRTABLE_CAPTURE = require('../../fixtures/hostedMcp/airtable-tools-list-2026-10-09.json'); +const SENTRY_CAPTURE = require('../../fixtures/hostedMcp/sentry-tools-list-2026-10-19.json'); const ENTRY_TEXT = 'ENTRY TEXT the pin agreed to.'; const VENDOR_TEXT = 'VENDOR TEXT nobody reviewed.'; @@ -564,10 +565,12 @@ describe('the comparison is cached per row, and an outage is not cacheable', () }); describe('the shipped catalogue cannot land half-wired', () => { - test('ships Linear, Google Calendar, Atlassian, then Airtable, and lookup names the requested entry', () => { + test('ships Linear, Google Calendar, Atlassian, Airtable and Sentry, and lookup names the requested entry', () => { // The catalogue is the one place that says what an instance offers before // any member has connected anything. Its ordering follows the build plan. - expect(HOSTED_MCP_ENTRIES.map((entry) => entry.id)).toEqual(['linear', 'google-calendar', 'atlassian', 'airtable']); + expect(HOSTED_MCP_ENTRIES.map((entry) => entry.id)).toEqual([ + 'linear', 'google-calendar', 'atlassian', 'airtable', 'sentry', + ]); const catalogue = [linear(), linear({ id: 'notion', title: 'Notion' })]; expect(findHostedMcpEntry(catalogue, 'notion').title).toBe('Notion'); expect(findHostedMcpEntry(catalogue, 'linear').title).toBe('Linear'); @@ -737,6 +740,48 @@ describe('the shipped catalogue cannot land half-wired', () => { expect(capture.tools).toHaveLength(46); }); + test('the Sentry pin restricts resource lookup to type and ID and parks its destructive writer', () => { + const entry = findHostedMcpEntry(HOSTED_MCP_ENTRIES, 'sentry'); + const capture = SENTRY_CAPTURE; + expect(entry).toMatchObject({ + resource: 'https://mcp.sentry.dev/mcp', + issuer: 'https://mcp.sentry.dev', + client: 'cimd', + revoke: { + page: 'https://sentry.io/settings/account/authorizations/', + endpoint: 'https://mcp.sentry.dev/oauth/token', + }, + }); + expect(capture.server).toBe(entry.resource); + expect(capture.scopeRequested.split(' ').sort()).toEqual([...entry.scopes].sort()); + const excluded = new Set(['analyze_issue_with_seer', 'search_sentry_tools', 'execute_sentry_tool']); + expect(entry.tools.map((tool) => tool.upstreamName)).toEqual( + capture.tools.filter((tool) => !excluded.has(tool.name)).map((tool) => tool.name), + ); + expect(capture.tools).toHaveLength(15); + const measured = new Map(capture.tools.map((tool) => [tool.name, tool])); + const resource = entry.tools.find((tool) => tool.name === 'get_sentry_resource'); + expect(resource.inputSchema.properties.url).toBeUndefined(); + expect(measured.get('get_sentry_resource').inputSchema.properties.url).toBeDefined(); + expect(resource.inputSchema.properties.resourceType.enum).toEqual([ + 'issue', 'event', 'trace', 'ai_conversation', 'replay', 'monitor', 'snapshot', 'dashboard', + ]); + expect(resource.description).toContain('by type and ID'); + expect(assessEntryTools(entry, capture.tools).find((tool) => tool.name === 'get_sentry_resource')) + .toMatchObject({ verdict: 'ok', detail: expect.stringContaining('compatibly') }); + expect(entry.tools.find((tool) => tool.name === 'update_issue').irreversible).toBe(true); + for (const tool of entry.tools) { + const found = measured.get(tool.upstreamName); + expect(found).toBeDefined(); + if (tool.name !== 'get_sentry_resource') { + expect(canonicalJson(tool.inputSchema)).toBe(canonicalJson(found.inputSchema)); + } + expect(tool.annotations?.readOnlyHint).toBe(found.annotations.readOnlyHint); + expect(tool.annotations?.destructiveHint).toBe(found.annotations.destructiveHint); + expect(tool.class).toBe(tool.name === 'update_issue' ? 'write' : 'read'); + } + }); + test('the Linear pin is the measured list minus caller-chosen fetch paths', () => { // The latest capture `connector-ops` took (2026-10-14) is committed beside // this suite, so the pin's schemas are compared with the measurement rather diff --git a/backend/integrations/hostedMcp/entries.ts b/backend/integrations/hostedMcp/entries.ts index 8d70c2f40d..805fc64612 100644 --- a/backend/integrations/hostedMcp/entries.ts +++ b/backend/integrations/hostedMcp/entries.ts @@ -16,8 +16,11 @@ import { AIRTABLE_ENTRY } from './airtable'; import { ATLASSIAN_ENTRY } from './atlassian'; import { GOOGLE_CALENDAR_ENTRY } from './googleCalendar'; import { LINEAR_ENTRY } from './linear'; +import { SENTRY_ENTRY } from './sentry'; -export const HOSTED_MCP_ENTRIES: HostedMcpEntry[] = [LINEAR_ENTRY, GOOGLE_CALENDAR_ENTRY, ATLASSIAN_ENTRY, AIRTABLE_ENTRY]; +export const HOSTED_MCP_ENTRIES: HostedMcpEntry[] = [ + LINEAR_ENTRY, GOOGLE_CALENDAR_ENTRY, ATLASSIAN_ENTRY, AIRTABLE_ENTRY, SENTRY_ENTRY, +]; /** * Seven entry-local defects that would otherwise be silent, checked at module diff --git a/backend/integrations/hostedMcp/sentry.ts b/backend/integrations/hostedMcp/sentry.ts new file mode 100644 index 0000000000..3c284b5847 --- /dev/null +++ b/backend/integrations/hostedMcp/sentry.ts @@ -0,0 +1,1030 @@ +/** + * Sentry hosted-MCP entry (TASK-241). The OAuth proxy requests its complete + * five-scope set, including write scopes; every pinned writer must therefore + * have class `write` so the broker parks it for the credential owner's approval. + * + * Resource and authorization-server metadata were fetched from mcp.sentry.dev + * on 2026-10-19. The tool pin comes from connector-ops's consenting tools/list + * capture at 2026-10-19T07:02:33Z. The get_sentry_resource pin deliberately + * omits the vendor's optional `url`: that route can reach types outside the + * pinned enum, so its live schema always reports a compatible addition in + * the assessment log. The by-name dispatcher and its catalogue are excluded + * because they can reach operations outside this reviewed pin. Seer analysis is + * excluded because it starts a vendor AI workflow. + */ +import type { HostedMcpEntry } from '../../services/hostedMcpEntryService'; + +export const SENTRY_ENTRY: HostedMcpEntry = { + id: 'sentry', + title: 'Sentry', + description: 'Errors, projects and issue triage in your Sentry organization.', + resource: 'https://mcp.sentry.dev/mcp', + issuer: 'https://mcp.sentry.dev', + client: 'cimd', + scopes: ['org:read', 'project:write', 'team:write', 'event:write', 'alerts:write'], + revoke: { + page: 'https://sentry.io/settings/account/authorizations/', + endpoint: 'https://mcp.sentry.dev/oauth/token', + }, + tools: [ + { + name: 'find_organizations', + upstreamName: 'find_organizations', + description: "Find Sentry organizations available to this account.", + class: 'read', + inputSchema: { + type: "object", + properties: { + query: { + default: null, + anyOf: [ + { + type: "string", + description: "Search query to filter results by name or slug. Use this to narrow down results when there are many items.", + }, + { + type: "null", + }, + ], + }, + cursor: { + default: null, + anyOf: [ + { + type: "string", + description: "Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.", + }, + { + type: "null", + }, + ], + }, + }, + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'find_projects', + upstreamName: 'find_projects', + description: "Find projects in a Sentry organization.", + class: 'read', + 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: { + default: null, + 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", + }, + ], + }, + query: { + default: null, + anyOf: [ + { + type: "string", + description: "Search query to filter results by name or slug. Use this to narrow down results when there are many items.", + }, + { + type: "null", + }, + ], + }, + cursor: { + default: null, + anyOf: [ + { + type: "string", + description: "Pagination cursor from a previous call's nextCursor. Reuse it with the same filters and scope to fetch the next page.", + }, + { + type: "null", + }, + ], + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'update_issue', + upstreamName: 'update_issue', + description: "Change a Sentry issue status or assignee, with an optional reason.", + class: 'write', + irreversible: true, + 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: { + default: null, + 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", + }, + ], + }, + 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", + }, + status: { + type: "string", + enum: [ + "resolved", + "resolvedInNextRelease", + "unresolved", + "ignored", + ], + description: "The new status for the issue. Valid values are 'resolved', 'resolvedInNextRelease', 'unresolved', and 'ignored'.", + }, + assignedTo: { + anyOf: [ + { + type: "string", + minLength: 1, + }, + { + type: "null", + }, + ], + description: "The assignee in format 'user:ID' or 'team:ID_OR_SLUG' where ID is numeric. Pass null to unassign the issue. Example: 'user:123456', 'team:789', or 'team:my-team-slug'. Use `execute_sentry_tool(name='whoami', arguments={})` to find your user ID.", + }, + ignoreMode: { + type: "string", + enum: [ + "untilEscalating", + "forever", + "forDuration", + "untilOccurrenceCount", + "untilUserCount", + ], + description: "How ignored issues should behave. Use 'untilEscalating' to match the Sentry UI default, 'forever' for a permanent ignore, 'forDuration' with ignoreDurationMinutes, 'untilOccurrenceCount' with ignoreCount and optional ignoreWindowMinutes, or 'untilUserCount' with ignoreUserCount and optional ignoreUserWindowMinutes.", + }, + ignoreDurationMinutes: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + description: "How many minutes to ignore the issue when ignoreMode is 'forDuration'.", + }, + ignoreCount: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + description: "How many times the issue must occur before it stops being ignored when ignoreMode is 'untilOccurrenceCount'.", + }, + ignoreWindowMinutes: { + type: "integer", + exclusiveMinimum: 0, + maximum: 10080, + description: "Optional time window in minutes for ignoreCount. If omitted, Sentry counts all future occurrences.", + }, + ignoreUserCount: { + type: "integer", + exclusiveMinimum: 0, + maximum: 9007199254740991, + description: "How many users must be affected before the issue stops being ignored when ignoreMode is 'untilUserCount'.", + }, + ignoreUserWindowMinutes: { + type: "integer", + exclusiveMinimum: 0, + maximum: 10080, + description: "Optional time window in minutes for ignoreUserCount. If omitted, Sentry counts all future affected users.", + }, + reason: { + type: "string", + minLength: 1, + description: "Optional reason for taking this action. When provided, it will be posted as a comment on the issue's activity feed.", + }, + }, + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: false, destructiveHint: true }, + }, + { + name: 'search_errors', + upstreamName: 'search_errors', + description: "Search error events and stack traces in Sentry.", + class: 'read', + 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.", + }, + query: { + description: "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + type: "string", + }, + fields: { + description: "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + anyOf: [ + { + type: "array", + items: { + type: "string", + }, + }, + { + type: "null", + }, + ], + }, + sort: { + description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + period: { + type: "string", + pattern: "^\\d+[hdw]$", + description: "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of results to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'search_logs', + upstreamName: 'search_logs', + description: "Search application log entries in Sentry.", + class: 'read', + 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.", + }, + query: { + description: "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + type: "string", + }, + fields: { + description: "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + anyOf: [ + { + type: "array", + items: { + type: "string", + }, + }, + { + type: "null", + }, + ], + }, + sort: { + description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + period: { + type: "string", + pattern: "^\\d+[hdw]$", + description: "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of results to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'search_traces', + upstreamName: 'search_traces', + description: "Search spans and traces in Sentry.", + class: 'read', + 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.", + }, + query: { + description: "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + type: "string", + }, + fields: { + description: "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + anyOf: [ + { + type: "array", + items: { + type: "string", + }, + }, + { + type: "null", + }, + ], + }, + sort: { + description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + period: { + type: "string", + pattern: "^\\d+[hdw]$", + description: "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of results to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'search_metrics', + upstreamName: 'search_metrics', + description: "Search Sentry metrics and trends.", + class: 'read', + 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.", + }, + query: { + description: "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + type: "string", + }, + fields: { + description: "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + anyOf: [ + { + type: "array", + items: { + type: "string", + }, + }, + { + type: "null", + }, + ], + }, + sort: { + description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + period: { + type: "string", + pattern: "^\\d+[hdw]$", + description: "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of results to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'search_profiles', + upstreamName: 'search_profiles', + description: "Search Sentry profiling results.", + class: 'read', + 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.", + }, + query: { + description: "What to find, in natural language (preferred) or Sentry search syntax. Include environment, release, or other filters here.", + type: "string", + }, + fields: { + description: "Fields to return. If not specified, uses sensible defaults. Include aggregate functions like count(), avg() for statistics.", + anyOf: [ + { + type: "array", + items: { + type: "string", + }, + }, + { + type: "null", + }, + ], + }, + sort: { + description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + period: { + type: "string", + pattern: "^\\d+[hdw]$", + description: "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of results to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'search_replays', + upstreamName: 'search_replays', + description: "Search Sentry session replays.", + class: 'read', + 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.", + }, + query: { + description: "What to find, in natural language (preferred) or replay search syntax.", + type: "string", + }, + sort: { + description: "Replay sort (prefix with - for descending): -started_at (default), -count_errors, -count_rage_clicks, or -duration.", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + environment: { + description: "Optional environment filter. Use a string for one environment or an array for multiple. Omit when unused.", + anyOf: [ + { + type: "string", + minLength: 1, + }, + { + minItems: 1, + type: "array", + items: { + type: "string", + minLength: 1, + }, + }, + ], + }, + period: { + type: "string", + pattern: "^\\d+[hdw]$", + description: "Relative time window, such as `24h`, `7d`, `14d`, `30d`, or `90d`. Controls which records fall within the search window.", + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of results to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'search_issues', + upstreamName: 'search_issues', + description: "Search grouped Sentry issues.", + class: 'read', + 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.", + }, + query: { + default: "is:unresolved", + description: "Natural language or Sentry issue search query syntax.", + type: "string", + }, + sort: { + default: "date", + description: "Sort order: date (last seen), freq (frequency), new (first seen), user (user count), recommended (Sentry's prioritized ranking)", + type: "string", + enum: [ + "date", + "freq", + "new", + "user", + "recommended", + ], + }, + projectSlugOrId: { + default: null, + description: "The project's slug or numeric ID (optional)", + anyOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + regionUrl: { + default: null, + 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", + }, + ], + }, + limit: { + default: 10, + description: "Maximum number of issues to return (1-100)", + type: "number", + minimum: 1, + maximum: 100, + }, + period: { + default: "30d", + description: "Time window for issue search results. Controls which issues are returned based on when they had activity. Default 30d is a balance between coverage and query performance; use 24h for very recent issues or 90d for broader historical searches.", + type: "string", + enum: [ + "24h", + "7d", + "14d", + "30d", + "90d", + ], + }, + includeExplanation: { + default: false, + description: "Include explanation of how the query was translated or repaired", + type: "boolean", + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'find_dropped_events', + upstreamName: 'find_dropped_events', + description: "Find events dropped before Sentry stored them.", + class: 'read', + 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: { + default: null, + 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", + }, + ], + }, + dataset: { + default: "spans", + type: "string", + enum: [ + "spans", + "logs", + "metrics", + "errors", + ], + description: "Which data type to report drops for.", + }, + projectSlug: { + default: null, + anyOf: [ + { + type: "string", + description: "The project's slug. You can find a list of existing projects in an organization using the `find_projects()` tool.", + }, + { + type: "null", + }, + ], + }, + statsPeriod: { + default: null, + anyOf: [ + { + type: "string", + description: "Relative time range, e.g. '24h', '7d', '30d'. Mutually exclusive with start/end.", + }, + { + type: "null", + }, + ], + }, + start: { + default: null, + anyOf: [ + { + type: "string", + description: "Absolute start (ISO 8601). Must be paired with end.", + }, + { + type: "null", + }, + ], + }, + end: { + default: null, + anyOf: [ + { + type: "string", + description: "Absolute end (ISO 8601). Must be paired with start.", + }, + { + type: "null", + }, + ], + }, + interval: { + default: null, + anyOf: [ + { + type: "string", + description: "Bucket size, e.g. '1h', '1d'. Omit to let Sentry pick for the range.", + }, + { + type: "null", + }, + ], + }, + outcome: { + default: null, + anyOf: [ + { + type: "string", + enum: [ + "rate_limited", + "filtered", + "invalid", + "abuse", + "client_discard", + "cardinality_limited", + ], + description: "Scope the dropped side to one top-level drop classification. Accepted volume is still returned in full.", + }, + { + type: "null", + }, + ], + }, + reason: { + default: null, + anyOf: [ + { + type: "string", + description: "Scope the dropped side to one reason (sub-classification within an outcome, e.g. 'spike_protection'). Combine with `outcome`.", + }, + { + type: "null", + }, + ], + }, + }, + required: [ + "organizationSlug", + ], + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + { + name: 'get_sentry_resource', + upstreamName: 'get_sentry_resource', + description: 'Read a Sentry issue, event, trace, AI conversation, replay, ' + + 'monitor, snapshot, or dashboard by type and ID.', + class: 'read', + inputSchema: { + type: "object", + properties: { + resourceType: { + description: "Resource type. With a URL, can override a span-focused trace URL with `trace`. Use `monitor` with a monitor slug only when inspect monitor tools are available, `snapshot` with a snapshot artifact ID, or `dashboard` with a dashboard ID or exact title when inspect dashboard tools are available.", + type: "string", + enum: [ + "issue", + "event", + "trace", + "ai_conversation", + "replay", + "monitor", + "snapshot", + "dashboard", + ], + }, + resourceId: { + description: "Resource identifier: issue shortId (e.g., 'PROJECT-123'), event ID, trace ID, agent conversation ID, replay ID, monitor slug when inspect monitor tools are available, dashboard ID or title when inspect dashboard tools are available, or snapshot artifact ID. Required when not using a URL.", + type: "string", + }, + 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.", + }, + }, + $schema: "https://json-schema.org/draft/2020-12/schema", + }, + annotations: { readOnlyHint: true, destructiveHint: false }, + }, + ], +}; diff --git a/frontend/src/v2/__tests__/platformGlyphCatalogue.test.tsx b/frontend/src/v2/__tests__/platformGlyphCatalogue.test.tsx index f9e59add45..3ce42e77b6 100644 --- a/frontend/src/v2/__tests__/platformGlyphCatalogue.test.tsx +++ b/frontend/src/v2/__tests__/platformGlyphCatalogue.test.tsx @@ -14,6 +14,7 @@ jest.mock('../../../../backend/models/Installable', () => ({})); // Simple Icons marks (CC0), pinned so a re-draw is a visible diff. const ATLASSIAN_PATH = 'M7.12 11.084a.683.683 0 00-1.16.126L.075 22.974a.703.703 0 00.63 1.018h8.19a.678.678 0 00.63-.39c1.767-3.65.696-9.203-2.406-12.52zM11.434.386a15.515 15.515 0 00-.906 15.317l3.95 7.9a.703.703 0 00.628.388h8.19a.703.703 0 00.63-1.017L12.63.38a.664.664 0 00-1.196.006z'; const AIRTABLE_PATH = 'M11.992 1.966c-.434 0-.87.086-1.28.257L1.779 5.917c-.503.208-.49.908.012 1.116l8.982 3.558a3.266 3.266 0 0 0 2.454 0l8.982-3.558c.503-.196.503-.908.012-1.116l-8.957-3.694a3.255 3.255 0 0 0-1.272-.257zM23.4 8.056a.589.589 0 0 0-.222.045l-10.012 3.877a.612.612 0 0 0-.38.564v8.896a.6.6 0 0 0 .821.552L23.62 18.1a.583.583 0 0 0 .38-.551V8.653a.6.6 0 0 0-.6-.596zM.676 8.095a.644.644 0 0 0-.48.19C.086 8.396 0 8.53 0 8.69v8.355c0 .442.515.737.908.54l6.27-3.006.307-.147 2.969-1.436c.466-.22.43-.908-.061-1.092L.883 8.138a.57.57 0 0 0-.207-.044z'; +const SENTRY_PATH = 'M13.91 2.505c-.873-1.448-2.972-1.448-3.844 0L6.904 7.92a15.478 15.478 0 0 1 8.53 12.811h-2.221A13.301 13.301 0 0 0 5.784 9.814l-2.926 5.06a7.65 7.65 0 0 1 4.435 5.848H2.194a.365.365 0 0 1-.298-.534l1.413-2.402a5.16 5.16 0 0 0-1.614-.913L.296 19.275a2.182 2.182 0 0 0 .812 2.999 2.24 2.24 0 0 0 1.086.288h6.983a9.322 9.322 0 0 0-3.845-8.318l1.11-1.922a11.47 11.47 0 0 1 4.95 10.24h5.915a17.242 17.242 0 0 0-7.885-15.28l2.244-3.845a.37.37 0 0 1 .504-.13c.255.14 9.75 16.708 9.928 16.9a.365.365 0 0 1-.327.543h-2.287c.029.612.029 1.223 0 1.831h2.297a2.206 2.206 0 0 0 1.922-3.31z'; const LINEAR_PATH = 'M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z'; describe('platform glyph catalogue contract', () => { @@ -36,6 +37,7 @@ describe('platform glyph catalogue contract', () => { it.each([ ['atlassian', ATLASSIAN_PATH], ['airtable', AIRTABLE_PATH], + ['sentry', SENTRY_PATH], ])('renders %s with its Simple Icons mark, in ink, instead of the unknown-platform dot', (type, path) => { const { container } = render(React.createElement(PlatformGlyph, { type })); const svg = container.querySelector('svg'); diff --git a/frontend/src/v2/icons/platforms/index.tsx b/frontend/src/v2/icons/platforms/index.tsx index a0bfeefcca..719ccc5e69 100644 --- a/frontend/src/v2/icons/platforms/index.tsx +++ b/frontend/src/v2/icons/platforms/index.tsx @@ -71,6 +71,11 @@ const Airtable = () => ( ); +// Simple Icons Sentry mark, CC0 1.0. Monochrome, inheriting the row's ink. +const Sentry = () => ( + +); + // Original Commonly drawing; no third-party glyph or licensed asset. This is // a simplified calendar symbol, not Google's trademarked brand mark. const GoogleCalendar = () => ( @@ -86,6 +91,7 @@ export const PLATFORM_GLYPHS: Record = { 'google-calendar': GoogleCalendar, atlassian: Atlassian, airtable: Airtable, + sentry: Sentry, telegram: Telegram, slack: Slack, discord: Discord,