Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
79 changes: 79 additions & 0 deletions packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { z } from "zod";
import {
getContinuousProfileUrl as getContinuousProfileUrlUtil,
getAIConversationUrl as getAIConversationUrlUtil,
getDashboardUrl as getDashboardUrlUtil,
getIssueUrl as getIssueUrlUtil,
getMonitorUrl as getMonitorUrlUtil,
getPreprodSnapshotUrl as getPreprodSnapshotUrlUtil,
Expand All @@ -13,6 +14,7 @@ import {
getTraceMetricsExploreUrl,
getTraceUrl as getTraceUrlUtil,
isSentryHost,
type DashboardUrlOptions,
type TraceMetricIdentifier,
} from "../utils/url-utils";
import { isNumericId } from "../utils/slug-validation";
Expand Down Expand Up @@ -57,6 +59,8 @@ import {
ClientKeyListSchema,
AutofixRunSchema,
AutofixRunStateSchema,
DashboardListSchema,
DashboardSchema,
TraceMetaSchema,
TraceSchema,
UserSchema,
Expand All @@ -79,6 +83,8 @@ import type {
AutofixRunState,
ClientKey,
ClientKeyList,
Dashboard,
DashboardListItem,
Event,
EventAttachment,
EventAttachmentList,
Expand Down Expand Up @@ -882,6 +888,20 @@ export class SentryApiService {
);
}

getDashboardUrl(
organizationSlug: string,
dashboardId: string,
options: DashboardUrlOptions = {},
): string {
return getDashboardUrlUtil(
this.host,
organizationSlug,
dashboardId,
options,
this.protocol,
);
}

// ================================================================================
// URL BUILDERS FOR DIFFERENT SENTRY APIS
// ================================================================================
Expand Down Expand Up @@ -1450,6 +1470,65 @@ export class SentryApiService {
return ProjectListSchema.parse(body);
}

async listDashboards(
{
organizationSlug,
query,
sortBy,
limit = 10,
cursor,
}: {
organizationSlug: string;
query?: string | null;
sortBy?: "title" | "-title" | "dateCreated" | "-dateCreated";
limit?: number;
cursor?: string | null;
},
opts?: RequestOptions,
): Promise<{ dashboards: DashboardListItem[]; nextCursor: string | null }> {
const queryParams = new URLSearchParams();
queryParams.set("per_page", String(limit));
if (query) {
queryParams.set("query", query);
}
if (sortBy) {
queryParams.set("sort", sortBy);
}
if (cursor) {
queryParams.set("cursor", cursor);
}

const response = await this.request(
`/organizations/${organizationSlug}/dashboards/?${queryParams.toString()}`,
undefined,
opts,
);
const body = await this.parseJsonResponse(response);

return {
dashboards: DashboardListSchema.parse(body),
nextCursor: getNextCursor(response.headers.get("link")),
};
}

async getDashboard(
{
organizationSlug,
dashboardId,
}: {
organizationSlug: string;
dashboardId: string;
},
opts?: RequestOptions,
): Promise<Dashboard> {
const body = await this.requestJSON(
`/organizations/${organizationSlug}/dashboards/${dashboardId}/`,
undefined,
opts,
);
return DashboardSchema.parse(body);
}

/**
* Gets a single project by slug or ID.
*
Expand Down
106 changes: 106 additions & 0 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,112 @@ export const ProjectRepoLinkSchema = z
})
.passthrough();

/**
* Dashboard schemas validated against getsentry/sentry:
* - `src/sentry/dashboards/endpoints/organization_dashboards.py`
* - `src/sentry/dashboards/endpoints/organization_dashboard_details.py`
* - `src/sentry/api/serializers/models/dashboard.py`
*/
export const DashboardPermissionsSchema = z
.object({
isEditableByEveryone: z.boolean().optional(),
teamsWithEditAccess: z.array(z.union([z.string(), z.number()])).optional(),
})
.passthrough();

export const DashboardWidgetPreviewSchema = z
.object({
displayType: z.string(),
layout: z.record(z.string(), z.unknown()).nullable().optional(),
})
.passthrough();

export const DashboardWidgetQuerySchema = z
.object({
id: z.union([z.string(), z.number()]),
name: z.string(),
fields: z.array(z.string()),
aggregates: z.array(z.string()),
columns: z.array(z.string()),
fieldAliases: z.array(z.string()),
conditions: z.string(),
orderby: z.string(),
widgetId: z.union([z.string(), z.number()]),
isHidden: z.boolean().optional(),
selectedAggregate: z.number().nullable().optional(),
linkedDashboards: z
.array(
z
.object({
field: z.string(),
dashboardId: z.union([z.string(), z.number()]),
})
.passthrough(),
)
.optional(),
})
.passthrough();

export const DashboardWidgetSchema = z
.object({
id: z.union([z.string(), z.number()]),
title: z.string(),
description: z.string().nullable().optional(),
displayType: z.string(),
interval: z.string().nullable().optional(),
dateCreated: z.string(),
dashboardId: z.union([z.string(), z.number()]),
queries: z.array(DashboardWidgetQuerySchema).default([]),
limit: z.number().nullable().optional(),
widgetType: z.string().nullable().optional(),
layout: z.record(z.string(), z.unknown()).nullable().optional(),
datasetSource: z.string().optional(),
})
.passthrough();

export const DashboardFiltersSchema = z.record(z.string(), z.unknown());

export const DashboardListItemSchema = z
.object({
id: z.union([z.string(), z.number()]),
title: z.string(),
dateCreated: z.string(),
createdBy: z.unknown().nullable().optional(),
environment: z.array(z.string()).default([]),
filters: DashboardFiltersSchema.default({}),
lastVisited: z.string().nullable().optional(),
widgetDisplay: z.array(z.string()).default([]),
widgetPreview: z.array(DashboardWidgetPreviewSchema).default([]),
permissions: DashboardPermissionsSchema.nullable().optional(),
isFavorited: z.boolean().optional(),
projects: z.array(z.number()).default([]),
prebuiltId: z.union([z.string(), z.number()]).nullable().optional(),
})
.passthrough();

export const DashboardListSchema = z.array(DashboardListItemSchema);

export const DashboardSchema = z
.object({
id: z.union([z.string(), z.number()]),
title: z.string(),
dateCreated: z.string(),
createdBy: z.unknown().nullable().optional(),
widgets: z.array(DashboardWidgetSchema),
projects: z.array(z.number()).default([]),
environment: z.array(z.string()).default([]),
filters: DashboardFiltersSchema.default({}),
permissions: DashboardPermissionsSchema.nullable().optional(),
isFavorited: z.boolean().optional(),
prebuiltId: z.union([z.string(), z.number()]).nullable().optional(),
period: z.string().nullable().optional(),
start: z.string().nullable().optional(),
end: z.string().nullable().optional(),
utc: z.union([z.string(), z.boolean()]).nullable().optional(),
expired: z.boolean().optional(),
})
.passthrough();

const ReplayTagsSchema = z.preprocess(
(value) => {
if (value === undefined || value === null || Array.isArray(value)) {
Expand Down
8 changes: 8 additions & 0 deletions packages/mcp-core/src/api-client/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ import type {
ClientKeySchema,
CommitListSchema,
CommitSchema,
DashboardListItemSchema,
DashboardSchema,
DashboardWidgetSchema,
DefaultEventSchema,
DeployListSchema,
DeploySchema,
Expand Down Expand Up @@ -167,6 +170,11 @@ export type EventAttachmentList = z.infer<typeof EventAttachmentListSchema>;
export type TagList = z.infer<typeof TagListSchema>;
export type ClientKeyList = z.infer<typeof ClientKeyListSchema>;

// Dashboard types
export type Dashboard = z.infer<typeof DashboardSchema>;
export type DashboardListItem = z.infer<typeof DashboardListItemSchema>;
export type DashboardWidget = z.infer<typeof DashboardWidgetSchema>;

// Trace types
export type TraceMeta = z.infer<typeof TraceMetaSchema>;
export type TraceSpan = z.infer<typeof TraceSpanSchema>;
Expand Down
12 changes: 11 additions & 1 deletion packages/mcp-core/src/skillDefinitions.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
"description": "Read-only access to core Sentry data: issues, events, traces, replays, releases, monitors, profiles, documentation, and project metadata",
"defaultEnabled": true,
"order": 1,
"toolCount": 26,
"toolCount": 28,
"tools": [
{
"name": "find_dashboards",
"description": "Find Sentry dashboards in an organization.\n\nUse this tool when you need to:\n- List dashboards in an organization\n- Find a dashboard ID before calling get_dashboard_details\n- Search dashboards by title\n\n<examples>\nfind_dashboards(organizationSlug='my-organization')\nfind_dashboards(organizationSlug='my-organization', titleQuery='errors')\n</examples>\n\n<hints>\n- Dashboard IDs are organization-scoped.\n- Use `get_dashboard_details` after finding the correct dashboard ID.\n</hints>",
"requiredScopes": ["org:read"]
},
{
"name": "find_monitors",
"description": "Find Sentry cron monitors.\n\nUse this tool when you need to:\n- List cron monitors in an organization\n- Find a monitor by name or slug before getting details\n- Check monitor status, owner, project, schedule, or recent environment state\n\n<examples>\nfind_monitors(organizationSlug='my-organization')\nfind_monitors(organizationSlug='my-organization', projectSlug='backend', query='billing')\n</examples>",
Expand Down Expand Up @@ -37,6 +42,11 @@
"description": "Fetch all spans for an AI conversation by its gen_ai.conversation.id.\n\nA conversation is a set of spans sharing the same gen_ai.conversation.id. To discover conversation IDs, use search_events with dataset='spans' and query='has:gen_ai.conversation.id'.",
"requiredScopes": ["event:read", "project:read"]
},
{
"name": "get_dashboard_details",
"description": "Get detailed information about a specific Sentry dashboard.\n\nUse this tool when you need to:\n- Inspect a dashboard's widgets and saved query definitions\n- View dashboard projects, environments, filters, layout, and widget IDs\n- Resolve a dashboard by exact title or numeric ID\n\n<examples>\nget_dashboard_details(organizationSlug='my-organization', dashboardIdOrTitle='12345')\nget_dashboard_details(organizationSlug='my-organization', dashboardIdOrTitle='Errors Overview')\n</examples>\n\n<hints>\n- Numeric dashboard IDs are resolved directly.\n- Title lookups require one exact case-insensitive match. Use `find_dashboards` first if uncertain.\n- This returns saved widget query definitions, not live widget data.\n</hints>",
"requiredScopes": ["org:read"]
},
{
"name": "get_doc",
"description": "Fetch the full markdown content of a Sentry documentation page.\n\nUse this tool when you need to:\n- Read the complete documentation for a specific topic\n- Get detailed implementation examples or code snippets\n- Access the full context of a documentation page\n- Extract specific sections from documentation\n\n<examples>\n### Get the Next.js integration guide\n\n```\nget_doc(path='/platforms/javascript/guides/nextjs.md')\n```\n</examples>\n\n<hints>\n- Use the path from search_docs results for accurate fetching\n- Paths should end with .md extension\n</hints>",
Expand Down
Loading
Loading