diff --git a/backend/__tests__/fixtures/hostedMcp/sentry-tools-list-2026-11-01.json b/backend/__tests__/fixtures/hostedMcp/sentry-tools-list-2026-11-01.json new file mode 100644 index 000000000..f165e870f --- /dev/null +++ b/backend/__tests__/fixtures/hostedMcp/sentry-tools-list-2026-11-01.json @@ -0,0 +1,989 @@ +{ + "server": "https://mcp.sentry.dev/mcp", + "capturedAt": "2026-11-01T22:13:49.109Z", + "method": "tools/list through listHostedMcpTools (the broker drift check's client, guarded fetch) with the consenting Lily-owned row's credential, run from ops-runner; tool metadata only", + "toolCount": 12, + "consentedSkills": "inferred, not recorded: the list leaves out update_issue (Sentry's Triage skill) and analyze_issue_with_seer (its Seer skill), so the row was consented without either. Sentry grants all five scopes whatever the skills, so the scope cannot show them. Per getsentry/sentry-mcp main on 2026-11-01: packages/mcp-core/src/tools/surfaces.ts and packages/mcp-core/src/skills.ts", + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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" + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "organizations", + "hasMore", + "nextCursor" + ], + "additionalProperties": false + } + }, + { + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": true + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "slug": { + "type": "string" + } + }, + "required": [ + "slug" + ], + "additionalProperties": false + } + }, + "hasMore": { + "type": "boolean" + }, + "nextCursor": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "projects", + "hasMore", + "nextCursor" + ], + "additionalProperties": false + } + }, + { + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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.", + "type": [ + "string", + "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+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, 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" + ] + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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.", + "type": [ + "string", + "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+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, 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" + ] + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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.", + "type": [ + "string", + "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+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, 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" + ] + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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.", + "type": [ + "string", + "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+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, 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" + ] + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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.", + "type": [ + "string", + "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+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, 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" + ] + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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.", + "type": [ + "string", + "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+[mhdw]$", + "description": "Relative time window, such as `30m`, `1h`, `24h`, `7d`, 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" + ] + }, + "annotations": { + "readOnlyHint": true, + "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\nNo AND/OR or parentheses: spaces mean AND. For OR on one field use field:[a,b]; for several statuses use status:[resolved,ignored] or substatus:[regressed,escalating].\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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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)", + "type": [ + "string", + "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 1h or 24h for very recent issues or 90d for broader historical searches.", + "type": "string", + "enum": [ + "1h", + "24h", + "7d", + "14d", + "30d", + "90d" + ] + }, + "includeExplanation": { + "default": false, + "description": "Include explanation of how the query was translated or repaired", + "type": "boolean" + } + }, + "required": [ + "organizationSlug" + ] + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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." + } + } + }, + "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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "openWorldHint": false + }, + "outputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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" + ], + "additionalProperties": 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", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "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" + ] + }, + "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 c99299f6e..0428b8ff0 100644 --- a/backend/__tests__/unit/services/hostedMcpEntry.test.js +++ b/backend/__tests__/unit/services/hostedMcpEntry.test.js @@ -25,7 +25,11 @@ const LINEAR_CAPTURE = require('../../fixtures/hostedMcp/linear-tools-list-2026- const ATLASSIAN_CAPTURE = require('../../fixtures/hostedMcp/atlassian-tools-list-2026-10-19.json'); const AIRTABLE_CAPTURE = require('../../fixtures/hostedMcp/airtable-tools-list-2026-10-09.json'); const AIRTABLE_AS_CAPTURE = require('../../fixtures/hostedMcp/airtable-authorization-server-2026-10-28.json'); -const SENTRY_CAPTURE = require('../../fixtures/hostedMcp/sentry-tools-list-2026-10-19.json'); +// Two Sentry captures, from rows consented with different skills. The 10-19 row +// had Triage, so its list carries update_issue. The 11-01 row has neither Triage +// nor Seer, and its list carries the seven search reads as Sentry now spells them. +const SENTRY_TRIAGE_CAPTURE = require('../../fixtures/hostedMcp/sentry-tools-list-2026-10-19.json'); +const SENTRY_CAPTURE = require('../../fixtures/hostedMcp/sentry-tools-list-2026-11-01.json'); const NOTION_CAPTURE = require('../../fixtures/hostedMcp/notion-tools-list-2026-10-19.json'); const GITLAB_CAPTURE = require('../../fixtures/hostedMcp/gitlab-tools-list-2026-10-19.json'); const SUPABASE_CAPTURE = require('../../fixtures/hostedMcp/supabase-tools-list-2026-10-19.json'); @@ -795,7 +799,7 @@ describe('the shipped catalogue cannot land half-wired', () => { expect([...entry.scopes].every((scope) => metadata.scopes_supported.includes(scope))).toBe(true); }); - test('the Sentry pin restricts resource lookup to type and ID and parks its destructive writer', () => { + test('the Sentry pin follows the 11-01 list, keeps the Triage writer, and restricts resource lookup to type and ID', () => { const entry = findHostedMcpEntry(HOSTED_MCP_ENTRIES, 'sentry'); const capture = SENTRY_CAPTURE; expect(entry).toMatchObject({ @@ -808,13 +812,20 @@ describe('the shipped catalogue cannot land half-wired', () => { }, }); expect(capture.server).toBe(entry.resource); - expect(capture.scopeRequested.split(' ').sort()).toEqual([...entry.scopes].sort()); + expect(SENTRY_TRIAGE_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( + // update_issue needs the Triage skill, so only the 10-19 list carries it. + expect(entry.tools.map((tool) => tool.upstreamName).filter((name) => name !== 'update_issue')).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])); + expect(capture.tools).toHaveLength(12); + expect(capture.tools.some((tool) => tool.name === 'update_issue')).toBe(false); + // find_dropped_events is no longer top-level for any row, so it is not pinned. + expect(entry.tools.some((tool) => tool.name === 'find_dropped_events')).toBe(false); + const measured = new Map([ + ...SENTRY_TRIAGE_CAPTURE.tools.filter((tool) => tool.name === 'update_issue').map((tool) => [tool.name, tool]), + ...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(); @@ -825,6 +836,9 @@ describe('the shipped catalogue cannot land half-wired', () => { 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); + // On the 11-01 row every read assesses ok; the writer is unavailable to it. + expect(assessEntryTools(entry, capture.tools).filter((tool) => tool.verdict !== 'ok')) + .toEqual([expect.objectContaining({ name: 'update_issue', verdict: 'tool_unavailable' })]); for (const tool of entry.tools) { const found = measured.get(tool.upstreamName); expect(found).toBeDefined(); diff --git a/backend/integrations/hostedMcp/sentry.ts b/backend/integrations/hostedMcp/sentry.ts index 3c284b584..88bdeb4c1 100644 --- a/backend/integrations/hostedMcp/sentry.ts +++ b/backend/integrations/hostedMcp/sentry.ts @@ -2,14 +2,23 @@ * 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. + * The scope set is the proxy's, not ours to narrow + * (docs/plans/hosted-mcp-catalogue-sweep-2026-10-13.md). * * 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 + * capture at 2026-10-19T07:02:33Z, a row consented with the Triage skill. The + * seven search reads are re-pinned from a capture at 2026-11-01T22:13:49Z, + * where Sentry had respelled nullable strings, added minutes to `period` and + * added `1h` to search_issues. That row was consented without Triage or Seer, + * so its list leaves out update_issue and Seer analysis: update_issue keeps its + * 10-19 pin, and a row without Triage sees it as unavailable. + * find_dropped_events is no longer a top-level tool (it moved behind the + * dispatcher), so it is not pinned. 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 + * 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'; @@ -234,6 +243,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -259,13 +269,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, sort: { description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, projectSlug: { @@ -282,8 +288,8 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + pattern: "^\\d+[mhdw]$", + description: "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", }, regionUrl: { default: null, @@ -313,7 +319,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { required: [ "organizationSlug", ], - $schema: "https://json-schema.org/draft/2020-12/schema", }, annotations: { readOnlyHint: true, destructiveHint: false }, }, @@ -324,6 +329,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -349,13 +355,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, sort: { description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, projectSlug: { @@ -372,8 +374,8 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + pattern: "^\\d+[mhdw]$", + description: "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", }, regionUrl: { default: null, @@ -403,7 +405,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { required: [ "organizationSlug", ], - $schema: "https://json-schema.org/draft/2020-12/schema", }, annotations: { readOnlyHint: true, destructiveHint: false }, }, @@ -414,6 +415,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -439,13 +441,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, sort: { description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, projectSlug: { @@ -462,8 +460,8 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + pattern: "^\\d+[mhdw]$", + description: "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", }, regionUrl: { default: null, @@ -493,7 +491,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { required: [ "organizationSlug", ], - $schema: "https://json-schema.org/draft/2020-12/schema", }, annotations: { readOnlyHint: true, destructiveHint: false }, }, @@ -504,6 +501,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -529,13 +527,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, sort: { description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, projectSlug: { @@ -552,8 +546,8 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + pattern: "^\\d+[mhdw]$", + description: "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", }, regionUrl: { default: null, @@ -583,7 +577,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { required: [ "organizationSlug", ], - $schema: "https://json-schema.org/draft/2020-12/schema", }, annotations: { readOnlyHint: true, destructiveHint: false }, }, @@ -594,6 +587,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -619,13 +613,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, sort: { description: "Sort field (prefix with - for descending). Defaults to -timestamp. Use -count() for aggregations.", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, projectSlug: { @@ -642,8 +632,8 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + pattern: "^\\d+[mhdw]$", + description: "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", }, regionUrl: { default: null, @@ -673,7 +663,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { required: [ "organizationSlug", ], - $schema: "https://json-schema.org/draft/2020-12/schema", }, annotations: { readOnlyHint: true, destructiveHint: false }, }, @@ -684,6 +673,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -695,13 +685,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, sort: { description: "Replay sort (prefix with - for descending): -started_at (default), -count_errors, -count_rage_clicks, or -duration.", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, projectSlug: { @@ -735,8 +721,8 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + pattern: "^\\d+[mhdw]$", + description: "Relative time window, such as `30m`, `1h`, `24h`, `7d`, or `90d`. Controls which records fall within the search window.", }, regionUrl: { default: null, @@ -766,7 +752,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { required: [ "organizationSlug", ], - $schema: "https://json-schema.org/draft/2020-12/schema", }, annotations: { readOnlyHint: true, destructiveHint: false }, }, @@ -777,6 +762,7 @@ export const SENTRY_ENTRY: HostedMcpEntry = { class: 'read', inputSchema: { type: "object", + $schema: "https://json-schema.org/draft/2020-12/schema", properties: { organizationSlug: { type: "string", @@ -802,13 +788,9 @@ export const SENTRY_ENTRY: HostedMcpEntry = { projectSlugOrId: { default: null, description: "The project's slug or numeric ID (optional)", - anyOf: [ - { - type: "string", - }, - { - type: "null", - }, + type: [ + "string", + "null", ], }, regionUrl: { @@ -832,9 +814,10 @@ export const SENTRY_ENTRY: HostedMcpEntry = { }, 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.", + 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 1h or 24h for very recent issues or 90d for broader historical searches.", type: "string", enum: [ + "1h", "24h", "7d", "14d", @@ -851,142 +834,6 @@ export const SENTRY_ENTRY: HostedMcpEntry = { 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 }, }, diff --git a/docs/plans/integration-readiness-matrix.md b/docs/plans/integration-readiness-matrix.md index db8b1a994..40b9156a6 100644 --- a/docs/plans/integration-readiness-matrix.md +++ b/docs/plans/integration-readiness-matrix.md @@ -57,7 +57,7 @@ The matrix below is the definition of "ready". A row is ready when every cell is | Linear (hosted MCP) | yes | **Fresh-account walk 2026-10-15 on `cfe91c21`, green** (connector-ops 77850 and 77962; the build is Deploy Dev's last run before the walk): a new commonly.me account and a new Linear workspace, neither connected before. **C1 green**: connected through the Connectors page. Linear's consent read "Read, Identity", the authorize request asked for `read openid`, and the token response granted `read openid` (`config.grantedScope`). **C3 green in an eligible pod**: a read grant of the 36 pinned tools, made through the grant panel, which offers only Read, in the account's own My Workspace. That pod is private and invite-only, with no bridge and Scout as its only agent, so it passes §12's pod rule in `hosted-mcp-connection-scope.md` (#2211). Scout listed the workspace's one team through the grant, the trail records the call ok, and Scout quoted #2206's untrusted-content marker ahead of the data. #2211 deployed after the walk, and the same call was ok again after it, at 16:22:37Z. The earlier C3 and C4 walks ran in the C4 run pod, which fails that rule: since `ff909fb3` the deployed rule returns `pod_joinable` for both of its grants. Partly walked live 2026-10-01 (the owner's account, Chrome). Before the fresh-account walk, C1 was open: the owner's operator-led connect succeeded on `cf9b6abf`, through CIMD with scope `read openid`, and the row records its client and the Linear subject. That didn't prove a stranger connects unaided, and no stranger had tried. **C2 green at 1440, the rest unwalked**: in the fresh-account walk the grant panel offered only Read. Earlier, on `cf9b6abf` the grant rendered as GitHub, because the catalogue projected 0 Linear tools. #2068 (`207c68d5`) fixed it, and on the deployed `e370fe9b` (its descendant) the row reads Linear, with its mark and the read eye, at 1200. 390, a failure reaching Activity, and revoke reachability are unwalked. Earlier, C3 was green in the C4 run pod: Scout listed the team and three issues through `grant_c6af31b8`; the trail showed the calls ok, with the owner as credential owner. **C4 refused by design** from #2211 (the C4 column), and c4-smoke is now offered no Linear tool. Before that it was green (2026-10-09): c4-smoke, a public local claude seat confined by Seatbelt, called `linear.list_teams` through its broker on `grant_e3a1507b` at 06:12:57Z, and the trail shows it ok. Its daemon started at 06:11:59Z, after global cli 0.1.84 (npm latest) was installed at 06:11:41Z, so it runs the current build. An earlier run on 2026-09-27 didn't count, because that daemon predated the cli it should have been running. The same spawn showed that TASK-228's headers helper works under the public Seatbelt profile. A by-name scan of the spawn's claude process found `COMMONLY_TOKEN_FILE` set and `COMMONLY_AGENT_TOKEN` absent. The rest of #2067's consumer proof is unmeasured: the child processes' environments, and a credential rotation followed by a 401 retry succeeding. **C8 open**: in a pod with no grant, Scout is offered no `linear.*` tools. That's confinement by projection. An attempted out-of-pod call, refused and recorded in the trail, hasn't been made. **C9 open**, now only for whether a revoke takes effect at Linear. Linear's revoke is page-only. The fresh-account walk reached the page: `https://linear.app/settings/account/security`, whose Authorized applications list shows the connection as "MCP · Read · Revoke". #2230 points the entry's page there. Before #2230 the entry named `https://linear.app/settings/security`, which is Linear's workspace admin page. Before that walk, every linear.app settings URL redirected to the old workspace's unfinished welcome flow, so the page had not been reached. C5, C6, C7 and C10 are unwalked | | Google Calendar (hosted MCP) | yes | **Built, live, not offered.** #2066 (`e370fe9b`) pins the 5 read tools from Google's unauthenticated `tools/list`, with a pre-registered client and `openid` for the account subject. "Not offered" is a **config fact, not a code fact**. The client values arrived on 2026-10-09 (#2111), and the tile was offered until 2026-10-14. OAuth worked, but every tool call failed Google's Developer Preview gate, which is **per Cloud project** (Google named the project in its error on the 2026-10-09 gate test). Since 2026-10-14 `hostedMcp.googleCalendar.enabled` is `false` in values-dev, so the instance holds no client values, the catalogue reports `available: false`, and the page shows "not enabled on this instance" with no Connect. Flip the flag once the project is enrolled and an enrolled `list_calendars` call succeeds. Waiting on the owner: a Workspace seat on the domain and Developer Preview enrollment for the connectors project. The OAuth client already exists in a project of its own (a revoke withdraws every scope a person granted the project) | | Atlassian (hosted MCP), writes in #2328 | yes (writes since #2328) | **Deployed, hidden on commonly.me since #2530.** The live build (`19de6035d`, 2026-10-24) carries every change below. `hostedMcp.hiddenEntries` in values-dev leaves the entry out of the catalogue and refuses every connect with `unknown_entry`, a reconnect included (`hostedMcpConnect.ts:251`). **C1 blocked on staging** (2026-10-23, `876870378`, #2536): the site's MCP-server-domain allowlist does not include the staging origin, and only a site admin can add it. The entry pins six Jira and Confluence reads. In a 2026-10-22 walk C3 was red: every call failed with "tools/list could not be verified", which TASK-263 attributed to our client never opening an MCP session. #2397 opens one for each list and call; the C3 re-check after its deploy belongs to TASK-263. #2328 adds five named writers: `update_confluence_content`, `create_jira_issue`, `edit_jira_issue`, `transition_jira_issue` and `add_or_edit_jira_issue_comment`, the last marked irreversible. Each is pinned `write`, so every write parks for the connection's owner with its exact arguments, and runs only under the definition it parked under (#2378). The entry now requests write scopes, so a row connected before #2328 holds a token that cannot write, and its owner must reconnect, which the hide refuses until it is lifted. **C4 refused by design** (the C4 column). The walk's checks are below | -| Sentry (hosted MCP), #2329 | yes (#2329) | **Deployed. Hidden on commonly.me from #2530, and offered again by the connect-and-read unhide** (still open on TASK-308: C6, and three C9 arms: the 12 `write-with-confirm` tool bounds, whether `get_sentry_resource` is offered without `url`, and a revoke taking effect). While hidden, #2649 and #2658 gave it a walk exception for one account; that key left the allowlist overlay with the unhide. **C0, C1 and C2 green on staging, each with a part #2536 did not record** (2026-10-23, `876870378`). Staging has no agent runtime, so C3, C6 and C9 were not walked there. **C3 green on commonly.me except the page comparison** (2026-10-25, `c9c0d71c1`): a hosted agent read through a pod grant, and the trail records the call ok. **C9's "cannot do more than granted" green for a read grant**: the agent was offered the 11 reads and no write tool. Whether a revoke takes effect, at Sentry included, is not walked, and neither is C6 (the commonly.me walk below). #2329 pins 11 named reads and `update_issue`, an irreversible write that parks for the connection's owner with its exact arguments. `get_sentry_resource` is pinned without the vendor's `url` argument, and the vendor's dispatcher, its catalogue and Seer are left out, as ruled in the PR thread. The client is CIMD, and the entry requests five scopes, four of them write. **C4 refused by design** (the C4 column). The walk's checks are below | +| Sentry (hosted MCP), #2329 | yes (#2329) | **Seven reads re-pinned 2026-11-01.** The Lily-owned row's tools/list that day showed Sentry had changed seven read schemas, so on commonly.me those seven, `search_issues` among them, were refused `tool_drift`. `find_dropped_events` is no longer a top-level tool and is unpinned, so the pin is now 10 reads and `update_issue`. That row was consented without the Triage skill, so its list leaves out `update_issue`, which keeps its 10-19 pin. C6 and the writer's C9 arms need a Triage-consented row, and on it `update_issue` may assess `tool_drift` until it is re-pinned from that row's list. **Deployed. Hidden on commonly.me from #2530, and offered again by the connect-and-read unhide** (still open on TASK-308: C6, and three C9 arms: the 11 `write-with-confirm` tool bounds, whether `get_sentry_resource` is offered without `url`, and a revoke taking effect). While hidden, #2649 and #2658 gave it a walk exception for one account; that key left the allowlist overlay with the unhide. **C0, C1 and C2 green on staging, each with a part #2536 did not record** (2026-10-23, `876870378`). Staging has no agent runtime, so C3, C6 and C9 were not walked there. **C3 green on commonly.me except the page comparison** (2026-10-25, `c9c0d71c1`): a hosted agent read through a pod grant, and the trail records the call ok. **C9's "cannot do more than granted" green for a read grant**: the agent was offered the 11 reads then pinned and no write tool. Whether a revoke takes effect, at Sentry included, is not walked, and neither is C6 (the commonly.me walk below). #2329 pinned 11 named reads (10 since the 11-01 re-pin) and `update_issue`, an irreversible write that parks for the connection's owner with its exact arguments. `get_sentry_resource` is pinned without the vendor's `url` argument, and the vendor's dispatcher, its catalogue and Seer are left out, as ruled in the PR thread. The client is CIMD, and the entry requests five scopes, four of them write. **C4 refused by design** (the C4 column). The walk's checks are below | | next app | no | Open decision; see below | ## Staging connector walk — 2026-10-23 on `876870378` @@ -92,9 +92,9 @@ Evidence: TASK-485's notes on the board record the grant, the agent's call, the | cell | result | |---|---| | C3 | **green except the page comparison.** `grant_d8b399a9` is a `read` pod grant of the 11 reads, with audience `walk-agent` and a two-hour expiry. Woken by a message in the pod, the agent called `find_organizations`, and its turn reports the organization returned. The trail, the broker's call log, shows that one call, `sentry.find_organizations`, outcome ok. Unverified: whether the answer matches Sentry's own pages, which TASK-485 does not record | -| C9, cannot do more than granted | **green for a read grant.** The agent's turn listed exactly the 11 reads as its `sentry_*` tools and reported no create, update or resolve tool. So neither `update_issue` nor the vendor's dispatcher, its catalogue or Seer was offered. Unverified: the 12 tools under `write-with-confirm`, and whether `get_sentry_resource` was offered without `url` | +| C9, cannot do more than granted | **green for a read grant.** The agent's turn listed exactly the 11 reads then pinned as its `sentry_*` tools and reported no create, update or resolve tool. So neither `update_issue` nor the vendor's dispatcher, its catalogue or Seer was offered. Unverified: the pinned tools under `write-with-confirm` (12 then, 11 since the 11-01 re-pin), and whether `get_sentry_resource` was offered without `url` | | C9, a revoke takes effect | **open.** The grant was revoked at 04:16:08Z, and the record has no call after the revoke, so it does not show the revoke taking effect. Not walked on commonly.me: a parked `update_issue` refused with `grant_revoked` after its grant's revoke, and a disconnect's revocation at Sentry, with what Sentry's Authorized Applications page shows afterwards | -| C6 | **not walked.** It needs a `write-with-confirm` grant, with one `update_issue` approved and one declined | +| C6 | **not walked.** It needs a `write-with-confirm` grant on a Triage-consented row, with one `update_issue` approved and one declined | | finding | status | |---|---| @@ -211,7 +211,7 @@ What a walk checks for the two entries above: C1, C3, C6, C8 and C9 for Atlassia | C2 | The page shows the row connected. A failed call lands as an Activity row that names the failure. Revoke is reachable at 390 | **green on staging except the Activity row** (#2536): a read grant of the 11 reads, made in the walk pod and revoked. Unverified: the Activity row for a failed call, which needs an agent's call, and revoke at 390 | | C3 | In an eligible pod, a hosted agent completes a read through a pod grant, such as `find_projects`. The trail records it ok, and the answer matches what Sentry's own pages show | **green on commonly.me except the page comparison** (2026-10-25, `c9c0d71c1`, TASK-485): `find_organizations` through `grant_d8b399a9`, a `read` pod grant, and the trail records it ok. Unverified: whether the answer matches Sentry's own pages, which TASK-485 does not record | | C6 | Through a `write-with-confirm` grant, `update_issue` parks, and the card shows the owner its exact arguments. The owner approves one call and declines another. Only the approved one changes the issue in Sentry: it ends `ran` with a receipt in the pod, and the other ends `declined` (`approval_declined`). The seat receives each ending once as `approval.resolved` | unverified | -| C9 | Bounded: the 12 pinned tools are the only ones offered, without the dispatcher, its catalogue or Seer, and `get_sentry_resource` is offered without a `url` argument. Revocable: an `update_issue` call parked before its grant is revoked, then approved, ends `refused` (`grant_revoked`) without changing the issue. A disconnect on the Connectors page calls Sentry's revocation endpoint before deleting the row. Record what Sentry's Authorized Applications page shows afterwards: it lists the client as `mcp.sentry.dev`, and removing it there cuts every Sentry-MCP session on the account (#2536) | **Bounded: green for a read grant on commonly.me** (2026-10-25, `c9c0d71c1`, TASK-485). The agent was offered the 11 reads and no other Sentry tool. Unverified: the 12 tools under `write-with-confirm`, `get_sentry_resource` without `url`, and every revocable check. On staging the disconnect's revocation call returned 200 (#2536) | +| C9 | Bounded: the 11 pinned tools (12 before the 11-01 re-pin) are the only ones offered, without the dispatcher, its catalogue or Seer, and `get_sentry_resource` is offered without a `url` argument. Revocable: an `update_issue` call parked before its grant is revoked, then approved, ends `refused` (`grant_revoked`) without changing the issue. A disconnect on the Connectors page calls Sentry's revocation endpoint before deleting the row. Record what Sentry's Authorized Applications page shows afterwards: it lists the client as `mcp.sentry.dev`, and removing it there cuts every Sentry-MCP session on the account (#2536) | **Bounded: green for a read grant on commonly.me** (2026-10-25, `c9c0d71c1`, TASK-485). The agent was offered the 11 reads and no other Sentry tool. Unverified: the pinned tools under `write-with-confirm` (12 then, 11 since the 11-01 re-pin), `get_sentry_resource` without `url`, and every revocable check. On staging the disconnect's revocation call returned 200 (#2536) | | C4 | Refused by design until TASK-213 | refused by design | ## Cross-cutting reds