diff --git a/packages/mcp-core/src/server.test.ts b/packages/mcp-core/src/server.test.ts index d0a99a772..f6051172b 100644 --- a/packages/mcp-core/src/server.test.ts +++ b/packages/mcp-core/src/server.test.ts @@ -42,6 +42,7 @@ vi.mock("@sentry/core", () => ({ ), captureException: vi.fn(), captureMessage: vi.fn(), + getClient: vi.fn(), })); vi.mock("@sentry/core/server", () => ({ diff --git a/packages/mcp-core/src/skillDefinitions.json b/packages/mcp-core/src/skillDefinitions.json index 1f3a1d245..41beeedd0 100644 --- a/packages/mcp-core/src/skillDefinitions.json +++ b/packages/mcp-core/src/skillDefinitions.json @@ -5,7 +5,7 @@ "description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, cron monitors, uptime monitors, metric monitors, profiles, documentation, and project metadata", "defaultEnabled": true, "order": 1, - "toolCount": 47, + "toolCount": 48, "tools": [ { "name": "find_alert_rules", @@ -237,6 +237,11 @@ "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", "requiredScopes": ["event:read"] }, + { + "name": "submit_feedback_to_sentry_team", + "description": "Report friction with the Sentry MCP server to its maintainers.\n\nUse this tool when you need to:\n- Report that no Sentry MCP tool could do what the user asked\n- Report a tool that failed, returned wrong data, or was confusing\n- Suggest an improvement to the Sentry MCP tools\n\n\nsubmit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')\n\n\n\n- Call at most once per task, after you finish or give up.\n- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.\n- Do not include secrets, credentials, or personal data.\n", + "requiredScopes": [] + }, { "name": "whoami", "description": "Identify the authenticated user in Sentry.\n\nUse this tool when you need to:\n- Get the user's name and email address.", @@ -250,7 +255,7 @@ "description": "Sentry's AI debugger that helps you analyze, root cause, and fix issues", "defaultEnabled": true, "order": 2, - "toolCount": 17, + "toolCount": 18, "tools": [ { "name": "analyze_issue_with_seer", @@ -332,6 +337,11 @@ "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", "requiredScopes": ["event:read"] }, + { + "name": "submit_feedback_to_sentry_team", + "description": "Report friction with the Sentry MCP server to its maintainers.\n\nUse this tool when you need to:\n- Report that no Sentry MCP tool could do what the user asked\n- Report a tool that failed, returned wrong data, or was confusing\n- Suggest an improvement to the Sentry MCP tools\n\n\nsubmit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')\n\n\n\n- Call at most once per task, after you finish or give up.\n- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.\n- Do not include secrets, credentials, or personal data.\n", + "requiredScopes": [] + }, { "name": "whoami", "description": "Identify the authenticated user in Sentry.\n\nUse this tool when you need to:\n- Get the user's name and email address.", @@ -346,7 +356,7 @@ "defaultEnabled": false, "deprecated": true, "order": 3, - "toolCount": 6, + "toolCount": 7, "tools": [ { "name": "find_organizations", @@ -373,6 +383,11 @@ "description": "Search Sentry documentation for SDK setup, instrumentation, and configuration guidance.\n\nUse this tool when you need to:\n- Set up Sentry SDK or framework integrations (Django, Flask, Express, Next.js, etc.)\n- Configure features like performance monitoring, error sampling, or release tracking\n- Implement custom instrumentation (spans, transactions, breadcrumbs)\n- Configure data scrubbing, filtering, or sampling rules\n\nReturns snippets only. Use the Sentry tool `get_doc` to fetch full documentation content.\n\n\n```\nsearch_docs(query='Django setup configuration SENTRY_DSN', guide='python/django')\nsearch_docs(query='source maps webpack upload', guide='javascript/nextjs')\n```\n\n\n\n- Use guide parameter to filter to specific technologies (e.g., 'javascript/nextjs')\n- Include specific feature names like 'beforeSend', 'tracesSampleRate', 'SENTRY_DSN'\n", "requiredScopes": [] }, + { + "name": "submit_feedback_to_sentry_team", + "description": "Report friction with the Sentry MCP server to its maintainers.\n\nUse this tool when you need to:\n- Report that no Sentry MCP tool could do what the user asked\n- Report a tool that failed, returned wrong data, or was confusing\n- Suggest an improvement to the Sentry MCP tools\n\n\nsubmit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')\n\n\n\n- Call at most once per task, after you finish or give up.\n- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.\n- Do not include secrets, credentials, or personal data.\n", + "requiredScopes": [] + }, { "name": "whoami", "description": "Identify the authenticated user in Sentry.\n\nUse this tool when you need to:\n- Get the user's name and email address.", @@ -386,7 +401,7 @@ "description": "Resolve, assign, and update issues", "defaultEnabled": false, "order": 4, - "toolCount": 25, + "toolCount": 26, "tools": [ { "name": "add_issue_note", @@ -498,6 +513,11 @@ "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", "requiredScopes": ["event:read"] }, + { + "name": "submit_feedback_to_sentry_team", + "description": "Report friction with the Sentry MCP server to its maintainers.\n\nUse this tool when you need to:\n- Report that no Sentry MCP tool could do what the user asked\n- Report a tool that failed, returned wrong data, or was confusing\n- Suggest an improvement to the Sentry MCP tools\n\n\nsubmit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')\n\n\n\n- Call at most once per task, after you finish or give up.\n- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.\n- Do not include secrets, credentials, or personal data.\n", + "requiredScopes": [] + }, { "name": "unlink_issue", "description": "Remove an external ticket or GitHub pull request reference from a Sentry issue by URL.\nRemoves only the Sentry association. It does not delete the external ticket or the Sentry issue, or change resolution status.\nSupports native integrations and installed Sentry Apps. Repeating the request is safe: not_linked means the association is absent, whether removed by this call or already absent.\n\nunlink_issue(organizationSlug='my-org', issueId='PROJECT-123', externalIssueUrl='https://github.com/example/repo/issues/42')\n", @@ -521,7 +541,7 @@ "description": "Create and modify projects, teams, DSNs, cron monitor environments, uptime monitors, metric monitors, and alert rules", "defaultEnabled": false, "order": 5, - "toolCount": 23, + "toolCount": 24, "tools": [ { "name": "add_team_to_project", @@ -608,6 +628,11 @@ "description": "Revoke a team's access to an existing Sentry project.\n\nUse this tool when you need to:\n- Remove a team from a project\n- Revoke team access without changing project metadata\n- Check project team assignments before removing access\n\nBe careful when using this tool because it revokes project access.\n\n\nremove_team_from_project(organizationSlug='my-organization', projectSlug='my-project', teamSlug='my-team')\n\n\n\n- The team must already be assigned to the project.\n- This tool will not remove the last team assigned to a project.\n", "requiredScopes": ["project:write", "team:read", "org:read"] }, + { + "name": "submit_feedback_to_sentry_team", + "description": "Report friction with the Sentry MCP server to its maintainers.\n\nUse this tool when you need to:\n- Report that no Sentry MCP tool could do what the user asked\n- Report a tool that failed, returned wrong data, or was confusing\n- Suggest an improvement to the Sentry MCP tools\n\n\nsubmit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')\n\n\n\n- Call at most once per task, after you finish or give up.\n- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.\n- Do not include secrets, credentials, or personal data.\n", + "requiredScopes": [] + }, { "name": "update_alert_rule", "description": "Update a Sentry Alert (workflow), including notification actions and connections.\nUse get_alert_rule with kind='issue' first to inspect the complete triggers and actionFilters configuration.\nUse get_alert_options to discover notification actions, integrations, conditions, and available sources.\nOmit fields to leave them unchanged. Pass null to clear owner or environment.\ntriggers replaces the trigger conditions. actionFilters replaces ALL action groups: copy the complete configuration, retain existing IDs, and change only the intended values. Omitted groups, conditions, and actions are removed.\nFor Slack or Microsoft Teams, change config.targetDisplay to the channel name and use integrationId for the workspace or team. Sentry resolves the channel ID. Slack also accepts an explicit new targetIdentifier; a copied old ID is cleared when the name or workspace changes.\nOther actions use their provider's config and data. For Discord, PagerDuty, Opsgenie, and email, update targetIdentifier to the channel, service, team, or recipient ID; changing only its display name does not change the destination.\nUse addProjectSlugs/removeProjectSlugs to connect/disconnect a project's issue stream. Other monitors in that project remain connected. Use addDetectorIds/removeDetectorIds for individual monitors. Unmentioned connections are preserved.\nMetric Monitor detection queries and thresholds are separate operations.\nAll-project connections require Sentry's all-project feature and an API token with org:write in addition to alerts:write.\nA project-constrained session can only edit alerts affecting that project exclusively.\nRequires alerts:write; reconnect OAuth if the existing token lacks it.\n\nupdate_alert_rule(organizationSlug='my-org', ruleIdOrName='12345', status='disabled')\nupdate_alert_rule(organizationSlug='my-org', projectSlug='backend', ruleIdOrName='Notify backend team', frequencyMinutes=30)\n", diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 3165f01c0..827186518 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -7567,6 +7567,57 @@ "skills": ["inspect", "triage", "seer"], "surface": "direct" }, + { + "name": "submit_feedback_to_sentry_team", + "description": "Report friction with the Sentry MCP server to its maintainers.\n\nUse this tool when you need to:\n- Report that no Sentry MCP tool could do what the user asked\n- Report a tool that failed, returned wrong data, or was confusing\n- Suggest an improvement to the Sentry MCP tools\n\n\nsubmit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')\n\n\n\n- Call at most once per task, after you finish or give up.\n- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.\n- Do not include secrets, credentials, or personal data.\n", + "inputSchema": { + "type": "object", + "properties": { + "task": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "A short phrase naming what the user asked you to do." + }, + "kind": { + "type": "string", + "enum": ["missing", "broken", "confusing", "idea"], + "description": "missing: no tool could do it; broken: a tool failed or returned something wrong; confusing: a tool or its docs were unclear; idea: a suggestion." + }, + "impact": { + "type": "string", + "enum": ["blocked", "worked_around", "friction"], + "description": "blocked: you could not finish; worked_around: you finished another way; friction: you finished, but it was harder than it should be." + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 4096, + "description": "What you tried, including tool calls and any error, and what went wrong." + }, + "toolName": { + "description": "The Sentry MCP tool the feedback is about, if any.", + "type": "string", + "maxLength": 100 + } + }, + "required": ["task", "kind", "impact", "description"] + }, + "outputSchema": { + "type": "object", + "properties": { + "feedbackId": { + "type": "string", + "description": "ID of the feedback sent to the Sentry MCP maintainers." + } + }, + "required": ["feedbackId"], + "additionalProperties": false + }, + "requiredScopes": [], + "skills": ["inspect", "seer", "docs", "triage", "project-management"], + "surface": "catalog" + }, { "name": "unlink_issue", "description": "Remove an external ticket or GitHub pull request reference from a Sentry issue by URL.\nRemoves only the Sentry association. It does not delete the external ticket or the Sentry issue, or change resolution status.\nSupports native integrations and installed Sentry Apps. Repeating the request is safe: not_linked means the association is absent, whether removed by this call or already absent.\n\nunlink_issue(organizationSlug='my-org', issueId='PROJECT-123', externalIssueUrl='https://github.com/example/repo/issues/42')\n", diff --git a/packages/mcp-core/src/tools/catalog-runtime/availability.test.ts b/packages/mcp-core/src/tools/catalog-runtime/availability.test.ts index b627354c8..be2fc1cc5 100644 --- a/packages/mcp-core/src/tools/catalog-runtime/availability.test.ts +++ b/packages/mcp-core/src/tools/catalog-runtime/availability.test.ts @@ -1,4 +1,5 @@ -import { describe, expect, it } from "vitest"; +import { getClient } from "@sentry/core"; +import { describe, expect, it, vi } from "vitest"; import type { Skill } from "../../skills"; import { getServerContext } from "../../test-setup"; import type { ServerContext } from "../../types"; @@ -14,6 +15,11 @@ import { prepareToolParams, } from "./availability"; +vi.mock("@sentry/core", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, getClient: vi.fn(actual.getClient) }; +}); + function getProjectManagementContext( constraints: Partial = {}, ): ServerContext { @@ -74,6 +80,24 @@ describe("catalog availability", () => { ); }); + it("shows submit_feedback_to_sentry_team only when Sentry telemetry has a DSN", () => { + const context = getServerContext({ + grantedSkills: new Set(["inspect"]), + }); + + vi.mocked(getClient).mockReturnValueOnce(undefined); + expect(getSearchableToolNames(context)).not.toContain( + "submit_feedback_to_sentry_team", + ); + + vi.mocked(getClient).mockReturnValueOnce({ + getDsn: () => ({ host: "o1.ingest.sentry.io" }), + } as unknown as ReturnType); + expect(getSearchableToolNames(context)).toContain( + "submit_feedback_to_sentry_team", + ); + }); + it("hides create_project from project-scoped project-management sessions", () => { const context = getProjectManagementContext({ organizationSlug: "my-org", diff --git a/packages/mcp-core/src/tools/catalog-runtime/availability.ts b/packages/mcp-core/src/tools/catalog-runtime/availability.ts index dc9c0237c..7d3feff6c 100644 --- a/packages/mcp-core/src/tools/catalog-runtime/availability.ts +++ b/packages/mcp-core/src/tools/catalog-runtime/availability.ts @@ -1,4 +1,5 @@ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js"; +import { getClient } from "@sentry/core"; import type { ServerNotification, ServerRequest, @@ -117,6 +118,12 @@ function isHiddenByConstraints({ ); } +// submit_feedback_to_sentry_team can only deliver reports when Sentry telemetry has a DSN +// (e.g. stdio without SENTRY_DSN has none), so hide it otherwise. +function isMissingFeedbackDsn(key: string): boolean { + return key === "submit_feedback_to_sentry_team" && !getClient()?.getDsn(); +} + export function getFilteredInputSchema( tool: ToolConfig, context: ServerContext, @@ -180,6 +187,10 @@ export function getAvailableTools({ continue; } + if (isMissingFeedbackDsn(key)) { + continue; + } + availableTools.push({ key, tool, ...placement }); } diff --git a/packages/mcp-core/src/tools/catalog/index.ts b/packages/mcp-core/src/tools/catalog/index.ts index a156b888e..0b47f82f1 100644 --- a/packages/mcp-core/src/tools/catalog/index.ts +++ b/packages/mcp-core/src/tools/catalog/index.ts @@ -71,6 +71,7 @@ import searchAgentConversations from "./search-agent-conversations"; import addIssueNote from "./add-issue-note"; import onboardingStatusUpdate from "./onboarding-status-update"; import searchAttributes from "./search-attributes"; +import submitFeedbackToSentryTeam from "./submit-feedback-to-sentry-team"; import type { ToolConfig } from "../types"; const legacyGetAIConversationDetails = { @@ -180,6 +181,7 @@ const catalogTools = { search_ai_conversations: legacySearchAIConversations, add_issue_note: addIssueNote, onboarding_status_update: onboardingStatusUpdate, + submit_feedback_to_sentry_team: submitFeedbackToSentryTeam, } as const satisfies Record>; export default catalogTools; diff --git a/packages/mcp-core/src/tools/catalog/submit-feedback-to-sentry-team.test.ts b/packages/mcp-core/src/tools/catalog/submit-feedback-to-sentry-team.test.ts new file mode 100644 index 000000000..398af683b --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/submit-feedback-to-sentry-team.test.ts @@ -0,0 +1,67 @@ +import { captureFeedback } from "@sentry/core"; +import { describe, expect, it, vi } from "vitest"; +import { createTestContext } from "../../test-utils/context.js"; +import { + assertStructuredOnlyResult, + getStructuredContent, +} from "../../test-utils/structured-content.js"; +import submitFeedbackToSentryTeam from "./submit-feedback-to-sentry-team.js"; + +vi.mock("@sentry/core", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, captureFeedback: vi.fn(() => "event-id") }; +}); + +describe("submit_feedback_to_sentry_team", () => { + it("sends scrubbed feedback to Sentry", async () => { + const result = await submitFeedbackToSentryTeam.handler( + { + task: "find slow endpoints", + kind: "missing", + impact: "blocked", + description: "No tool returns p95 latency. Contact jane@example.com.", + toolName: "search_traces", + }, + createTestContext(), + ); + + assertStructuredOnlyResult(result); + expect(getStructuredContent(result)).toMatchInlineSnapshot(` + { + "feedbackId": "event-id", + } + `); + expect(vi.mocked(captureFeedback).mock.calls[0]?.[0]) + .toMatchInlineSnapshot(` + { + "message": "Task: find slow endpoints + Tool: search_traces + + No tool returns p95 latency. Contact [REDACTED_EMAIL].", + "source": "mcp_agent", + "tags": { + "feedback.impact": "blocked", + "feedback.kind": "missing", + "feedback.tool": "search_traces", + }, + } + `); + }); + + it("truncates messages to Sentry's feedback length limit", async () => { + await submitFeedbackToSentryTeam.handler( + { + task: "find slow endpoints", + kind: "broken", + impact: "friction", + description: "x".repeat(4096), + toolName: "search_traces", + }, + createTestContext(), + ); + + const message = vi.mocked(captureFeedback).mock.calls.at(-1)?.[0].message; + expect(message?.startsWith("Task: find slow endpoints\n")).toBe(true); + expect(message).toHaveLength(4096); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/submit-feedback-to-sentry-team.ts b/packages/mcp-core/src/tools/catalog/submit-feedback-to-sentry-team.ts new file mode 100644 index 000000000..a9d9021da --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/submit-feedback-to-sentry-team.ts @@ -0,0 +1,102 @@ +import { captureFeedback } from "@sentry/core"; +import { z } from "zod"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { structuredResult } from "../../internal/tool-helpers/results"; +import { ALL_SKILLS } from "../../skills"; +import { scrubSensitiveText } from "../../telem/sentry"; + +// Sentry rejects User Feedback messages longer than 4096 code points. Slicing +// UTF-16 units keeps us at or under that. +const MAX_FEEDBACK_MESSAGE_LENGTH = 4096; + +export const submitFeedbackToSentryTeamOutputSchema = z.object({ + feedbackId: z + .string() + .describe("ID of the feedback sent to the Sentry MCP maintainers."), +}); + +export default defineTool({ + name: "submit_feedback_to_sentry_team", + description: [ + "Report friction with the Sentry MCP server to its maintainers.", + "", + "Use this tool when you need to:", + "- Report that no Sentry MCP tool could do what the user asked", + "- Report a tool that failed, returned wrong data, or was confusing", + "- Suggest an improvement to the Sentry MCP tools", + "", + "", + "submit_feedback_to_sentry_team(task='find slow endpoints', kind='missing', impact='blocked', description='No tool returns p95 latency per endpoint.')", + "", + "", + "", + "- Call at most once per task, after you finish or give up.", + "- Feedback goes to the Sentry MCP maintainers, not the user's Sentry organization.", + "- Do not include secrets, credentials, or personal data.", + "", + ].join("\n"), + inputSchema: { + task: z + .string() + .trim() + .min(1) + .max(200) + .describe("A short phrase naming what the user asked you to do."), + kind: z + .enum(["missing", "broken", "confusing", "idea"]) + .describe( + "missing: no tool could do it; broken: a tool failed or returned something wrong; confusing: a tool or its docs were unclear; idea: a suggestion.", + ), + impact: z + .enum(["blocked", "worked_around", "friction"]) + .describe( + "blocked: you could not finish; worked_around: you finished another way; friction: you finished, but it was harder than it should be.", + ), + description: z + .string() + .trim() + .min(1) + .max(4096) + .describe( + "What you tried, including tool calls and any error, and what went wrong.", + ), + toolName: z + .string() + .trim() + .max(100) + .optional() + .describe("The Sentry MCP tool the feedback is about, if any."), + }, + outputSchema: submitFeedbackToSentryTeamOutputSchema, + skills: ALL_SKILLS, + requiredScopes: [], + annotations: { + readOnlyHint: false, + destructiveHint: false, + openWorldHint: true, + }, + async handler(params) { + const message = scrubSensitiveText( + [ + `Task: ${params.task}`, + params.toolName ? `Tool: ${params.toolName}` : null, + "", + params.description, + ] + .filter((line): line is string => line !== null) + .join("\n"), + ); + + const feedbackId = captureFeedback({ + source: "mcp_agent", + message: message.slice(0, MAX_FEEDBACK_MESSAGE_LENGTH), + tags: { + "feedback.kind": params.kind, + "feedback.impact": params.impact, + ...(params.toolName ? { "feedback.tool": params.toolName } : {}), + }, + }); + + return structuredResult({ feedbackId }); + }, +});