Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -931,9 +931,15 @@ export const ProfileFrameSchema = z
function: z.string(),
in_app: z.boolean(),
lineno: z.number().nullable().optional(),
colno: z.number().nullable().optional(),
module: z.string().nullable().optional(),
abs_path: z.string().nullable().optional(),
platform: z.string().nullable().optional(),
instruction_addr: z.string().nullable().optional(),
class_name: z.string().nullable().optional(),
raw_function: z.string().nullable().optional(),
symbol: z.string().nullable().optional(),
lang: z.string().nullable().optional(),
data: z.record(z.unknown()).optional(),
})
.passthrough();
Expand All @@ -949,6 +955,7 @@ export const ProfileSampleSchema = z
stack_id: z.number(),
thread_id: z.string(),
timestamp: z.number(),
queue_address: z.string().optional(),
})
.passthrough();

Expand All @@ -967,6 +974,7 @@ export const ProfileChunkSchema = z
.object({
chunk_id: z.string(),
profiler_id: z.string(),
event_id: z.string().optional(),
environment: z.string().nullable(),
platform: z.string(),
release: z.string(),
Expand All @@ -979,6 +987,7 @@ export const ProfileChunkSchema = z
z
.object({
name: z.string().nullable(),
priority: z.number().nullable().optional(),
})
.passthrough(),
),
Expand Down
7 changes: 6 additions & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"description": "Search for errors, analyze traces, and explore event details",
"defaultEnabled": true,
"order": 1,
"toolCount": 14,
"toolCount": 15,
"tools": [
{
"name": "find_organizations",
Expand Down Expand Up @@ -37,6 +37,11 @@
"description": "Get tag value distribution for a specific Sentry issue.\n\nUse this tool when you need to:\n- Understand how an issue is distributed across different tag values\n- Get aggregate counts of unique tag values (e.g., 'how many unique URLs are affected')\n- Analyze which browsers, environments, or URLs are most impacted by an issue\n- View the tag distributions page data programmatically\n\nCommon tag keys:\n- `url`: Request URLs affected by the issue\n- `browser`: Browser types and versions\n- `browser.name`: Browser names only\n- `os`: Operating systems\n- `environment`: Deployment environments (production, staging, etc.)\n- `release`: Software releases\n- `device`: Device types\n- `user`: Affected users\n\n<examples>\n### Get URL distribution for an issue\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='url')\n```\n\n### Get browser distribution using issue URL\n```\nget_issue_tag_values(issueUrl='https://sentry.io/issues/PROJECT-123/', tagKey='browser')\n```\n\n### Get environment distribution\n```\nget_issue_tag_values(organizationSlug='my-organization', issueId='PROJECT-123', tagKey='environment')\n```\n</examples>\n\n<hints>\n- If user provides a Sentry URL, pass the ENTIRE URL to issueUrl parameter unchanged\n- Common tag keys: url, browser, browser.name, os, environment, release, device, user\n- Tag keys are case-sensitive\n</hints>",
"requiredScopes": ["event:read"]
},
{
"name": "get_profile_details",
"description": "Retrieve raw profile chunk data to inspect individual function calls, threads, and stack traces.\n\nUSE THIS TOOL WHEN:\n- User wants to inspect raw profiling samples for a specific profiler session\n- User needs to see individual thread activity and stack traces\n- User wants detailed frame-level data (function names, file locations, call counts)\n\nRETURNS:\n- Profile chunk metadata (platform, release, environment)\n- Per-thread sample counts and names\n- Top frames by occurrence with file locations\n- User code vs library code breakdown\n\nNOTE: This tool requires a `profilerId` which identifies a specific profiling session.\nUse `get_profile` for aggregated flamegraph analysis by transaction name.\n\n<examples>\n### Inspect a profiler session\n```\nget_profile_details(\n organizationSlug='my-org',\n projectSlugOrId='backend',\n profilerId='041bde57b9844e36b8b7e5734efae5f7',\n start='2024-01-01T00:00:00',\n end='2024-01-01T01:00:00'\n)\n```\n</examples>\n\n<hints>\n- Use `focusOnUserCode: true` (default) to filter out library/system frames\n- The profilerId can be found in Sentry profile URLs or event data\n</hints>",
"requiredScopes": ["event:read"]
},
{
"name": "get_sentry_resource",
"description": "Fetch a Sentry resource by URL or by type and ID.\n\n<examples>\n### From a Sentry URL\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\n\n### Breadcrumbs from a Sentry URL\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/', resourceType='breadcrumbs')\n\n### By type and ID\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\n</examples>",
Expand Down
57 changes: 57 additions & 0 deletions packages/mcp-core/src/toolDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,63 @@
},
"requiredScopes": ["event:read"]
},
{
"name": "get_profile_details",
"description": "Retrieve raw profile chunk data to inspect individual function calls, threads, and stack traces.\n\nUSE THIS TOOL WHEN:\n- User wants to inspect raw profiling samples for a specific profiler session\n- User needs to see individual thread activity and stack traces\n- User wants detailed frame-level data (function names, file locations, call counts)\n\nRETURNS:\n- Profile chunk metadata (platform, release, environment)\n- Per-thread sample counts and names\n- Top frames by occurrence with file locations\n- User code vs library code breakdown\n\nNOTE: This tool requires a `profilerId` which identifies a specific profiling session.\nUse `get_profile` for aggregated flamegraph analysis by transaction name.\n\n<examples>\n### Inspect a profiler session\n```\nget_profile_details(\n organizationSlug='my-org',\n projectSlugOrId='backend',\n profilerId='041bde57b9844e36b8b7e5734efae5f7',\n start='2024-01-01T00:00:00',\n end='2024-01-01T01:00:00'\n)\n```\n</examples>\n\n<hints>\n- Use `focusOnUserCode: true` (default) to filter out library/system frames\n- The profilerId can be found in Sentry profile URLs or event data\n</hints>",
"inputSchema": {
"type": "object",
"properties": {
"organizationSlug": {
"type": "string",
"description": "The organization's slug. You can find a existing list of organizations you have access to using the `find_organizations()` tool."
},
"regionUrl": {
"anyOf": [
{
"type": "string",
"description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool."
},
{
"type": "null"
}
],
"description": "The region URL for the organization you're querying, if known. For Sentry's Cloud Service (sentry.io), this is typically the region-specific URL like 'https://us.sentry.io'. For self-hosted Sentry installations, this parameter is usually not needed and should be omitted. You can find the correct regionUrl from the organization details using the `find_organizations()` tool.",
"default": null
},
"projectSlugOrId": {
"type": ["string", "number"],
"description": "Project slug or numeric ID"
},
"profilerId": {
"type": "string",
"description": "Profiler session ID (UUID from Sentry profile data)"
},
"start": {
"type": "string",
"description": "Start time for the profile chunk query (ISO 8601 format, e.g., '2024-01-01T00:00:00')"
},
"end": {
"type": "string",
"description": "End time for the profile chunk query (ISO 8601 format, e.g., '2024-01-01T01:00:00')"
},
"focusOnUserCode": {
"type": "boolean",
"default": true,
"description": "Show only user code (in_app: true). Set to false to include library code."
}
},
"required": [
"organizationSlug",
"projectSlugOrId",
"profilerId",
"start",
"end"
],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
},
"requiredScopes": ["event:read"]
},
{
"name": "get_sentry_resource",
"description": "Fetch a Sentry resource by URL or by type and ID.\n\n<examples>\n### From a Sentry URL\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/')\n\n### Breadcrumbs from a Sentry URL\nget_sentry_resource(url='https://sentry.io/issues/PROJECT-123/', resourceType='breadcrumbs')\n\n### By type and ID\nget_sentry_resource(resourceType='issue', organizationSlug='my-org', resourceId='PROJECT-123')\n</examples>",
Expand Down
86 changes: 86 additions & 0 deletions packages/mcp-core/src/tools/get-profile-details.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, it, expect } from "vitest";
import getProfileDetails from "./get-profile-details";

const baseContext = {
constraints: {
organizationSlug: null,
},
accessToken: "access-token",
userId: "1",
};

function callHandler(params: {
organizationSlug: string;
projectSlugOrId: string | number;
profilerId: string;
start: string;
end: string;
regionUrl?: string | null;
focusOnUserCode?: boolean;
}) {
return getProfileDetails.handler(
{ regionUrl: null, focusOnUserCode: true, ...params },
baseContext,
);
}

describe("get_profile_details", () => {
describe("handler", () => {
it("fetches and formats a profile chunk", async () => {
const result = await callHandler({
organizationSlug: "sentry-mcp-evals",
projectSlugOrId: "backend",
profilerId: "041bde57b9844e36b8b7e5734efae5f7",
start: "2024-01-01T00:00:00",
end: "2024-01-01T01:00:00",
});

expect(result).toContain("# Profile Chunk Details");
expect(result).toContain("## Metadata");
expect(result).toContain("## Sample Summary");
expect(result).toContain("## Thread Information");
expect(result).toContain("## Top Frames by Occurrence");
});

it("respects focusOnUserCode option", async () => {
const resultAll = await callHandler({
organizationSlug: "sentry-mcp-evals",
projectSlugOrId: "backend",
profilerId: "041bde57b9844e36b8b7e5734efae5f7",
start: "2024-01-01T00:00:00",
end: "2024-01-01T01:00:00",
focusOnUserCode: false,
});

// With focusOnUserCode false, should include library frames
expect(resultAll).toContain("Library");
});
});

describe("tool definition", () => {
it("has read-only annotation", () => {
expect(getProfileDetails.annotations.readOnlyHint).toBe(true);
});

it("belongs to inspect skill", () => {
expect(getProfileDetails.skills).toContain("inspect");
});

it("requires profiles capability", () => {
expect(getProfileDetails.requiredCapabilities).toContain("profiles");
});

it("has expected params", () => {
const schemaKeys = Object.keys(getProfileDetails.inputSchema);
expect(schemaKeys).toEqual([
"organizationSlug",
"regionUrl",
"projectSlugOrId",
"profilerId",
"start",
"end",
"focusOnUserCode",
]);
});
});
});
118 changes: 118 additions & 0 deletions packages/mcp-core/src/tools/get-profile-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { setTag } from "@sentry/core";
import { z } from "zod";
import { defineTool } from "../internal/tool-helpers/define";
import { apiServiceFromContext } from "../internal/tool-helpers/api";
import { UserInputError } from "../errors";
import type { ServerContext } from "../types";
import { ParamOrganizationSlug, ParamRegionUrl } from "../schema";
import { formatProfileChunkAnalysis } from "./profile/formatter";

export default defineTool({
name: "get_profile_details",
skills: ["inspect"],
requiredScopes: ["event:read"],
requiredCapabilities: ["profiles"],
hideInExperimentalMode: true,

description: [
"Retrieve raw profile chunk data to inspect individual function calls, threads, and stack traces.",
"",
"USE THIS TOOL WHEN:",
"- User wants to inspect raw profiling samples for a specific profiler session",
"- User needs to see individual thread activity and stack traces",
"- User wants detailed frame-level data (function names, file locations, call counts)",
"",
"RETURNS:",
"- Profile chunk metadata (platform, release, environment)",
"- Per-thread sample counts and names",
"- Top frames by occurrence with file locations",
"- User code vs library code breakdown",
"",
"NOTE: This tool requires a `profilerId` which identifies a specific profiling session.",
"Use `get_profile` for aggregated flamegraph analysis by transaction name.",
"",
"<examples>",
"### Inspect a profiler session",
"```",
"get_profile_details(",
" organizationSlug='my-org',",
" projectSlugOrId='backend',",
" profilerId='041bde57b9844e36b8b7e5734efae5f7',",
" start='2024-01-01T00:00:00',",
" end='2024-01-01T01:00:00'",
")",
"```",
"</examples>",
"",
"<hints>",
"- Use `focusOnUserCode: true` (default) to filter out library/system frames",
"- The profilerId can be found in Sentry profile URLs or event data",
"</hints>",
].join("\n"),

inputSchema: {
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.nullable().default(null),
projectSlugOrId: z
.union([z.string(), z.number()])
.describe("Project slug or numeric ID"),
profilerId: z
.string()
.trim()
.describe("Profiler session ID (UUID from Sentry profile data)"),
start: z
.string()
.trim()
.describe(
"Start time for the profile chunk query (ISO 8601 format, e.g., '2024-01-01T00:00:00')",
),
end: z
.string()
.trim()
.describe(
"End time for the profile chunk query (ISO 8601 format, e.g., '2024-01-01T01:00:00')",
),
focusOnUserCode: z
.boolean()
.default(true)
.describe(
"Show only user code (in_app: true). Set to false to include library code.",
),
},

annotations: { readOnlyHint: true, openWorldHint: false },

async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
regionUrl: params.regionUrl ?? undefined,
});

const {
organizationSlug,
projectSlugOrId,
profilerId,
start,
end,
focusOnUserCode,
} = params;

if (!profilerId) {
throw new UserInputError(
"profilerId is required. This is the profiler session UUID found in Sentry profile URLs or event data.",
);
}

setTag("organization.slug", organizationSlug);
setTag("profiler.id", profilerId);

const chunk = await apiService.getProfileChunk({
organizationSlug,
profilerId,
projectId: projectSlugOrId,
start,
end,
});
Comment thread
sentry[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.

return formatProfileChunkAnalysis(chunk, { focusOnUserCode });
},
});
2 changes: 2 additions & 0 deletions packages/mcp-core/src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import useSentry from "./use-sentry";
import listIssues from "./list-issues";
import listEvents from "./list-events";
import listIssueEvents from "./list-issue-events";
import getProfileDetails from "./get-profile-details";
import getSentryResource from "./get-sentry-resource";

/**
Expand Down Expand Up @@ -75,6 +76,7 @@ export default {
list_issues: listIssues,
list_events: listEvents,
list_issue_events: listIssueEvents,
get_profile_details: getProfileDetails,
get_sentry_resource: getSentryResource,
} as const;

Expand Down
Loading
Loading