chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,75 @@
import type {
GrafanaDataSourceHealthParams,
GrafanaDataSourceHealthResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const checkDataSourceHealthTool: ToolConfig<
GrafanaDataSourceHealthParams,
GrafanaDataSourceHealthResponse
> = {
id: 'grafana_check_data_source_health',
name: 'Grafana Check Data Source Health',
description: 'Test connectivity to a data source by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
dataSourceUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the data source to health-check (e.g., P1234AB5678)',
},
},
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/datasources/uid/${params.dataSourceUid.trim()}/health`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
status: (data.status as string) ?? 'UNKNOWN',
message: (data.message as string) ?? '',
},
}
},
outputs: {
status: { type: 'string', description: 'Health status of the data source (e.g., OK)' },
message: { type: 'string', description: 'Detailed health message from the data source' },
},
}
+227
View File
@@ -0,0 +1,227 @@
import type {
GrafanaCreateAlertRuleParams,
GrafanaCreateAlertRuleResponse,
} from '@/tools/grafana/types'
import { ALERT_RULE_OUTPUT_FIELDS } from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig } from '@/tools/types'
export const createAlertRuleTool: ToolConfig<
GrafanaCreateAlertRuleParams,
GrafanaCreateAlertRuleResponse
> = {
id: 'grafana_create_alert_rule',
name: 'Grafana Create Alert Rule',
description: 'Create a new alert rule',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the alert rule',
},
folderUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the folder to create the alert in (e.g., folder-abc123)',
},
ruleGroup: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The name of the rule group',
},
condition: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'The refId of the query or expression to use as the alert condition (required for alerting rules; omit for recording rules)',
},
data: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'JSON array of query/expression data objects',
},
forDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Duration to wait before firing (e.g., 5m, 1h)',
},
noDataState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'State when no data is returned (NoData, Alerting, OK)',
},
execErrState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'State on execution error (Error, Alerting, OK)',
},
annotations: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object of annotations',
},
labels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object of labels',
},
uid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional custom UID for the alert rule',
},
isPaused: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether the rule is paused on creation',
},
keepFiringFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Duration to keep firing after the condition stops (e.g., 5m)',
},
missingSeriesEvalsToResolve: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Number of missing series evaluations before resolving',
},
notificationSettings: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'JSON object of per-rule notification settings (overrides)',
},
record: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object configuring this as a recording rule (omit for alerting rules)',
},
disableProvenance: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`,
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
if (params.disableProvenance) {
headers['X-Disable-Provenance'] = 'true'
}
return headers
},
body: (params) => {
let dataArray: unknown[] = []
try {
dataArray = JSON.parse(params.data)
} catch {
throw new Error('Invalid JSON for data parameter')
}
const body: Record<string, unknown> = {
title: params.title,
folderUID: params.folderUid,
ruleGroup: params.ruleGroup,
data: dataArray,
}
if (params.organizationId) body.orgID = Number(params.organizationId)
if (params.condition) body.condition = params.condition
if (params.uid) body.uid = params.uid.trim()
if (params.forDuration) body.for = params.forDuration
if (params.noDataState) body.noDataState = params.noDataState
if (params.execErrState) body.execErrState = params.execErrState
if (params.isPaused !== undefined) body.isPaused = params.isPaused
if (params.keepFiringFor) body.keep_firing_for = params.keepFiringFor
if (params.missingSeriesEvalsToResolve !== undefined) {
body.missingSeriesEvalsToResolve = params.missingSeriesEvalsToResolve
}
if (params.annotations) {
try {
body.annotations = JSON.parse(params.annotations)
} catch {
throw new Error('Invalid JSON for annotations parameter')
}
}
if (params.labels) {
try {
body.labels = JSON.parse(params.labels)
} catch {
throw new Error('Invalid JSON for labels parameter')
}
}
if (params.notificationSettings) {
try {
body.notification_settings = JSON.parse(params.notificationSettings)
} catch {
throw new Error('Invalid JSON for notificationSettings parameter')
}
}
if (params.record) {
try {
body.record = JSON.parse(params.record)
} catch {
throw new Error('Invalid JSON for record parameter')
}
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return { success: true, output: mapAlertRule(data) }
},
outputs: ALERT_RULE_OUTPUT_FIELDS,
}
+130
View File
@@ -0,0 +1,130 @@
import type {
GrafanaCreateAnnotationParams,
GrafanaCreateAnnotationResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const createAnnotationTool: ToolConfig<
GrafanaCreateAnnotationParams,
GrafanaCreateAnnotationResponse
> = {
id: 'grafana_create_annotation',
name: 'Grafana Create Annotation',
description: 'Create an annotation on a dashboard or as a global annotation',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
text: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The text content of the annotation',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of tags',
},
dashboardUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'UID of the dashboard to add the annotation to (e.g., abc123def). Omit to create a global organization annotation.',
},
panelId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'ID of the panel to add the annotation to (e.g., 1, 2)',
},
time: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start time in epoch milliseconds (e.g., 1704067200000, defaults to now)',
},
timeEnd: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End time in epoch milliseconds for range annotations (e.g., 1704153600000)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations`,
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
body: (params) => {
const body: Record<string, unknown> = {
text: params.text,
}
if (params.time) body.time = params.time
if (params.timeEnd) body.timeEnd = params.timeEnd
if (params.dashboardUid) body.dashboardUID = params.dashboardUid
if (params.panelId) body.panelId = params.panelId
if (params.tags) {
body.tags = params.tags
.split(',')
.map((t) => t.trim())
.filter((t) => t)
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
message: data.message || 'Annotation created successfully',
},
}
},
outputs: {
id: {
type: 'number',
description: 'The ID of the created annotation',
},
message: {
type: 'string',
description: 'Confirmation message',
},
},
}
@@ -0,0 +1,132 @@
import type {
GrafanaCreateContactPointParams,
GrafanaCreateContactPointResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const createContactPointTool: ToolConfig<
GrafanaCreateContactPointParams,
GrafanaCreateContactPointResponse
> = {
id: 'grafana_create_contact_point',
name: 'Grafana Create Contact Point',
description: 'Create a notification contact point (e.g., Slack, email, PagerDuty)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
name: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Name of the contact point (groups receivers shown in the UI)',
},
type: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Receiver type (e.g., slack, email, pagerduty, webhook)',
},
settings: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description:
'JSON object of type-specific settings (e.g., {"addresses":"a@b.com"} for email, {"url":"..."} for slack)',
},
disableResolveMessage: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Do not send a notification when the alert resolves',
},
disableProvenance: {
type: 'boolean',
required: false,
visibility: 'user-only',
description:
'Set X-Disable-Provenance header so the contact point remains editable in the UI',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`,
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
if (params.disableProvenance) {
headers['X-Disable-Provenance'] = 'true'
}
return headers
},
body: (params) => {
let settings: Record<string, unknown> = {}
try {
settings = JSON.parse(params.settings)
} catch {
throw new Error('Invalid JSON for settings parameter')
}
const body: Record<string, unknown> = {
name: params.name,
type: params.type,
settings,
}
if (params.disableResolveMessage !== undefined) {
body.disableResolveMessage = params.disableResolveMessage
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
uid: (data.uid as string) ?? '',
name: (data.name as string) ?? '',
type: (data.type as string) ?? '',
settings: (data.settings as Record<string, unknown>) ?? {},
disableResolveMessage: (data.disableResolveMessage as boolean) ?? false,
provenance: (data.provenance as string) ?? '',
},
}
},
outputs: {
uid: { type: 'string', description: 'UID of the created contact point' },
name: { type: 'string', description: 'Name of the contact point' },
type: { type: 'string', description: 'Receiver type' },
settings: { type: 'json', description: 'Type-specific settings' },
disableResolveMessage: {
type: 'boolean',
description: 'Whether resolve notifications are suppressed',
},
provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' },
},
}
+182
View File
@@ -0,0 +1,182 @@
import type {
GrafanaCreateDashboardParams,
GrafanaCreateDashboardResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const createDashboardTool: ToolConfig<
GrafanaCreateDashboardParams,
GrafanaCreateDashboardResponse
> = {
id: 'grafana_create_dashboard',
name: 'Grafana Create Dashboard',
description: 'Create a new dashboard',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the new dashboard',
},
folderUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'The UID of the folder to create the dashboard in (e.g., folder-abc123)',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of tags',
},
timezone: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dashboard timezone (e.g., browser, utc)',
},
refresh: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)',
},
panels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of panel configurations',
},
overwrite: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Overwrite existing dashboard with same title',
},
message: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Commit message for the dashboard version',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/db`,
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
body: (params) => {
const dashboard: Record<string, any> = {
title: params.title,
tags: params.tags
? params.tags
.split(',')
.map((t) => t.trim())
.filter((t) => t)
: [],
timezone: params.timezone || 'browser',
schemaVersion: 39,
version: 0,
refresh: params.refresh || '',
}
if (params.panels) {
try {
dashboard.panels = JSON.parse(params.panels)
} catch {
dashboard.panels = []
}
} else {
dashboard.panels = []
}
const body: Record<string, any> = {
dashboard,
overwrite: params.overwrite || false,
}
if (params.folderUid) {
body.folderUid = params.folderUid
}
if (params.message) {
body.message = params.message
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id,
uid: data.uid,
url: data.url,
status: data.status,
version: data.version,
slug: data.slug,
},
}
},
outputs: {
id: {
type: 'number',
description: 'The numeric ID of the created dashboard',
},
uid: {
type: 'string',
description: 'The UID of the created dashboard',
},
url: {
type: 'string',
description: 'The URL path to the dashboard',
},
status: {
type: 'string',
description: 'Status of the operation (success)',
},
version: {
type: 'number',
description: 'The version number of the dashboard',
},
slug: {
type: 'string',
description: 'URL-friendly slug of the dashboard',
},
},
}
+157
View File
@@ -0,0 +1,157 @@
import type { GrafanaCreateFolderParams, GrafanaCreateFolderResponse } from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCreateFolderResponse> =
{
id: 'grafana_create_folder',
name: 'Grafana Create Folder',
description: 'Create a new folder in Grafana',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The title of the new folder',
},
uid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional UID for the folder (auto-generated if not provided)',
},
parentUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Parent folder UID for nested folders (requires nested folders enabled)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders`,
method: 'POST',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
body: (params) => {
const body: Record<string, unknown> = {
title: params.title,
}
if (params.uid) body.uid = params.uid
if (params.parentUid) body.parentUid = params.parentUid
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: (data.id as number) ?? null,
uid: (data.uid as string) ?? null,
title: (data.title as string) ?? null,
url: (data.url as string) ?? null,
parentUid: (data.parentUid as string) ?? null,
parents: (data.parents as { uid: string; title: string; url: string }[]) ?? [],
hasAcl: (data.hasAcl as boolean) ?? null,
canSave: (data.canSave as boolean) ?? null,
canEdit: (data.canEdit as boolean) ?? null,
canAdmin: (data.canAdmin as boolean) ?? null,
createdBy: (data.createdBy as string) ?? null,
created: (data.created as string) ?? null,
updatedBy: (data.updatedBy as string) ?? null,
updated: (data.updated as string) ?? null,
version: (data.version as number) ?? null,
},
}
},
outputs: {
id: { type: 'number', description: 'The numeric ID of the created folder' },
uid: { type: 'string', description: 'The UID of the created folder' },
title: { type: 'string', description: 'The title of the created folder' },
url: { type: 'string', description: 'The URL path to the folder', optional: true },
parentUid: {
type: 'string',
description: 'Parent folder UID (nested folders only)',
optional: true,
},
parents: {
type: 'array',
description: 'Ancestor folder hierarchy (nested folders only)',
optional: true,
},
hasAcl: {
type: 'boolean',
description: 'Whether the folder has custom ACL permissions',
optional: true,
},
canSave: {
type: 'boolean',
description: 'Whether the current user can save the folder',
optional: true,
},
canEdit: {
type: 'boolean',
description: 'Whether the current user can edit the folder',
optional: true,
},
canAdmin: {
type: 'boolean',
description: 'Whether the current user has admin rights on the folder',
optional: true,
},
createdBy: {
type: 'string',
description: 'Username of who created the folder',
optional: true,
},
created: {
type: 'string',
description: 'Timestamp when the folder was created',
optional: true,
},
updatedBy: {
type: 'string',
description: 'Username of who last updated the folder',
optional: true,
},
updated: {
type: 'string',
description: 'Timestamp when the folder was last updated',
optional: true,
},
version: { type: 'number', description: 'Version number of the folder', optional: true },
},
}
@@ -0,0 +1,74 @@
import type {
GrafanaDeleteAlertRuleParams,
GrafanaDeleteAlertRuleResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const deleteAlertRuleTool: ToolConfig<
GrafanaDeleteAlertRuleParams,
GrafanaDeleteAlertRuleResponse
> = {
id: 'grafana_delete_alert_rule',
name: 'Grafana Delete Alert Rule',
description: 'Delete an alert rule by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
alertRuleUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the alert rule to delete',
},
},
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`,
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async () => {
return {
success: true,
output: {
message: 'Alert rule deleted successfully',
},
}
},
outputs: {
message: {
type: 'string',
description: 'Confirmation message',
},
},
}
@@ -0,0 +1,75 @@
import type {
GrafanaDeleteAnnotationParams,
GrafanaDeleteAnnotationResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const deleteAnnotationTool: ToolConfig<
GrafanaDeleteAnnotationParams,
GrafanaDeleteAnnotationResponse
> = {
id: 'grafana_delete_annotation',
name: 'Grafana Delete Annotation',
description: 'Delete an annotation by its ID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
annotationId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the annotation to delete',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`,
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
message: data.message || 'Annotation deleted successfully',
},
}
},
outputs: {
message: {
type: 'string',
description: 'Confirmation message',
},
},
}
@@ -0,0 +1,86 @@
import type {
GrafanaDeleteDashboardParams,
GrafanaDeleteDashboardResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const deleteDashboardTool: ToolConfig<
GrafanaDeleteDashboardParams,
GrafanaDeleteDashboardResponse
> = {
id: 'grafana_delete_dashboard',
name: 'Grafana Delete Dashboard',
description: 'Delete a dashboard by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
dashboardUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the dashboard to delete (e.g., abc123def)',
},
},
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`,
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
title: data.title || '',
message: data.message || 'Dashboard deleted',
id: data.id || 0,
},
}
},
outputs: {
title: {
type: 'string',
description: 'The title of the deleted dashboard',
},
message: {
type: 'string',
description: 'Confirmation message',
},
id: {
type: 'number',
description: 'The ID of the deleted dashboard',
},
},
}
+79
View File
@@ -0,0 +1,79 @@
import type { GrafanaDeleteFolderParams, GrafanaDeleteFolderResponse } from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const deleteFolderTool: ToolConfig<GrafanaDeleteFolderParams, GrafanaDeleteFolderResponse> =
{
id: 'grafana_delete_folder',
name: 'Grafana Delete Folder',
description: 'Delete a folder by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
folderUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the folder to delete (e.g., folder-abc123)',
},
forceDeleteRules: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Delete any alert rules stored in the folder along with it (default false)',
},
},
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const query = params.forceDeleteRules ? '?forceDeleteRules=true' : ''
return `${baseUrl}/api/folders/${params.folderUid.trim()}${query}`
},
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response, params) => {
const data = await response.json().catch(() => ({}))
return {
success: true,
output: {
uid: params?.folderUid?.trim() ?? '',
message: (data.message as string) ?? 'Folder deleted',
},
}
},
outputs: {
uid: { type: 'string', description: 'The UID of the deleted folder' },
message: { type: 'string', description: 'Confirmation message' },
},
}
+65
View File
@@ -0,0 +1,65 @@
import {
ALERT_RULE_OUTPUT_FIELDS,
type GrafanaGetAlertRuleParams,
type GrafanaGetAlertRuleResponse,
} from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig } from '@/tools/types'
export const getAlertRuleTool: ToolConfig<GrafanaGetAlertRuleParams, GrafanaGetAlertRuleResponse> =
{
id: 'grafana_get_alert_rule',
name: 'Grafana Get Alert Rule',
description: 'Get a specific alert rule by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
alertRuleUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the alert rule to retrieve',
},
},
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return { success: true, output: mapAlertRule(data) }
},
outputs: ALERT_RULE_OUTPUT_FIELDS,
}
+76
View File
@@ -0,0 +1,76 @@
import type { GrafanaGetDashboardParams, GrafanaGetDashboardResponse } from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const getDashboardTool: ToolConfig<GrafanaGetDashboardParams, GrafanaGetDashboardResponse> =
{
id: 'grafana_get_dashboard',
name: 'Grafana Get Dashboard',
description: 'Get a dashboard by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
dashboardUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the dashboard to retrieve (e.g., abc123def)',
},
},
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
dashboard: data.dashboard,
meta: data.meta,
},
}
},
outputs: {
dashboard: {
type: 'json',
description: 'The full dashboard JSON object',
},
meta: {
type: 'json',
description: 'Dashboard metadata (version, permissions, etc.)',
},
},
}
+122
View File
@@ -0,0 +1,122 @@
import type {
GrafanaGetDataSourceParams,
GrafanaGetDataSourceResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const getDataSourceTool: ToolConfig<
GrafanaGetDataSourceParams,
GrafanaGetDataSourceResponse
> = {
id: 'grafana_get_data_source',
name: 'Grafana Get Data Source',
description: 'Get a data source by its ID or UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
dataSourceId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The ID or UID of the data source to retrieve (e.g., prometheus, P1234AB5678)',
},
},
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const id = params.dataSourceId.trim()
const isNumericId = /^\d+$/.test(id) && id.length <= 18
if (isNumericId) {
return `${baseUrl}/api/datasources/${id}`
}
return `${baseUrl}/api/datasources/uid/${id}`
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: (data.id as number) ?? null,
uid: (data.uid as string) ?? null,
orgId: (data.orgId as number) ?? null,
name: (data.name as string) ?? null,
type: (data.type as string) ?? null,
typeLogoUrl: (data.typeLogoUrl as string) ?? null,
access: (data.access as string) ?? null,
url: (data.url as string) ?? null,
user: (data.user as string) ?? null,
database: (data.database as string) ?? null,
basicAuth: (data.basicAuth as boolean) ?? false,
basicAuthUser: (data.basicAuthUser as string) ?? null,
withCredentials: (data.withCredentials as boolean) ?? null,
isDefault: (data.isDefault as boolean) ?? false,
jsonData: (data.jsonData as Record<string, unknown>) ?? {},
secureJsonFields: (data.secureJsonFields as Record<string, boolean>) ?? {},
version: (data.version as number) ?? null,
readOnly: (data.readOnly as boolean) ?? false,
},
}
},
outputs: {
id: { type: 'number', description: 'Data source ID' },
uid: { type: 'string', description: 'Data source UID' },
orgId: { type: 'number', description: 'Organization ID' },
name: { type: 'string', description: 'Data source name' },
type: { type: 'string', description: 'Data source type' },
typeLogoUrl: { type: 'string', description: 'Logo URL for the data source type' },
access: { type: 'string', description: 'Access mode (proxy or direct)' },
url: { type: 'string', description: 'Data source connection URL' },
user: { type: 'string', description: 'Username used to connect' },
database: { type: 'string', description: 'Database name (if applicable)' },
basicAuth: { type: 'boolean', description: 'Whether basic auth is enabled' },
basicAuthUser: { type: 'string', description: 'Basic auth username', optional: true },
withCredentials: {
type: 'boolean',
description: 'Whether to send credentials with cross-origin requests',
optional: true,
},
isDefault: { type: 'boolean', description: 'Whether this is the default data source' },
jsonData: { type: 'json', description: 'Additional data source configuration' },
secureJsonFields: {
type: 'object',
description: 'Map of secure fields that are set (values are not returned)',
optional: true,
},
version: { type: 'number', description: 'Data source version', optional: true },
readOnly: { type: 'boolean', description: 'Whether the data source is read-only' },
},
}
+134
View File
@@ -0,0 +1,134 @@
import type { GrafanaGetFolderParams, GrafanaGetFolderResponse } from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const getFolderTool: ToolConfig<GrafanaGetFolderParams, GrafanaGetFolderResponse> = {
id: 'grafana_get_folder',
name: 'Grafana Get Folder',
description: 'Get a folder by its UID',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
folderUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the folder to retrieve (e.g., folder-abc123)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders/${params.folderUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: (data.id as number) ?? null,
uid: (data.uid as string) ?? null,
title: (data.title as string) ?? null,
url: (data.url as string) ?? null,
parentUid: (data.parentUid as string) ?? null,
parents: (data.parents as { uid: string; title: string; url: string }[]) ?? [],
hasAcl: (data.hasAcl as boolean) ?? null,
canSave: (data.canSave as boolean) ?? null,
canEdit: (data.canEdit as boolean) ?? null,
canAdmin: (data.canAdmin as boolean) ?? null,
createdBy: (data.createdBy as string) ?? null,
created: (data.created as string) ?? null,
updatedBy: (data.updatedBy as string) ?? null,
updated: (data.updated as string) ?? null,
version: (data.version as number) ?? null,
},
}
},
outputs: {
id: { type: 'number', description: 'The numeric ID of the folder' },
uid: { type: 'string', description: 'The UID of the folder' },
title: { type: 'string', description: 'The title of the folder' },
url: { type: 'string', description: 'The URL path to the folder', optional: true },
parentUid: {
type: 'string',
description: 'Parent folder UID (nested folders only)',
optional: true,
},
parents: {
type: 'array',
description: 'Ancestor folder hierarchy (nested folders only)',
optional: true,
},
hasAcl: {
type: 'boolean',
description: 'Whether the folder has custom ACL permissions',
optional: true,
},
canSave: {
type: 'boolean',
description: 'Whether the current user can save the folder',
optional: true,
},
canEdit: {
type: 'boolean',
description: 'Whether the current user can edit the folder',
optional: true,
},
canAdmin: {
type: 'boolean',
description: 'Whether the current user has admin rights on the folder',
optional: true,
},
createdBy: {
type: 'string',
description: 'Username of who created the folder',
optional: true,
},
created: {
type: 'string',
description: 'Timestamp when the folder was created',
optional: true,
},
updatedBy: {
type: 'string',
description: 'Username of who last updated the folder',
optional: true,
},
updated: {
type: 'string',
description: 'Timestamp when the folder was last updated',
optional: true,
},
version: { type: 'number', description: 'Version number of the folder', optional: true },
},
}
+64
View File
@@ -0,0 +1,64 @@
import type { GrafanaHealthCheckParams, GrafanaHealthCheckResponse } from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const getHealthTool: ToolConfig<GrafanaHealthCheckParams, GrafanaHealthCheckResponse> = {
id: 'grafana_get_health',
name: 'Grafana Get Health',
description: 'Check the health of the Grafana instance (version, database status)',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/health`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
commit: (data.commit as string) ?? '',
database: (data.database as string) ?? '',
version: (data.version as string) ?? '',
},
}
},
outputs: {
commit: { type: 'string', description: 'Git commit hash of the running Grafana build' },
database: { type: 'string', description: 'Database health status (e.g., ok)' },
version: { type: 'string', description: 'Grafana version' },
},
}
+56
View File
@@ -0,0 +1,56 @@
import { checkDataSourceHealthTool } from '@/tools/grafana/check_data_source_health'
import { createAlertRuleTool } from '@/tools/grafana/create_alert_rule'
import { createAnnotationTool } from '@/tools/grafana/create_annotation'
import { createContactPointTool } from '@/tools/grafana/create_contact_point'
import { createDashboardTool } from '@/tools/grafana/create_dashboard'
import { createFolderTool } from '@/tools/grafana/create_folder'
import { deleteAlertRuleTool } from '@/tools/grafana/delete_alert_rule'
import { deleteAnnotationTool } from '@/tools/grafana/delete_annotation'
import { deleteDashboardTool } from '@/tools/grafana/delete_dashboard'
import { deleteFolderTool } from '@/tools/grafana/delete_folder'
import { getAlertRuleTool } from '@/tools/grafana/get_alert_rule'
import { getDashboardTool } from '@/tools/grafana/get_dashboard'
import { getDataSourceTool } from '@/tools/grafana/get_data_source'
import { getFolderTool } from '@/tools/grafana/get_folder'
import { getHealthTool } from '@/tools/grafana/get_health'
import { listAlertRulesTool } from '@/tools/grafana/list_alert_rules'
import { listAnnotationsTool } from '@/tools/grafana/list_annotations'
import { listContactPointsTool } from '@/tools/grafana/list_contact_points'
import { listDashboardsTool } from '@/tools/grafana/list_dashboards'
import { listDataSourcesTool } from '@/tools/grafana/list_data_sources'
import { listFoldersTool } from '@/tools/grafana/list_folders'
import { updateAlertRuleTool } from '@/tools/grafana/update_alert_rule'
import { updateAnnotationTool } from '@/tools/grafana/update_annotation'
import { updateDashboardTool } from '@/tools/grafana/update_dashboard'
import { updateFolderTool } from '@/tools/grafana/update_folder'
export const grafanaGetDashboardTool = getDashboardTool
export const grafanaListDashboardsTool = listDashboardsTool
export const grafanaCreateDashboardTool = createDashboardTool
export const grafanaUpdateDashboardTool = updateDashboardTool
export const grafanaDeleteDashboardTool = deleteDashboardTool
export const grafanaListAlertRulesTool = listAlertRulesTool
export const grafanaGetAlertRuleTool = getAlertRuleTool
export const grafanaCreateAlertRuleTool = createAlertRuleTool
export const grafanaUpdateAlertRuleTool = updateAlertRuleTool
export const grafanaDeleteAlertRuleTool = deleteAlertRuleTool
export const grafanaListContactPointsTool = listContactPointsTool
export const grafanaCreateContactPointTool = createContactPointTool
export const grafanaCreateAnnotationTool = createAnnotationTool
export const grafanaListAnnotationsTool = listAnnotationsTool
export const grafanaUpdateAnnotationTool = updateAnnotationTool
export const grafanaDeleteAnnotationTool = deleteAnnotationTool
export const grafanaListDataSourcesTool = listDataSourcesTool
export const grafanaGetDataSourceTool = getDataSourceTool
export const grafanaCheckDataSourceHealthTool = checkDataSourceHealthTool
export const grafanaListFoldersTool = listFoldersTool
export const grafanaCreateFolderTool = createFolderTool
export const grafanaGetFolderTool = getFolderTool
export const grafanaUpdateFolderTool = updateFolderTool
export const grafanaDeleteFolderTool = deleteFolderTool
export const grafanaGetHealthTool = getHealthTool
@@ -0,0 +1,77 @@
import {
ALERT_RULE_OUTPUT_FIELDS,
type GrafanaListAlertRulesParams,
type GrafanaListAlertRulesResponse,
} from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig } from '@/tools/types'
export const listAlertRulesTool: ToolConfig<
GrafanaListAlertRulesParams,
GrafanaListAlertRulesResponse
> = {
id: 'grafana_list_alert_rules',
name: 'Grafana List Alert Rules',
description: 'List all alert rules in the Grafana instance',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
rules: Array.isArray(data)
? data.map((rule: Record<string, unknown>) => mapAlertRule(rule))
: [],
},
}
},
outputs: {
rules: {
type: 'array',
description: 'List of alert rules',
items: {
type: 'object',
properties: ALERT_RULE_OUTPUT_FIELDS,
},
},
},
}
+202
View File
@@ -0,0 +1,202 @@
import type {
GrafanaListAnnotationsParams,
GrafanaListAnnotationsResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const listAnnotationsTool: ToolConfig<
GrafanaListAnnotationsParams,
GrafanaListAnnotationsResponse
> = {
id: 'grafana_list_annotations',
name: 'Grafana List Annotations',
description: 'Query annotations by time range, dashboard, or tags',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
from: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Start time in epoch milliseconds (e.g., 1704067200000)',
},
to: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'End time in epoch milliseconds (e.g., 1704153600000)',
},
dashboardUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Dashboard UID to query annotations from (e.g., abc123def). Omit to query annotations across the organization.',
},
dashboardId: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Legacy numeric dashboard ID filter (prefer dashboardUid)',
},
panelId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Filter by panel ID (e.g., 1, 2)',
},
alertId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Filter by alert ID',
},
userId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Filter by ID of the user who created the annotation',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of tags to filter by',
},
type: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Filter by type (alert or annotation)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of annotations to return',
},
},
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const searchParams = new URLSearchParams()
if (params.from) searchParams.set('from', String(params.from))
if (params.to) searchParams.set('to', String(params.to))
if (params.dashboardUid) searchParams.set('dashboardUID', params.dashboardUid)
if (params.dashboardId) searchParams.set('dashboardId', String(params.dashboardId))
if (params.panelId) searchParams.set('panelId', String(params.panelId))
if (params.alertId) searchParams.set('alertId', String(params.alertId))
if (params.userId) searchParams.set('userId', String(params.userId))
if (params.tags) {
params.tags.split(',').forEach((t) => searchParams.append('tags', t.trim()))
}
if (params.type) searchParams.set('type', params.type)
if (params.limit) searchParams.set('limit', String(params.limit))
const queryString = searchParams.toString()
return `${baseUrl}/api/annotations${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
const rawAnnotations = Array.isArray(data) ? data.flat() : []
return {
success: true,
output: {
annotations: rawAnnotations.map((a: Record<string, unknown>) => ({
id: (a.id as number) ?? null,
alertId: (a.alertId as number) ?? null,
dashboardId: (a.dashboardId as number) ?? null,
dashboardUID: (a.dashboardUID as string) ?? null,
panelId: (a.panelId as number) ?? null,
userId: (a.userId as number) ?? null,
userName: (a.userName as string) ?? null,
newState: (a.newState as string) ?? null,
prevState: (a.prevState as string) ?? null,
time: (a.time as number) ?? null,
timeEnd: (a.timeEnd as number) ?? null,
text: (a.text as string) ?? null,
metric: (a.metric as string) ?? null,
tags: (a.tags as string[]) ?? [],
data: (a.data as Record<string, unknown>) ?? {},
})),
},
}
},
outputs: {
annotations: {
type: 'array',
description: 'List of annotations',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Annotation ID' },
alertId: { type: 'number', description: 'Associated alert ID (0 if not alert-driven)' },
dashboardId: { type: 'number', description: 'Dashboard ID', optional: true },
dashboardUID: { type: 'string', description: 'Dashboard UID', optional: true },
panelId: { type: 'number', description: 'Panel ID within the dashboard', optional: true },
userId: { type: 'number', description: 'ID of the user who created the annotation' },
userName: {
type: 'string',
description: 'Username of the user who created the annotation',
optional: true,
},
newState: {
type: 'string',
description: 'New alert state (alert annotations only)',
optional: true,
},
prevState: {
type: 'string',
description: 'Previous alert state (alert annotations only)',
optional: true,
},
time: { type: 'number', description: 'Start time in epoch ms' },
timeEnd: { type: 'number', description: 'End time in epoch ms', optional: true },
text: { type: 'string', description: 'Annotation text' },
metric: {
type: 'string',
description: 'Metric associated with the annotation',
optional: true,
},
tags: { type: 'array', items: { type: 'string' }, description: 'Annotation tags' },
data: { type: 'json', description: 'Additional annotation data object from Grafana' },
},
},
},
},
}
@@ -0,0 +1,107 @@
import type {
GrafanaListContactPointsParams,
GrafanaListContactPointsResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const listContactPointsTool: ToolConfig<
GrafanaListContactPointsParams,
GrafanaListContactPointsResponse
> = {
id: 'grafana_list_contact_points',
name: 'Grafana List Contact Points',
description: 'List all alert notification contact points',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter contact points by exact name match',
},
},
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const searchParams = new URLSearchParams()
if (params.name) searchParams.set('name', params.name)
const queryString = searchParams.toString()
return `${baseUrl}/api/v1/provisioning/contact-points${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
contactPoints: Array.isArray(data)
? data.map((cp: Record<string, unknown>) => ({
uid: (cp.uid as string) ?? null,
name: (cp.name as string) ?? null,
type: (cp.type as string) ?? null,
settings: (cp.settings as Record<string, unknown>) ?? {},
disableResolveMessage: (cp.disableResolveMessage as boolean) ?? false,
provenance: (cp.provenance as string) ?? '',
}))
: [],
},
}
},
outputs: {
contactPoints: {
type: 'array',
description: 'List of contact points',
items: {
type: 'object',
properties: {
uid: { type: 'string', description: 'Contact point UID' },
name: { type: 'string', description: 'Contact point name' },
type: { type: 'string', description: 'Notification type (email, slack, etc.)' },
settings: { type: 'object', description: 'Type-specific settings' },
disableResolveMessage: {
type: 'boolean',
description: 'Whether resolve messages are disabled',
},
provenance: {
type: 'string',
description: 'Provisioning source (empty if API-managed)',
},
},
},
},
},
}
+159
View File
@@ -0,0 +1,159 @@
import type {
GrafanaListDashboardsParams,
GrafanaListDashboardsResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const listDashboardsTool: ToolConfig<
GrafanaListDashboardsParams,
GrafanaListDashboardsResponse
> = {
id: 'grafana_list_dashboards',
name: 'Grafana List Dashboards',
description: 'Search and list all dashboards',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Search query to filter dashboards by title',
},
tag: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by tag (comma-separated for multiple tags)',
},
folderUIDs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by folder UIDs (comma-separated, e.g., abc123,def456)',
},
dashboardUIDs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by dashboard UIDs (comma-separated, e.g., abc123,def456)',
},
starred: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Only return starred dashboards',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of dashboards to return (default 1000)',
},
page: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Page number for pagination (1-based)',
},
},
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const searchParams = new URLSearchParams()
searchParams.set('type', 'dash-db')
if (params.query) searchParams.set('query', params.query)
if (params.tag) {
params.tag.split(',').forEach((t) => searchParams.append('tag', t.trim()))
}
if (params.folderUIDs) {
params.folderUIDs.split(',').forEach((uid) => searchParams.append('folderUIDs', uid.trim()))
}
if (params.dashboardUIDs) {
params.dashboardUIDs
.split(',')
.forEach((uid) => searchParams.append('dashboardUIDs', uid.trim()))
}
if (params.starred) searchParams.set('starred', 'true')
if (params.limit) searchParams.set('limit', String(params.limit))
if (params.page) searchParams.set('page', String(params.page))
return `${baseUrl}/api/search?${searchParams.toString()}`
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
dashboards: Array.isArray(data)
? data.map((d: Record<string, unknown>) => ({
id: (d.id as number) ?? null,
uid: (d.uid as string) ?? null,
title: (d.title as string) ?? null,
uri: (d.uri as string) ?? null,
url: (d.url as string) ?? null,
type: (d.type as string) ?? null,
tags: (d.tags as string[]) ?? [],
isStarred: (d.isStarred as boolean) ?? false,
folderId: (d.folderId as number) ?? null,
folderUid: (d.folderUid as string) ?? null,
folderTitle: (d.folderTitle as string) ?? null,
folderUrl: (d.folderUrl as string) ?? null,
}))
: [],
},
}
},
outputs: {
dashboards: {
type: 'array',
description: 'List of dashboard search results',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Dashboard ID' },
uid: { type: 'string', description: 'Dashboard UID' },
title: { type: 'string', description: 'Dashboard title' },
url: { type: 'string', description: 'Dashboard URL path' },
tags: { type: 'array', description: 'Dashboard tags' },
folderTitle: { type: 'string', description: 'Parent folder title' },
},
},
},
},
}
+125
View File
@@ -0,0 +1,125 @@
import type {
GrafanaListDataSourcesParams,
GrafanaListDataSourcesResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const listDataSourcesTool: ToolConfig<
GrafanaListDataSourcesParams,
GrafanaListDataSourcesResponse
> = {
id: 'grafana_list_data_sources',
name: 'Grafana List Data Sources',
description: 'List all data sources configured in Grafana',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/datasources`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
dataSources: Array.isArray(data)
? data.map((ds: Record<string, unknown>) => ({
id: (ds.id as number) ?? null,
uid: (ds.uid as string) ?? null,
orgId: (ds.orgId as number) ?? null,
name: (ds.name as string) ?? null,
type: (ds.type as string) ?? null,
typeLogoUrl: (ds.typeLogoUrl as string) ?? null,
access: (ds.access as string) ?? null,
url: (ds.url as string) ?? null,
user: (ds.user as string) ?? null,
database: (ds.database as string) ?? null,
basicAuth: (ds.basicAuth as boolean) ?? false,
basicAuthUser: (ds.basicAuthUser as string) ?? null,
withCredentials: (ds.withCredentials as boolean) ?? null,
isDefault: (ds.isDefault as boolean) ?? false,
jsonData: (ds.jsonData as Record<string, unknown>) ?? {},
secureJsonFields: (ds.secureJsonFields as Record<string, boolean>) ?? {},
version: (ds.version as number) ?? null,
readOnly: (ds.readOnly as boolean) ?? false,
}))
: [],
},
}
},
outputs: {
dataSources: {
type: 'array',
description: 'List of data sources',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Data source ID' },
uid: { type: 'string', description: 'Data source UID' },
orgId: { type: 'number', description: 'Organization ID' },
name: { type: 'string', description: 'Data source name' },
type: { type: 'string', description: 'Data source type (prometheus, mysql, etc.)' },
typeLogoUrl: { type: 'string', description: 'Logo URL for the data source type' },
access: { type: 'string', description: 'Access mode (proxy or direct)' },
url: { type: 'string', description: 'Data source URL' },
user: { type: 'string', description: 'Username used to connect' },
database: { type: 'string', description: 'Database name (if applicable)' },
basicAuth: { type: 'boolean', description: 'Whether basic auth is enabled' },
basicAuthUser: {
type: 'string',
description: 'Basic auth username',
optional: true,
},
withCredentials: {
type: 'boolean',
description: 'Whether to send credentials with cross-origin requests',
optional: true,
},
isDefault: { type: 'boolean', description: 'Whether this is the default data source' },
jsonData: { type: 'object', description: 'Type-specific JSON configuration' },
secureJsonFields: {
type: 'object',
description: 'Map of secure fields that are set (values are not returned)',
optional: true,
},
version: { type: 'number', description: 'Data source version', optional: true },
readOnly: { type: 'boolean', description: 'Whether the data source is read-only' },
},
},
},
},
}
+169
View File
@@ -0,0 +1,169 @@
import type { GrafanaListFoldersParams, GrafanaListFoldersResponse } from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const listFoldersTool: ToolConfig<GrafanaListFoldersParams, GrafanaListFoldersResponse> = {
id: 'grafana_list_folders',
name: 'Grafana List Folders',
description: 'List all folders in Grafana',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
limit: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of folders to return',
},
page: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Page number for pagination',
},
parentUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'List children of this folder UID (requires nested folders enabled)',
},
},
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const searchParams = new URLSearchParams()
if (params.limit) searchParams.set('limit', String(params.limit))
if (params.page) searchParams.set('page', String(params.page))
if (params.parentUid) searchParams.set('parentUid', params.parentUid.trim())
const queryString = searchParams.toString()
return `${baseUrl}/api/folders${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
folders: Array.isArray(data)
? data.map((f: Record<string, unknown>) => ({
id: (f.id as number) ?? null,
uid: (f.uid as string) ?? null,
title: (f.title as string) ?? null,
url: (f.url as string) ?? null,
parentUid: (f.parentUid as string) ?? null,
parents: (f.parents as { uid: string; title: string; url: string }[]) ?? [],
hasAcl: (f.hasAcl as boolean) ?? null,
canSave: (f.canSave as boolean) ?? null,
canEdit: (f.canEdit as boolean) ?? null,
canAdmin: (f.canAdmin as boolean) ?? null,
createdBy: (f.createdBy as string) ?? null,
created: (f.created as string) ?? null,
updatedBy: (f.updatedBy as string) ?? null,
updated: (f.updated as string) ?? null,
version: (f.version as number) ?? null,
}))
: [],
},
}
},
outputs: {
folders: {
type: 'array',
description: 'List of folders',
items: {
type: 'object',
properties: {
id: { type: 'number', description: 'Folder ID' },
uid: { type: 'string', description: 'Folder UID' },
title: { type: 'string', description: 'Folder title' },
url: { type: 'string', description: 'Folder URL path', optional: true },
parentUid: {
type: 'string',
description: 'Parent folder UID (nested folders only)',
optional: true,
},
parents: {
type: 'array',
description: 'Ancestor folder hierarchy (nested folders only)',
optional: true,
},
hasAcl: {
type: 'boolean',
description: 'Whether the folder has custom ACL permissions',
optional: true,
},
canSave: {
type: 'boolean',
description: 'Whether the current user can save the folder',
optional: true,
},
canEdit: {
type: 'boolean',
description: 'Whether the current user can edit the folder',
optional: true,
},
canAdmin: {
type: 'boolean',
description: 'Whether the current user has admin rights',
optional: true,
},
createdBy: {
type: 'string',
description: 'Username of who created the folder',
optional: true,
},
created: {
type: 'string',
description: 'Timestamp when the folder was created',
optional: true,
},
updatedBy: {
type: 'string',
description: 'Username of who last updated the folder',
optional: true,
},
updated: {
type: 'string',
description: 'Timestamp when the folder was last updated',
optional: true,
},
version: { type: 'number', description: 'Folder version number', optional: true },
},
},
},
},
}
+564
View File
@@ -0,0 +1,564 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
* Canonical output schema fields shared across alert rule tools.
*/
export const ALERT_RULE_OUTPUT_FIELDS: Record<string, OutputProperty> = {
id: { type: 'number', description: 'Alert rule numeric ID', optional: true },
uid: { type: 'string', description: 'Alert rule UID' },
title: { type: 'string', description: 'Alert rule title' },
condition: { type: 'string', description: 'RefId of the query used as the alert condition' },
data: { type: 'json', description: 'Alert rule query/expression data array' },
updated: { type: 'string', description: 'Last update timestamp', optional: true },
noDataState: { type: 'string', description: 'State when no data is returned' },
execErrState: { type: 'string', description: 'State on execution error' },
for: { type: 'string', description: 'Duration the condition must hold before firing' },
keepFiringFor: {
type: 'string',
description: 'Duration to keep firing after condition stops',
optional: true,
},
missingSeriesEvalsToResolve: {
type: 'number',
description: 'Number of missing series evaluations before resolving',
optional: true,
},
annotations: { type: 'json', description: 'Alert annotations' },
labels: { type: 'json', description: 'Alert labels' },
isPaused: { type: 'boolean', description: 'Whether the rule is paused' },
folderUID: { type: 'string', description: 'Parent folder UID' },
ruleGroup: { type: 'string', description: 'Rule group name' },
orgID: { type: 'number', description: 'Organization ID' },
provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' },
notification_settings: {
type: 'json',
description: 'Per-rule notification settings (overrides)',
optional: true,
},
record: {
type: 'json',
description: 'Recording rule configuration (recording rules only)',
optional: true,
},
}
interface GrafanaBaseParams {
apiKey: string
baseUrl: string
organizationId?: string
}
export interface GrafanaHealthCheckParams extends GrafanaBaseParams {}
export interface GrafanaHealthCheckResponse extends ToolResponse {
output: {
commit: string
database: string
version: string
}
}
export interface GrafanaDataSourceHealthParams extends GrafanaBaseParams {
dataSourceUid: string
}
export interface GrafanaDataSourceHealthResponse extends ToolResponse {
output: {
status: string
message: string
}
}
export interface GrafanaGetDashboardParams extends GrafanaBaseParams {
dashboardUid: string
}
interface GrafanaDashboardMeta {
type: string
canSave: boolean
canEdit: boolean
canAdmin: boolean
canStar: boolean
canDelete: boolean
slug: string
url: string
expires: string
created: string
updated: string
updatedBy: string
createdBy: string
version: number
hasAcl: boolean
isFolder: boolean
folderId: number
folderUid: string
folderTitle: string
folderUrl: string
provisioned: boolean
provisionedExternalId: string
}
interface GrafanaDashboard {
id: number
uid: string
title: string
tags: string[]
timezone: string
schemaVersion: number
version: number
refresh: string
panels: Record<string, unknown>[]
templating: Record<string, unknown>
annotations: Record<string, unknown>
time: {
from: string
to: string
}
}
export interface GrafanaGetDashboardResponse extends ToolResponse {
output: {
dashboard: GrafanaDashboard
meta: GrafanaDashboardMeta
}
}
export interface GrafanaListDashboardsParams extends GrafanaBaseParams {
query?: string
tag?: string
folderUIDs?: string
dashboardUIDs?: string
starred?: boolean
limit?: number
page?: number
}
interface GrafanaDashboardSearchResult {
id: number | null
uid: string | null
title: string | null
uri: string | null
url: string | null
type: string | null
tags: string[]
isStarred: boolean
folderId: number | null
folderUid: string | null
folderTitle: string | null
folderUrl: string | null
}
export interface GrafanaListDashboardsResponse extends ToolResponse {
output: {
dashboards: GrafanaDashboardSearchResult[]
}
}
export interface GrafanaCreateDashboardParams extends GrafanaBaseParams {
title: string
folderUid?: string
tags?: string
timezone?: string
refresh?: string
panels?: string
overwrite?: boolean
message?: string
}
export interface GrafanaCreateDashboardResponse extends ToolResponse {
output: {
id: number
uid: string
url: string
status: string
version: number
slug: string
}
}
export interface GrafanaUpdateDashboardParams extends GrafanaBaseParams {
dashboardUid: string
title?: string
folderUid?: string
tags?: string
timezone?: string
refresh?: string
panels?: string
overwrite?: boolean
message?: string
}
interface GrafanaUpdateDashboardResponse extends ToolResponse {
output: {
id: number
uid: string
url: string
status: string
version: number
slug: string
}
}
export interface GrafanaDeleteDashboardParams extends GrafanaBaseParams {
dashboardUid: string
}
export interface GrafanaDeleteDashboardResponse extends ToolResponse {
output: {
title: string
message: string
id: number
}
}
export interface GrafanaListAlertRulesParams extends GrafanaBaseParams {}
interface GrafanaAlertRule {
id: number | null
uid: string | null
title: string | null
condition: string | null
data: unknown[]
updated: string | null
noDataState: string | null
execErrState: string | null
for: string | null
keepFiringFor: string | null
missingSeriesEvalsToResolve: number | null
annotations: Record<string, string>
labels: Record<string, string>
isPaused: boolean
folderUID: string | null
ruleGroup: string | null
orgID: number | null
provenance: string
notification_settings: Record<string, unknown> | null
record: Record<string, unknown> | null
}
export interface GrafanaListAlertRulesResponse extends ToolResponse {
output: {
rules: GrafanaAlertRule[]
}
}
export interface GrafanaGetAlertRuleParams extends GrafanaBaseParams {
alertRuleUid: string
}
export interface GrafanaGetAlertRuleResponse extends ToolResponse {
output: GrafanaAlertRule
}
export interface GrafanaCreateAlertRuleParams extends GrafanaBaseParams {
title: string
folderUid: string
ruleGroup: string
condition?: string
data: string
forDuration?: string
noDataState?: string
execErrState?: string
annotations?: string
labels?: string
uid?: string
isPaused?: boolean
keepFiringFor?: string
missingSeriesEvalsToResolve?: number
notificationSettings?: string
record?: string
disableProvenance?: boolean
}
export interface GrafanaCreateAlertRuleResponse extends ToolResponse {
output: GrafanaAlertRule
}
export interface GrafanaUpdateAlertRuleParams extends GrafanaBaseParams {
alertRuleUid: string
title?: string
folderUid?: string
ruleGroup?: string
condition?: string
data?: string
forDuration?: string
noDataState?: string
execErrState?: string
annotations?: string
labels?: string
isPaused?: boolean
keepFiringFor?: string
missingSeriesEvalsToResolve?: number
notificationSettings?: string
record?: string
disableProvenance?: boolean
}
interface GrafanaUpdateAlertRuleResponse extends ToolResponse {
output: GrafanaAlertRule
}
export interface GrafanaDeleteAlertRuleParams extends GrafanaBaseParams {
alertRuleUid: string
}
export interface GrafanaDeleteAlertRuleResponse extends ToolResponse {
output: {
message: string
}
}
export interface GrafanaCreateAnnotationParams extends GrafanaBaseParams {
text: string
tags?: string
dashboardUid?: string
panelId?: number
time?: number
timeEnd?: number
}
interface GrafanaAnnotation {
id: number | null
alertId: number | null
dashboardId: number | null
dashboardUID: string | null
panelId: number | null
userId: number | null
userName: string | null
newState: string | null
prevState: string | null
time: number | null
timeEnd: number | null
text: string | null
metric: string | null
tags: string[]
data: Record<string, unknown>
}
export interface GrafanaCreateAnnotationResponse extends ToolResponse {
output: {
id: number
message: string
}
}
export interface GrafanaListAnnotationsParams extends GrafanaBaseParams {
from?: number
to?: number
dashboardId?: number
dashboardUid?: string
panelId?: number
alertId?: number
userId?: number
tags?: string
type?: string
limit?: number
}
export interface GrafanaListAnnotationsResponse extends ToolResponse {
output: {
annotations: GrafanaAnnotation[]
}
}
export interface GrafanaUpdateAnnotationParams extends GrafanaBaseParams {
annotationId: number
text?: string
tags?: string
time?: number
timeEnd?: number
}
export interface GrafanaUpdateAnnotationResponse extends ToolResponse {
output: {
id: number
message: string
}
}
export interface GrafanaDeleteAnnotationParams extends GrafanaBaseParams {
annotationId: number
}
export interface GrafanaDeleteAnnotationResponse extends ToolResponse {
output: {
message: string
}
}
export interface GrafanaListDataSourcesParams extends GrafanaBaseParams {}
interface GrafanaDataSource {
id: number
uid: string
orgId: number
name: string
type: string
typeLogoUrl: string
access: string
url: string
user: string
database: string
basicAuth: boolean
basicAuthUser?: string
withCredentials?: boolean
isDefault: boolean
jsonData: Record<string, unknown>
secureJsonFields?: Record<string, boolean>
version?: number
readOnly: boolean
}
export interface GrafanaListDataSourcesResponse extends ToolResponse {
output: {
dataSources: GrafanaDataSource[]
}
}
export interface GrafanaGetDataSourceParams extends GrafanaBaseParams {
dataSourceId: string
}
export interface GrafanaGetDataSourceResponse extends ToolResponse {
output: GrafanaDataSource
}
export interface GrafanaListFoldersParams extends GrafanaBaseParams {
limit?: number
page?: number
parentUid?: string
}
interface GrafanaFolderParent {
uid: string
title: string
url: string
}
interface GrafanaFolder {
id: number
uid: string
title: string
url?: string
hasAcl?: boolean
canSave?: boolean
canEdit?: boolean
canAdmin?: boolean
createdBy?: string
created?: string
updatedBy?: string
updated?: string
version?: number
parentUid?: string | null
parents?: GrafanaFolderParent[]
}
export interface GrafanaListFoldersResponse extends ToolResponse {
output: {
folders: GrafanaFolder[]
}
}
export interface GrafanaCreateFolderParams extends GrafanaBaseParams {
title: string
uid?: string
parentUid?: string
}
export interface GrafanaCreateFolderResponse extends ToolResponse {
output: GrafanaFolder
}
export interface GrafanaGetFolderParams extends GrafanaBaseParams {
folderUid: string
}
export interface GrafanaGetFolderResponse extends ToolResponse {
output: GrafanaFolder
}
export interface GrafanaUpdateFolderParams extends GrafanaBaseParams {
folderUid: string
title?: string
}
export interface GrafanaUpdateFolderResponse extends ToolResponse {
output: GrafanaFolder
}
export interface GrafanaDeleteFolderParams extends GrafanaBaseParams {
folderUid: string
forceDeleteRules?: boolean
}
export interface GrafanaDeleteFolderResponse extends ToolResponse {
output: {
uid: string
message: string
}
}
export interface GrafanaListContactPointsParams extends GrafanaBaseParams {
name?: string
}
interface GrafanaContactPoint {
uid: string
name: string
type: string
settings: Record<string, unknown>
disableResolveMessage: boolean
provenance: string
}
export interface GrafanaListContactPointsResponse extends ToolResponse {
output: {
contactPoints: GrafanaContactPoint[]
}
}
export interface GrafanaCreateContactPointParams extends GrafanaBaseParams {
name: string
type: string
settings: string
disableResolveMessage?: boolean
disableProvenance?: boolean
}
export interface GrafanaCreateContactPointResponse extends ToolResponse {
output: {
uid: string
name: string
type: string
settings: Record<string, unknown>
disableResolveMessage: boolean
provenance: string
}
}
export type GrafanaResponse =
| GrafanaHealthCheckResponse
| GrafanaDataSourceHealthResponse
| GrafanaGetDashboardResponse
| GrafanaListDashboardsResponse
| GrafanaCreateDashboardResponse
| GrafanaUpdateDashboardResponse
| GrafanaDeleteDashboardResponse
| GrafanaListAlertRulesResponse
| GrafanaGetAlertRuleResponse
| GrafanaCreateAlertRuleResponse
| GrafanaUpdateAlertRuleResponse
| GrafanaDeleteAlertRuleResponse
| GrafanaCreateAnnotationResponse
| GrafanaListAnnotationsResponse
| GrafanaUpdateAnnotationResponse
| GrafanaDeleteAnnotationResponse
| GrafanaListDataSourcesResponse
| GrafanaGetDataSourceResponse
| GrafanaListFoldersResponse
| GrafanaCreateFolderResponse
| GrafanaGetFolderResponse
| GrafanaUpdateFolderResponse
| GrafanaDeleteFolderResponse
| GrafanaListContactPointsResponse
| GrafanaCreateContactPointResponse
+171
View File
@@ -0,0 +1,171 @@
import { ALERT_RULE_OUTPUT_FIELDS, type GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolResponse> = {
id: 'grafana_update_alert_rule',
name: 'Grafana Update Alert Rule',
description: 'Update an existing alert rule. Fetches the current rule and merges your changes.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
alertRuleUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the alert rule to update',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the alert rule',
},
folderUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New folder UID to move the alert to (e.g., folder-abc123)',
},
ruleGroup: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New rule group name',
},
condition: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New condition refId',
},
data: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New JSON array of query/expression data objects',
},
forDuration: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Duration to wait before firing (e.g., 5m, 1h)',
},
noDataState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'State when no data is returned (NoData, Alerting, OK)',
},
execErrState: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'State on execution error (Error, Alerting, OK)',
},
annotations: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object of annotations',
},
labels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object of labels',
},
isPaused: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether the rule is paused',
},
keepFiringFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Duration to keep firing after the condition stops (e.g., 5m)',
},
missingSeriesEvalsToResolve: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Number of missing series evaluations before resolving',
},
notificationSettings: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'JSON object of per-rule notification settings (overrides)',
},
record: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object configuring this as a recording rule',
},
disableProvenance: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI',
},
},
request: {
url: () => '/api/tools/grafana/update_alert_rule',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
baseUrl: params.baseUrl,
organizationId: params.organizationId,
alertRuleUid: params.alertRuleUid,
title: params.title,
folderUid: params.folderUid,
ruleGroup: params.ruleGroup,
condition: params.condition,
data: params.data,
forDuration: params.forDuration,
noDataState: params.noDataState,
execErrState: params.execErrState,
annotations: params.annotations,
labels: params.labels,
isPaused: params.isPaused,
keepFiringFor: params.keepFiringFor,
missingSeriesEvalsToResolve: params.missingSeriesEvalsToResolve,
notificationSettings: params.notificationSettings,
record: params.record,
disableProvenance: params.disableProvenance,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output ?? {},
...(data.error ? { error: data.error } : {}),
}
},
outputs: ALERT_RULE_OUTPUT_FIELDS,
}
+120
View File
@@ -0,0 +1,120 @@
import type {
GrafanaUpdateAnnotationParams,
GrafanaUpdateAnnotationResponse,
} from '@/tools/grafana/types'
import type { ToolConfig } from '@/tools/types'
export const updateAnnotationTool: ToolConfig<
GrafanaUpdateAnnotationParams,
GrafanaUpdateAnnotationResponse
> = {
id: 'grafana_update_annotation',
name: 'Grafana Update Annotation',
description: 'Update an existing annotation',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
annotationId: {
type: 'number',
required: true,
visibility: 'user-or-llm',
description: 'The ID of the annotation to update',
},
text: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New text content for the annotation (PATCH supports partial updates)',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of new tags',
},
time: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'New start time in epoch milliseconds (e.g., 1704067200000)',
},
timeEnd: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'New end time in epoch milliseconds (e.g., 1704153600000)',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/annotations/${params.annotationId}`,
method: 'PATCH',
headers: (params) => {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
return headers
},
body: (params) => {
const body: Record<string, unknown> = {}
if (params.text !== undefined) body.text = params.text
if (params.time) body.time = params.time
if (params.timeEnd) body.timeEnd = params.timeEnd
if (params.tags) {
body.tags = params.tags
.split(',')
.map((t) => t.trim())
.filter((t) => t)
}
return body
},
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
id: data.id || 0,
message: data.message || 'Annotation updated successfully',
},
}
},
outputs: {
id: {
type: 'number',
description: 'The ID of the updated annotation',
},
message: {
type: 'string',
description: 'Confirmation message',
},
},
}
+142
View File
@@ -0,0 +1,142 @@
import type { GrafanaUpdateDashboardParams } from '@/tools/grafana/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const updateDashboardTool: ToolConfig<GrafanaUpdateDashboardParams, ToolResponse> = {
id: 'grafana_update_dashboard',
name: 'Grafana Update Dashboard',
description:
'Update an existing dashboard. Fetches the current dashboard and merges your changes.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
dashboardUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the dashboard to update (e.g., abc123def)',
},
title: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New title for the dashboard',
},
folderUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'New folder UID to move the dashboard to (e.g., folder-abc123)',
},
tags: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Comma-separated list of new tags',
},
timezone: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Dashboard timezone (e.g., browser, utc)',
},
refresh: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'Auto-refresh interval (e.g., 5s, 1m, 5m)',
},
panels: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON array of panel configurations',
},
overwrite: {
type: 'boolean',
required: false,
visibility: 'user-only',
description:
'Overwrite even if there is a version conflict (defaults to false to surface 412 conflicts)',
},
message: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Commit message for this version',
},
},
request: {
url: () => '/api/tools/grafana/update_dashboard',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
baseUrl: params.baseUrl,
organizationId: params.organizationId,
dashboardUid: params.dashboardUid,
title: params.title,
folderUid: params.folderUid,
tags: params.tags,
timezone: params.timezone,
refresh: params.refresh,
panels: params.panels,
overwrite: params.overwrite,
message: params.message,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output ?? {},
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: {
type: 'number',
description: 'The numeric ID of the updated dashboard',
},
uid: {
type: 'string',
description: 'The UID of the updated dashboard',
},
url: {
type: 'string',
description: 'The URL path to the dashboard',
},
status: {
type: 'string',
description: 'Status of the operation (success)',
},
version: {
type: 'number',
description: 'The new version number of the dashboard',
},
slug: {
type: 'string',
description: 'URL-friendly slug of the dashboard',
},
},
}
+122
View File
@@ -0,0 +1,122 @@
import type { GrafanaUpdateFolderParams } from '@/tools/grafana/types'
import type { ToolConfig, ToolResponse } from '@/tools/types'
export const updateFolderTool: ToolConfig<GrafanaUpdateFolderParams, ToolResponse> = {
id: 'grafana_update_folder',
name: 'Grafana Update Folder',
description: 'Update (rename) a folder. Fetches the current folder and merges your changes.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana Service Account Token',
},
baseUrl: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Grafana instance URL (e.g., https://your-grafana.com)',
},
organizationId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
folderUid: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The UID of the folder to update (e.g., folder-abc123)',
},
title: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'New title for the folder',
},
},
request: {
url: () => '/api/tools/grafana/update_folder',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
apiKey: params.apiKey,
baseUrl: params.baseUrl,
organizationId: params.organizationId,
folderUid: params.folderUid,
title: params.title,
}),
},
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: data.success ?? true,
output: data.output ?? {},
...(data.error ? { error: data.error } : {}),
}
},
outputs: {
id: { type: 'number', description: 'The numeric ID of the folder' },
uid: { type: 'string', description: 'The UID of the folder' },
title: { type: 'string', description: 'The updated title of the folder' },
url: { type: 'string', description: 'The URL path to the folder', optional: true },
parentUid: {
type: 'string',
description: 'Parent folder UID (nested folders only)',
optional: true,
},
parents: {
type: 'array',
description: 'Ancestor folder hierarchy (nested folders only)',
optional: true,
},
hasAcl: {
type: 'boolean',
description: 'Whether the folder has custom ACL permissions',
optional: true,
},
canSave: {
type: 'boolean',
description: 'Whether the current user can save the folder',
optional: true,
},
canEdit: {
type: 'boolean',
description: 'Whether the current user can edit the folder',
optional: true,
},
canAdmin: {
type: 'boolean',
description: 'Whether the current user has admin rights on the folder',
optional: true,
},
createdBy: {
type: 'string',
description: 'Username of who created the folder',
optional: true,
},
created: {
type: 'string',
description: 'Timestamp when the folder was created',
optional: true,
},
updatedBy: {
type: 'string',
description: 'Username of who last updated the folder',
optional: true,
},
updated: {
type: 'string',
description: 'Timestamp when the folder was last updated',
optional: true,
},
version: { type: 'number', description: 'Version number of the folder', optional: true },
},
}
+31
View File
@@ -0,0 +1,31 @@
/**
* Map a raw Grafana ProvisionedAlertRule JSON object to the canonical output shape
* shared across list/get/create/update alert rule tools.
*/
export function mapAlertRule(rule: Record<string, unknown>) {
return {
id: (rule.id as number) ?? null,
uid: (rule.uid as string) ?? null,
title: (rule.title as string) ?? null,
condition: (rule.condition as string) ?? null,
data: (rule.data as unknown[]) ?? [],
updated: (rule.updated as string) ?? null,
noDataState: (rule.noDataState as string) ?? null,
execErrState: (rule.execErrState as string) ?? null,
for: (rule.for as string) ?? null,
keepFiringFor: (rule.keep_firing_for as string) ?? (rule.keepFiringFor as string) ?? null,
missingSeriesEvalsToResolve:
(rule.missingSeriesEvalsToResolve as number) ??
(rule.missing_series_evals_to_resolve as number) ??
null,
annotations: (rule.annotations as Record<string, string>) ?? {},
labels: (rule.labels as Record<string, string>) ?? {},
isPaused: (rule.isPaused as boolean) ?? false,
folderUID: (rule.folderUID as string) ?? null,
ruleGroup: (rule.ruleGroup as string) ?? null,
orgID: (rule.orgID as number) ?? (rule.orgId as number) ?? null,
provenance: (rule.provenance as string) ?? '',
notification_settings: (rule.notification_settings as Record<string, unknown>) ?? null,
record: (rule.record as Record<string, unknown>) ?? null,
}
}