diff --git a/packages/mcp-core/scripts/generate-definitions.ts b/packages/mcp-core/scripts/generate-definitions.ts index 228d39eb3..94805e898 100644 --- a/packages/mcp-core/scripts/generate-definitions.ts +++ b/packages/mcp-core/scripts/generate-definitions.ts @@ -79,6 +79,7 @@ type DefinitionTool = { inputSchema: Record; outputSchema?: ZodTypeAny; skills: string[]; + includeInSkillDefinitions?: boolean; requiredScopes: string[]; experimental?: boolean; hideInExperimentalMode?: boolean; @@ -253,6 +254,7 @@ async function generateSkillDefinitions() { const t = tool as DefinitionTool; return ( isSkillDefinitionTool(t) && + t.includeInSkillDefinitions !== false && Array.isArray(t.skills) && t.skills.includes(skill.id) ); diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index 7db61f58e..2642b50d5 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -34,6 +34,7 @@ import { USER_AGENT } from "../version"; import { apiPath } from "./api-path"; import { ApiNotFoundError, ApiValidationError, createApiError } from "./errors"; import { + AgenticOnboardingRunSchema, AIConversationDetailsResponseSchema, AIConversationSummaryListSchema, ApiErrorSchema, @@ -92,6 +93,8 @@ import { UserSchema, } from "./schema"; import type { + AgenticOnboardingRun, + AgenticOnboardingStatusUpdate, AIConversationDetails, AIConversationSpanList, AIConversationSummary, @@ -1858,6 +1861,28 @@ export class SentryApiService { * @param opts Request options * @returns Updated project data */ + async updateAgenticOnboardingStatus( + { + organizationSlug, + update, + }: { + organizationSlug: string; + update: AgenticOnboardingStatusUpdate; + }, + opts?: RequestOptions, + ): Promise { + const body = await this.requestJSON( + apiPath`/organizations/${organizationSlug}/onboarding/agent/status/`, + { + method: "POST", + body: JSON.stringify(update), + }, + opts, + ); + + return AgenticOnboardingRunSchema.parse(body); + } + async updateProject( { organizationSlug, @@ -2904,8 +2929,7 @@ export class SentryApiService { } this.applyTimeParams(searchQuery, effectiveStatsPeriod, start, end); - const path = - apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeMonitorId}/checks/`; + const path = apiPath`/projects/${organizationSlug}/${projectSlug}/uptime/${uptimeMonitorId}/checks/`; const body = await this.requestJSON( searchQuery.toString() ? `${path}?${searchQuery.toString()}` : path, undefined, diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index e982d95e2..c29f99eec 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -2076,3 +2076,77 @@ export const TransactionProfileSchema = z .optional(), }) .passthrough(); + +export const AgenticOnboardingStageSchema = z.enum([ + "connect_mcp", + "analyze_project", + "create_project", + "instrument_app", + "plan_test_error", + "send_verification_error", + "receive_verification_error", + "prepare_production", + "check_stack_trace_quality", +]); + +export const AgenticOnboardingStageStatusSchema = z.enum([ + "active", + "waiting", + "completed", + "skipped", + "failed", + "bypassed", +]); + +export const AgenticOnboardingStageStatusUpdateSchema = z.enum([ + "active", + "waiting", + "completed", + "skipped", + "failed", +]); + +export const AgenticOnboardingStageStateSchema = z.object({ + stage: AgenticOnboardingStageSchema, + status: AgenticOnboardingStageStatusSchema.nullable(), + eventNote: z.string().nullable(), +}); + +export const AgenticOnboardingRunStatusSchema = z.enum([ + "active", + "completed", + "failed", + "cancelled", +]); + +export const AgenticOnboardingRunStatusUpdateSchema = z.enum([ + "completed", + "failed", +]); + +export const AgenticOnboardingStatusUpdateSchema = z.object({ + schemaVersion: z.literal(1), + runToken: z.string().regex(/^[A-Za-z0-9]{10}$/), + stage: AgenticOnboardingStageSchema, + status: AgenticOnboardingStageStatusUpdateSchema, + runStatus: AgenticOnboardingRunStatusUpdateSchema.optional(), + eventNote: z.string().trim().min(1).max(256).optional(), + projectSlugs: z.array(z.string().trim().min(1)).min(1).max(100).optional(), + issueIds: z.array(z.string().trim().min(1)).min(1).max(100).optional(), +}); + +export const AgenticOnboardingRunSchema = z.object({ + schemaVersion: z.literal(1), + runId: z.string(), + channelId: z.string(), + clientRunId: z.string(), + createdAt: z.string(), + updatedAt: z.string(), + sequence: z.number().int().nonnegative(), + expiresAt: z.string(), + continueUpdates: z.boolean(), + runStatus: AgenticOnboardingRunStatusSchema, + projectSlugs: z.array(z.string()), + issueIds: z.array(z.string()), + stages: z.array(AgenticOnboardingStageStateSchema), +}); diff --git a/packages/mcp-core/src/api-client/types.ts b/packages/mcp-core/src/api-client/types.ts index 7d609f82a..ffeda5280 100644 --- a/packages/mcp-core/src/api-client/types.ts +++ b/packages/mcp-core/src/api-client/types.ts @@ -40,6 +40,8 @@ */ import type { z } from "zod"; import type { + AgenticOnboardingRunSchema, + AgenticOnboardingStatusUpdateSchema, AssignedToSchema, AIConversationSummaryListSchema, AIConversationSummarySchema, @@ -125,6 +127,10 @@ import type { UserReportListSchema, } from "./schema"; +export type AgenticOnboardingRun = z.infer; +export type AgenticOnboardingStatusUpdate = z.infer< + typeof AgenticOnboardingStatusUpdateSchema +>; export type User = z.infer; export type Organization = z.infer; export type Team = z.infer; diff --git a/packages/mcp-core/src/skills.test.ts b/packages/mcp-core/src/skills.test.ts index 664f66c52..1ab5142db 100644 --- a/packages/mcp-core/src/skills.test.ts +++ b/packages/mcp-core/src/skills.test.ts @@ -32,6 +32,14 @@ function getGeneratedSkillToolDescription(skillId: string, toolName: string) { } describe("skills module", () => { + it("does not advertise workflow-only tools in generated skill prompts", () => { + for (const skill of skillDefinitions) { + expect(skill.tools?.map((tool) => tool.name) ?? []).not.toContain( + "onboarding_status_update", + ); + } + }); + describe("SKILLS registry", () => { it("has all expected skills", () => { expect(SKILLS.inspect).toBeDefined(); diff --git a/packages/mcp-core/src/skills.ts b/packages/mcp-core/src/skills.ts index 2b0a41dc0..b8a3af21f 100644 --- a/packages/mcp-core/src/skills.ts +++ b/packages/mcp-core/src/skills.ts @@ -93,6 +93,9 @@ export async function getSkillsArrayWithCounts(): Promise { if (isWrapperTool(toolName) || isCatalogInfrastructureTool(toolName)) { continue; } + if (tool.includeInSkillDefinitions === false) { + continue; + } if (Array.isArray(tool.skills)) { for (const skill of tool.skills) { counts.set(skill as Skill, (counts.get(skill as Skill) || 0) + 1); diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 2d2b1ef7c..a7930c767 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -3779,6 +3779,91 @@ "skills": ["inspect"], "surface": "catalog" }, + { + "name": "onboarding_status_update", + "description": "Update the progress shown in Sentry's agentic onboarding UI.\n\nUse this tool only when the Sentry getting started skill provides a run token. Call it at the workflow boundaries described by that skill.\n\n\n- Progress updates are operational UI state, not user analytics.\n- Keep eventNote brief and limited to context useful in the progress UI.\n- Do not include source code, repository paths, credentials, error output, or other sensitive data.\n- status is required for every update.\n- Set runStatus to completed or failed only when the entire onboarding run reaches that state.\n- A failed status requires a brief eventNote explaining the failure.\n", + "inputSchema": { + "type": "object", + "properties": { + "organizationSlug": { + "type": "string", + "description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool." + }, + "regionUrl": { + "default": null, + "anyOf": [ + { + "type": "string", + "description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool." + }, + { + "type": "null" + } + ] + }, + "runToken": { + "type": "string", + "pattern": "^[A-Za-z0-9]{10}$", + "description": "The 10-character onboarding run token supplied by Sentry." + }, + "stage": { + "type": "string", + "enum": [ + "connect_mcp", + "analyze_project", + "create_project", + "instrument_app", + "plan_test_error", + "send_verification_error", + "receive_verification_error", + "prepare_production", + "check_stack_trace_quality" + ], + "description": "The onboarding stage being updated." + }, + "status": { + "type": "string", + "enum": ["active", "waiting", "completed", "skipped", "failed"], + "description": "The stage's current status." + }, + "runStatus": { + "description": "Set only when the entire onboarding run has completed or failed.", + "type": "string", + "enum": ["completed", "failed"] + }, + "eventNote": { + "description": "Short context to display with this progress event. Required when status is failed.", + "type": "string", + "minLength": 1, + "maxLength": 256 + }, + "projectSlugs": { + "description": "Validated Sentry project slugs. Send only for create_project. Values accumulate across updates; include each project as it becomes usable while active, then all known projects when completed.", + "minItems": 1, + "maxItems": 100, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + }, + "issueIds": { + "description": "Validated Sentry issue IDs returned by MCP. Send only for receive_verification_error. Values accumulate across updates; include each matching issue as it is confirmed, then all known issues when completed.", + "minItems": 1, + "maxItems": 100, + "type": "array", + "items": { + "type": "string", + "minLength": 1 + } + } + }, + "required": ["organizationSlug", "runToken", "stage", "status"] + }, + "requiredScopes": ["org:read"], + "skills": ["inspect", "seer", "docs", "triage", "project-management"], + "surface": "catalog" + }, { "name": "remove_team_from_project", "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", diff --git a/packages/mcp-core/src/tools/catalog/index.ts b/packages/mcp-core/src/tools/catalog/index.ts index ab77d4818..e519aefea 100644 --- a/packages/mcp-core/src/tools/catalog/index.ts +++ b/packages/mcp-core/src/tools/catalog/index.ts @@ -49,6 +49,7 @@ import getLatestBaseSnapshot from "./get-latest-base-snapshot"; import getAIConversationDetails from "./get-ai-conversation-details"; import searchAIConversations from "./search-ai-conversations"; import addIssueNote from "./add-issue-note"; +import onboardingStatusUpdate from "./onboarding-status-update"; import type { ToolConfig } from "../types"; /** @@ -112,6 +113,7 @@ const catalogTools = { get_ai_conversation_details: getAIConversationDetails, search_ai_conversations: searchAIConversations, add_issue_note: addIssueNote, + onboarding_status_update: onboardingStatusUpdate, } as const satisfies Record>; export default catalogTools; diff --git a/packages/mcp-core/src/tools/catalog/onboarding-status-update.test.ts b/packages/mcp-core/src/tools/catalog/onboarding-status-update.test.ts new file mode 100644 index 000000000..7b52812e4 --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/onboarding-status-update.test.ts @@ -0,0 +1,148 @@ +import { mswServer } from "@sentry/mcp-server-mocks"; +import { HttpResponse, http } from "msw"; +import { afterEach, describe, expect, it } from "vitest"; +import onboardingStatusUpdate from "./onboarding-status-update.js"; + +const context = { + constraints: { + organizationSlug: null, + projectSlug: null, + regionUrl: null, + }, + accessToken: "access-token", + userId: "1", +}; + +describe("onboarding_status_update", () => { + afterEach(() => { + mswServer.resetHandlers(); + }); + + it("updates onboarding progress without echoing its inputs", async () => { + let requestBody: unknown; + mswServer.use( + http.post( + "https://us.sentry.io/api/0/organizations/sentry-mcp-evals/onboarding/agent/status/", + async ({ request }) => { + requestBody = await request.json(); + return HttpResponse.json({ + schemaVersion: 1, + runId: "2d27f6654b754dcaa2d26af18274d142", + channelId: "6835652362204cb1b10719783c26983a", + clientRunId: "e806c6f4-fef8-47b4-a720-5ab582b2fcf0", + createdAt: "2026-08-12T12:00:00Z", + updatedAt: "2026-08-12T12:01:00Z", + sequence: 3, + expiresAt: "2026-08-13T12:00:00Z", + continueUpdates: true, + runStatus: "active", + projectSlugs: ["private-project", "worker-project"], + issueIds: [], + stages: [ + { + stage: "analyze_project", + status: "bypassed", + eventNote: null, + }, + ], + }); + }, + ), + ); + + const result = await onboardingStatusUpdate.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: "https://us.sentry.io", + runToken: "a1B2c3D4e5", + stage: "create_project", + status: "completed", + eventNote: "Project already existed.", + projectSlugs: ["private-project", "worker-project"], + }, + context, + ); + + expect(requestBody).toEqual({ + schemaVersion: 1, + runToken: "a1B2c3D4e5", + stage: "create_project", + status: "completed", + eventNote: "Project already existed.", + projectSlugs: ["private-project", "worker-project"], + }); + expect(result).toMatchInlineSnapshot( + `"Onboarding status updated. Continue updates: yes."`, + ); + expect(result).not.toContain("a1B2c3D4e5"); + expect(result).not.toContain("private-project"); + }); + + it("does not accept the backend-derived bypassed status as input", () => { + expect( + onboardingStatusUpdate.inputSchema.status.safeParse("bypassed").success, + ).toBe(false); + expect(onboardingStatusUpdate.inputSchema).not.toHaveProperty( + "failureReason", + ); + expect( + onboardingStatusUpdate.inputSchema.projectSlugs.safeParse([]).success, + ).toBe(false); + expect( + onboardingStatusUpdate.inputSchema.issueIds.safeParse([]).success, + ).toBe(false); + }); + + it("uses the default mock endpoint", async () => { + await expect( + onboardingStatusUpdate.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: null, + runToken: "a1B2c3D4e5", + stage: "connect_mcp", + status: "completed", + }, + context, + ), + ).resolves.toBe("Onboarding status updated. Continue updates: yes."); + }); + + it("reports when the run no longer accepts updates", async () => { + mswServer.use( + http.post( + "https://us.sentry.io/api/0/organizations/sentry-mcp-evals/onboarding/agent/status/", + () => + HttpResponse.json({ + schemaVersion: 1, + runId: "2d27f6654b754dcaa2d26af18274d142", + channelId: "6835652362204cb1b10719783c26983a", + clientRunId: "e806c6f4-fef8-47b4-a720-5ab582b2fcf0", + createdAt: "2026-08-12T12:00:00Z", + updatedAt: "2026-08-12T12:01:00Z", + sequence: 9, + expiresAt: "2026-08-13T12:00:00Z", + continueUpdates: false, + runStatus: "completed", + projectSlugs: [], + issueIds: [], + stages: [], + }), + ), + ); + + await expect( + onboardingStatusUpdate.handler( + { + organizationSlug: "sentry-mcp-evals", + regionUrl: "https://us.sentry.io", + runToken: "a1B2c3D4e5", + stage: "check_stack_trace_quality", + status: "completed", + runStatus: "completed", + }, + context, + ), + ).resolves.toBe("Onboarding status updated. Continue updates: no."); + }); +}); diff --git a/packages/mcp-core/src/tools/catalog/onboarding-status-update.ts b/packages/mcp-core/src/tools/catalog/onboarding-status-update.ts new file mode 100644 index 000000000..d4f96932f --- /dev/null +++ b/packages/mcp-core/src/tools/catalog/onboarding-status-update.ts @@ -0,0 +1,95 @@ +import { setTag } from "@sentry/core"; +import { z } from "zod"; +import { + AgenticOnboardingRunStatusUpdateSchema, + AgenticOnboardingStageSchema, + AgenticOnboardingStageStatusUpdateSchema, + AgenticOnboardingStatusUpdateSchema, +} from "../../api-client/schema"; +import { apiServiceFromContext } from "../../internal/tool-helpers/api"; +import { defineTool } from "../../internal/tool-helpers/define"; +import { ParamOrganizationSlug, ParamRegionUrl } from "../../schema"; +import { ALL_SKILLS } from "../../skills"; +import type { ServerContext } from "../../types"; + +export default defineTool({ + name: "onboarding_status_update", + skills: ALL_SKILLS, + includeInSkillDefinitions: false, + requiredScopes: ["org:read"], + description: [ + "Update the progress shown in Sentry's agentic onboarding UI.", + "", + "Use this tool only when the Sentry getting started skill provides a run token. Call it at the workflow boundaries described by that skill.", + "", + "", + "- Progress updates are operational UI state, not user analytics.", + "- Keep eventNote brief and limited to context useful in the progress UI.", + "- Do not include source code, repository paths, credentials, error output, or other sensitive data.", + "- status is required for every update.", + "- Set runStatus to completed or failed only when the entire onboarding run reaches that state.", + "- A failed status requires a brief eventNote explaining the failure.", + "", + ].join("\n"), + inputSchema: { + organizationSlug: ParamOrganizationSlug, + regionUrl: ParamRegionUrl.nullable().default(null), + runToken: AgenticOnboardingStatusUpdateSchema.shape.runToken.describe( + "The 10-character onboarding run token supplied by Sentry.", + ), + stage: AgenticOnboardingStageSchema.describe( + "The onboarding stage being updated.", + ), + status: AgenticOnboardingStageStatusUpdateSchema.describe( + "The stage's current status.", + ), + runStatus: AgenticOnboardingRunStatusUpdateSchema.optional().describe( + "Set only when the entire onboarding run has completed or failed.", + ), + eventNote: z + .string() + .trim() + .min(1) + .max(256) + .optional() + .describe( + "Short context to display with this progress event. Required when status is failed.", + ), + projectSlugs: + AgenticOnboardingStatusUpdateSchema.shape.projectSlugs.describe( + "Validated Sentry project slugs. Send only for create_project. Values accumulate across updates; include each project as it becomes usable while active, then all known projects when completed.", + ), + issueIds: AgenticOnboardingStatusUpdateSchema.shape.issueIds.describe( + "Validated Sentry issue IDs returned by MCP. Send only for receive_verification_error. Values accumulate across updates; include each matching issue as it is confirmed, then all known issues when completed.", + ), + }, + annotations: { + readOnlyHint: false, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, + async handler(params, context: ServerContext) { + const apiService = apiServiceFromContext(context, { + regionUrl: params.regionUrl ?? context.constraints.regionUrl ?? undefined, + }); + + setTag("organization.slug", params.organizationSlug); + + const run = await apiService.updateAgenticOnboardingStatus({ + organizationSlug: params.organizationSlug, + update: { + schemaVersion: 1, + runToken: params.runToken, + stage: params.stage, + status: params.status, + ...(params.runStatus ? { runStatus: params.runStatus } : {}), + ...(params.eventNote ? { eventNote: params.eventNote } : {}), + ...(params.projectSlugs ? { projectSlugs: params.projectSlugs } : {}), + ...(params.issueIds ? { issueIds: params.issueIds } : {}), + }, + }); + + return `Onboarding status updated. Continue updates: ${run.continueUpdates ? "yes" : "no"}.`; + }, +}); diff --git a/packages/mcp-core/src/tools/types.ts b/packages/mcp-core/src/tools/types.ts index 5cfd1187d..5f956de38 100644 --- a/packages/mcp-core/src/tools/types.ts +++ b/packages/mcp-core/src/tools/types.ts @@ -85,6 +85,7 @@ export interface ToolConfig< description: ToolDescription; inputSchema: TSchema; skills: Skill[]; // Which skill categories this tool belongs to + includeInSkillDefinitions?: boolean; // Whether generated skill prompts advertise this tool requiredScopes: Scope[]; // LEGACY: Which API scopes needed (deprecated, for backward compatibility) experimental?: boolean; // Mark tool as experimental (only shown in experimental mode) hideInExperimentalMode?: boolean; // Hide tool when experimental mode is active (for tools replaced by unified tools) diff --git a/packages/mcp-server-mocks/src/index.ts b/packages/mcp-server-mocks/src/index.ts index d80589a42..69bea77aa 100644 --- a/packages/mcp-server-mocks/src/index.ts +++ b/packages/mcp-server-mocks/src/index.ts @@ -401,6 +401,33 @@ export const restHandlers = buildHandlers([ ]); }, }, + { + method: "post", + path: "/api/0/organizations/sentry-mcp-evals/onboarding/agent/status/", + fetch: () => { + return HttpResponse.json({ + schemaVersion: 1, + runId: "2d27f6654b754dcaa2d26af18274d142", + channelId: "6835652362204cb1b10719783c26983a", + clientRunId: "e806c6f4-fef8-47b4-a720-5ab582b2fcf0", + createdAt: "2026-08-12T12:00:00Z", + updatedAt: "2026-08-12T12:01:00Z", + sequence: 1, + expiresAt: "2026-08-13T12:00:00Z", + continueUpdates: true, + runStatus: "active", + projectSlugs: [], + issueIds: [], + stages: [ + { + stage: "connect_mcp", + status: "completed", + eventNote: null, + }, + ], + }); + }, + }, { method: "get", path: "/api/0/organizations/sentry-mcp-evals/dashboards/",