feat(grafana): validate integration and add folder, health, and contact-point tools (#5082)

* feat(grafana): validate integration and add folder, health, and contact-point tools

- Require alert-rule title/ruleGroup/data in the block (create would 400 without them)
- Trim UID path params across dashboard and alert-rule tools to avoid copy-paste 404s
- Use Grafana brand color for the block background
- Surface previously-unsettable list params (limit, starred, annotation type)
- Add get/update/delete folder, check data source health, get health, and create contact point tools
- Strip non-TSDoc comments; regenerate docs and integrations.json

* fix(grafana): scope block param remaps per operation to prevent cross-operation leaks
This commit is contained in:
Waleed
2026-06-15 19:09:16 -07:00
committed by GitHub
parent ed31edf068
commit 324189ed39
21 changed files with 1226 additions and 107 deletions
+5 -6
View File
@@ -2936,12 +2936,11 @@ export function ClickHouseIcon(props: SVGProps<SVGSVGElement>) {
export function MicrosoftIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 23 23' {...props}>
<path fill='#f3f3f3' d='M0 0h23v23H0z' />
<path fill='#f35325' d='M1 1h10v10H1z' />
<path fill='#81bc06' d='M12 1h10v10H12z' />
<path fill='#05a6f0' d='M1 12h10v10H1z' />
<path fill='#ffba08' d='M12 12h10v10H12z' />
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 109 109' {...props}>
<polygon fill='#F1511B' points='51.9,51.9 0,51.9 0,0 51.9,0' />
<polygon fill='#80CC28' points='109.3,51.9 57.3,51.9 57.3,0 109.3,0' />
<polygon fill='#00ADEF' points='51.9,109.3 0,109.3 0,57.4 51.9,57.4' />
<polygon fill='#FBBC09' points='109.3,109.3 57.3,109.3 57.3,57.4 109.3,57.4' />
</svg>
)
}
@@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="grafana"
color="#FFFFFF"
color="#F46800"
/>
{/* MANUAL-CONTENT-START:intro */}
@@ -401,6 +401,34 @@ List all alert notification contact points
| ↳ `disableResolveMessage` | boolean | Whether resolve messages are disabled |
| ↳ `provenance` | string | Provisioning source \(empty if API-managed\) |
### `grafana_create_contact_point`
Create a notification contact point (e.g., Slack, email, PagerDuty)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Grafana Service Account Token |
| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) |
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `name` | string | Yes | Name of the contact point \(groups receivers shown in the UI\) |
| `type` | string | Yes | Receiver type \(e.g., slack, email, pagerduty, webhook\) |
| `settings` | string | Yes | JSON object of type-specific settings \(e.g., \{"addresses":"a@b.com"\} for email, \{"url":"..."\} for slack\) |
| `disableResolveMessage` | boolean | No | Do not send a notification when the alert resolves |
| `disableProvenance` | boolean | No | Set X-Disable-Provenance header so the contact point remains editable in the UI |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `uid` | string | UID of the created contact point |
| `name` | string | Name of the contact point |
| `type` | string | Receiver type |
| `settings` | json | Type-specific settings |
| `disableResolveMessage` | boolean | Whether resolve notifications are suppressed |
| `provenance` | string | Provisioning source \(empty if API-managed\) |
### `grafana_create_annotation`
Create an annotation on a dashboard or as a global annotation
@@ -584,6 +612,26 @@ Get a data source by its ID or UID
| `version` | number | Data source version |
| `readOnly` | boolean | Whether the data source is read-only |
### `grafana_check_data_source_health`
Test connectivity to a data source by its UID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Grafana Service Account Token |
| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) |
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `dataSourceUid` | string | Yes | The UID of the data source to health-check \(e.g., P1234AB5678\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `status` | string | Health status of the data source \(e.g., OK\) |
| `message` | string | Detailed health message from the data source |
### `grafana_list_folders`
List all folders in Grafana
@@ -655,4 +703,112 @@ Create a new folder in Grafana
| `updated` | string | Timestamp when the folder was last updated |
| `version` | number | Version number of the folder |
### `grafana_get_folder`
Get a folder by its UID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Grafana Service Account Token |
| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) |
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `folderUid` | string | Yes | The UID of the folder to retrieve \(e.g., folder-abc123\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | number | The numeric ID of the folder |
| `uid` | string | The UID of the folder |
| `title` | string | The title of the folder |
| `url` | string | The URL path to the folder |
| `parentUid` | string | Parent folder UID \(nested folders only\) |
| `parents` | array | Ancestor folder hierarchy \(nested folders only\) |
| `hasAcl` | boolean | Whether the folder has custom ACL permissions |
| `canSave` | boolean | Whether the current user can save the folder |
| `canEdit` | boolean | Whether the current user can edit the folder |
| `canAdmin` | boolean | Whether the current user has admin rights on the folder |
| `createdBy` | string | Username of who created the folder |
| `created` | string | Timestamp when the folder was created |
| `updatedBy` | string | Username of who last updated the folder |
| `updated` | string | Timestamp when the folder was last updated |
| `version` | number | Version number of the folder |
### `grafana_update_folder`
Update (rename) a folder. Fetches the current folder and merges your changes.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Grafana Service Account Token |
| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) |
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `folderUid` | string | Yes | The UID of the folder to update \(e.g., folder-abc123\) |
| `title` | string | Yes | New title for the folder |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | number | The numeric ID of the folder |
| `uid` | string | The UID of the folder |
| `title` | string | The updated title of the folder |
| `url` | string | The URL path to the folder |
| `parentUid` | string | Parent folder UID \(nested folders only\) |
| `parents` | array | Ancestor folder hierarchy \(nested folders only\) |
| `hasAcl` | boolean | Whether the folder has custom ACL permissions |
| `canSave` | boolean | Whether the current user can save the folder |
| `canEdit` | boolean | Whether the current user can edit the folder |
| `canAdmin` | boolean | Whether the current user has admin rights on the folder |
| `createdBy` | string | Username of who created the folder |
| `created` | string | Timestamp when the folder was created |
| `updatedBy` | string | Username of who last updated the folder |
| `updated` | string | Timestamp when the folder was last updated |
| `version` | number | Version number of the folder |
### `grafana_delete_folder`
Delete a folder by its UID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Grafana Service Account Token |
| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) |
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `folderUid` | string | Yes | The UID of the folder to delete \(e.g., folder-abc123\) |
| `forceDeleteRules` | boolean | No | Delete any alert rules stored in the folder along with it \(default false\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `uid` | string | The UID of the deleted folder |
| `message` | string | Confirmation message |
### `grafana_get_health`
Check the health of the Grafana instance (version, database status)
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `apiKey` | string | Yes | Grafana Service Account Token |
| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) |
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `commit` | string | Git commit hash of the running Grafana build |
| `database` | string | Database health status \(e.g., ok\) |
| `version` | string | Grafana version |
+254 -50
View File
@@ -13,44 +13,43 @@ export const GrafanaBlock: BlockConfig<GrafanaResponse> = {
docsLink: 'https://docs.sim.ai/integrations/grafana',
category: 'tools',
integrationType: IntegrationType.Observability,
bgColor: '#FFFFFF',
bgColor: '#F46800',
icon: GrafanaIcon,
subBlocks: [
// Operation dropdown
{
id: 'operation',
title: 'Operation',
type: 'dropdown',
options: [
// Dashboards
{ label: 'List Dashboards', id: 'grafana_list_dashboards' },
{ label: 'Get Dashboard', id: 'grafana_get_dashboard' },
{ label: 'Create Dashboard', id: 'grafana_create_dashboard' },
{ label: 'Update Dashboard', id: 'grafana_update_dashboard' },
{ label: 'Delete Dashboard', id: 'grafana_delete_dashboard' },
// Alerts
{ label: 'List Alert Rules', id: 'grafana_list_alert_rules' },
{ label: 'Get Alert Rule', id: 'grafana_get_alert_rule' },
{ label: 'Create Alert Rule', id: 'grafana_create_alert_rule' },
{ label: 'Update Alert Rule', id: 'grafana_update_alert_rule' },
{ label: 'Delete Alert Rule', id: 'grafana_delete_alert_rule' },
{ label: 'List Contact Points', id: 'grafana_list_contact_points' },
// Annotations
{ label: 'Create Contact Point', id: 'grafana_create_contact_point' },
{ label: 'Create Annotation', id: 'grafana_create_annotation' },
{ label: 'List Annotations', id: 'grafana_list_annotations' },
{ label: 'Update Annotation', id: 'grafana_update_annotation' },
{ label: 'Delete Annotation', id: 'grafana_delete_annotation' },
// Data Sources
{ label: 'List Data Sources', id: 'grafana_list_data_sources' },
{ label: 'Get Data Source', id: 'grafana_get_data_source' },
// Folders
{ label: 'Check Data Source Health', id: 'grafana_check_data_source_health' },
{ label: 'List Folders', id: 'grafana_list_folders' },
{ label: 'Create Folder', id: 'grafana_create_folder' },
{ label: 'Get Folder', id: 'grafana_get_folder' },
{ label: 'Update Folder', id: 'grafana_update_folder' },
{ label: 'Delete Folder', id: 'grafana_delete_folder' },
{ label: 'Get Health', id: 'grafana_get_health' },
],
value: () => 'grafana_list_dashboards',
},
// Base Configuration (common to all operations)
{
id: 'baseUrl',
title: 'Grafana URL',
@@ -73,7 +72,6 @@ export const GrafanaBlock: BlockConfig<GrafanaResponse> = {
placeholder: 'Optional - for multi-org instances',
},
// Data Source operations
{
id: 'dataSourceId',
title: 'Data Source ID',
@@ -85,8 +83,18 @@ export const GrafanaBlock: BlockConfig<GrafanaResponse> = {
value: 'grafana_get_data_source',
},
},
{
id: 'dataSourceUid',
title: 'Data Source UID',
type: 'short-input',
placeholder: 'Enter data source UID',
required: true,
condition: {
field: 'operation',
value: 'grafana_check_data_source_health',
},
},
// Dashboard operations
{
id: 'dashboardUid',
title: 'Dashboard UID',
@@ -152,8 +160,25 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`,
value: ['grafana_list_dashboards', 'grafana_list_folders'],
},
},
{
id: 'limit',
title: 'Limit',
type: 'short-input',
placeholder: 'Maximum results to return',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_list_dashboards', 'grafana_list_folders', 'grafana_list_annotations'],
},
},
{
id: 'starred',
title: 'Only Starred',
type: 'switch',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_dashboards' },
},
// Create/Update Dashboard
{
id: 'title',
title: 'Dashboard Title',
@@ -268,7 +293,6 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
},
},
// Alert Rule operations
{
id: 'alertRuleUid',
title: 'Alert Rule UID',
@@ -285,6 +309,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
title: 'Alert Title',
type: 'short-input',
placeholder: 'Enter alert rule name',
required: { field: 'operation', value: 'grafana_create_alert_rule' },
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
@@ -311,6 +336,7 @@ Return ONLY the alert title - no explanations, no quotes, no extra text.`,
title: 'Rule Group',
type: 'short-input',
placeholder: 'Enter rule group name',
required: { field: 'operation', value: 'grafana_create_alert_rule' },
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
@@ -331,6 +357,7 @@ Return ONLY the alert title - no explanations, no quotes, no extra text.`,
title: 'Query Data (JSON)',
type: 'long-input',
placeholder: 'JSON array of query/expression data objects',
required: { field: 'operation', value: 'grafana_create_alert_rule' },
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
@@ -509,11 +536,14 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
value: [
'grafana_create_alert_rule',
'grafana_update_alert_rule',
'grafana_create_contact_point',
],
},
},
// Annotation operations
{
id: 'text',
title: 'Annotation Text',
@@ -591,6 +621,19 @@ Return ONLY the annotation text - no explanations, no quotes, no extra text.`,
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_annotations' },
},
{
id: 'annotationType',
title: 'Type',
type: 'dropdown',
options: [
{ label: 'All', id: '' },
{ label: 'Alert', id: 'alert' },
{ label: 'Annotation', id: 'annotation' },
],
value: () => '',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_annotations' },
},
{
id: 'time',
title: 'Time (epoch ms)',
@@ -689,7 +732,6 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`,
},
},
// Folder operations
{
id: 'folderTitle',
title: 'Folder Title',
@@ -737,6 +779,33 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_folders' },
},
{
id: 'manageFolderUid',
title: 'Folder UID',
type: 'short-input',
placeholder: 'Enter folder UID',
required: true,
condition: {
field: 'operation',
value: ['grafana_get_folder', 'grafana_update_folder', 'grafana_delete_folder'],
},
},
{
id: 'updateFolderTitle',
title: 'New Folder Title',
type: 'short-input',
placeholder: 'Enter new folder title',
required: { field: 'operation', value: 'grafana_update_folder' },
condition: { field: 'operation', value: 'grafana_update_folder' },
},
{
id: 'forceDeleteRules',
title: 'Force Delete Alert Rules',
type: 'switch',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_delete_folder' },
},
{
id: 'contactPointName',
title: 'Contact Point Name',
@@ -745,6 +814,60 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_contact_points' },
},
{
id: 'contactPointNameNew',
title: 'Contact Point Name',
type: 'short-input',
placeholder: 'Enter contact point name',
required: true,
condition: { field: 'operation', value: 'grafana_create_contact_point' },
},
{
id: 'contactPointType',
title: 'Type',
type: 'dropdown',
options: [
{ label: 'Slack', id: 'slack' },
{ label: 'Email', id: 'email' },
{ label: 'PagerDuty', id: 'pagerduty' },
{ label: 'Webhook', id: 'webhook' },
{ label: 'Microsoft Teams', id: 'teams' },
{ label: 'Opsgenie', id: 'opsgenie' },
{ label: 'Discord', id: 'discord' },
],
value: () => 'slack',
required: true,
condition: { field: 'operation', value: 'grafana_create_contact_point' },
},
{
id: 'contactPointSettings',
title: 'Settings (JSON)',
type: 'long-input',
placeholder: 'JSON object of receiver settings (e.g., {"url":"https://hooks.slack.com/..."})',
required: true,
condition: { field: 'operation', value: 'grafana_create_contact_point' },
wandConfig: {
enabled: true,
prompt: `Generate a Grafana contact point settings JSON object based on the user's description and receiver type.
Examples by type:
- slack -> {"recipient":"#alerts","url":"https://hooks.slack.com/services/XXX"}
- email -> {"addresses":"oncall@example.com;sre@example.com"}
- pagerduty -> {"integrationKey":"YOUR_INTEGRATION_KEY","severity":"critical"}
- webhook -> {"url":"https://example.com/hook","httpMethod":"POST"}
Return ONLY the JSON object - no explanations, no markdown, no extra text.`,
placeholder: 'Describe the notification target...',
generationType: 'json-object',
},
},
{
id: 'disableResolveMessage',
title: 'Disable Resolve Message',
type: 'switch',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_create_contact_point' },
},
],
tools: {
access: [
@@ -759,39 +882,96 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
'grafana_update_alert_rule',
'grafana_delete_alert_rule',
'grafana_list_contact_points',
'grafana_create_contact_point',
'grafana_create_annotation',
'grafana_list_annotations',
'grafana_update_annotation',
'grafana_delete_annotation',
'grafana_list_data_sources',
'grafana_get_data_source',
'grafana_check_data_source_health',
'grafana_list_folders',
'grafana_create_folder',
'grafana_get_folder',
'grafana_update_folder',
'grafana_delete_folder',
'grafana_get_health',
],
config: {
tool: (params) => params.operation,
params: (params) => {
const result: Record<string, unknown> = {}
if (params.alertTitle) result.title = params.alertTitle
if (params.folderTitle) result.title = params.folderTitle
if (params.folderUidNew) result.uid = params.folderUidNew
if (params.alertRuleUidNew) result.uid = params.alertRuleUidNew
if (params.parentUidNew) result.parentUid = params.parentUidNew
if (params.parentUidList) result.parentUid = params.parentUidList
if (params.contactPointName) result.name = params.contactPointName
if (params.annotationTags) result.tags = params.annotationTags
if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid
if (params.panelId) result.panelId = Number(params.panelId)
if (params.annotationId) result.annotationId = Number(params.annotationId)
if (params.alertId) result.alertId = Number(params.alertId)
if (params.userId) result.userId = Number(params.userId)
if (params.time) result.time = Number(params.time)
if (params.timeEnd) result.timeEnd = Number(params.timeEnd)
if (params.from) result.from = Number(params.from)
if (params.to) result.to = Number(params.to)
if (params.page) result.page = Number(params.page)
if (params.missingSeriesEvalsToResolve) {
result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve)
switch (params.operation) {
case 'grafana_list_dashboards':
if (params.page) result.page = Number(params.page)
if (params.limit) result.limit = Number(params.limit)
break
case 'grafana_create_alert_rule':
if (params.alertTitle) result.title = params.alertTitle
if (params.alertRuleUidNew) result.uid = params.alertRuleUidNew
if (params.missingSeriesEvalsToResolve) {
result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve)
}
break
case 'grafana_update_alert_rule':
if (params.alertTitle) result.title = params.alertTitle
if (params.missingSeriesEvalsToResolve) {
result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve)
}
break
case 'grafana_list_contact_points':
if (params.contactPointName) result.name = params.contactPointName
break
case 'grafana_create_contact_point':
if (params.contactPointNameNew) result.name = params.contactPointNameNew
if (params.contactPointType) result.type = params.contactPointType
if (params.contactPointSettings) result.settings = params.contactPointSettings
break
case 'grafana_create_annotation':
if (params.annotationTags) result.tags = params.annotationTags
if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid
if (params.panelId) result.panelId = Number(params.panelId)
if (params.time) result.time = Number(params.time)
if (params.timeEnd) result.timeEnd = Number(params.timeEnd)
break
case 'grafana_update_annotation':
if (params.annotationTags) result.tags = params.annotationTags
if (params.annotationId) result.annotationId = Number(params.annotationId)
if (params.time) result.time = Number(params.time)
if (params.timeEnd) result.timeEnd = Number(params.timeEnd)
break
case 'grafana_delete_annotation':
if (params.annotationId) result.annotationId = Number(params.annotationId)
break
case 'grafana_list_annotations':
if (params.annotationTags) result.tags = params.annotationTags
if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid
if (params.annotationType) result.type = params.annotationType
if (params.panelId) result.panelId = Number(params.panelId)
if (params.alertId) result.alertId = Number(params.alertId)
if (params.userId) result.userId = Number(params.userId)
if (params.from) result.from = Number(params.from)
if (params.to) result.to = Number(params.to)
if (params.limit) result.limit = Number(params.limit)
break
case 'grafana_list_folders':
if (params.parentUidList) result.parentUid = params.parentUidList
if (params.page) result.page = Number(params.page)
if (params.limit) result.limit = Number(params.limit)
break
case 'grafana_create_folder':
if (params.folderTitle) result.title = params.folderTitle
if (params.folderUidNew) result.uid = params.folderUidNew
if (params.parentUidNew) result.parentUid = params.parentUidNew
break
case 'grafana_get_folder':
case 'grafana_delete_folder':
if (params.manageFolderUid) result.folderUid = params.manageFolderUid
break
case 'grafana_update_folder':
if (params.manageFolderUid) result.folderUid = params.manageFolderUid
if (params.updateFolderTitle) result.title = params.updateFolderTitle
break
}
return result
},
@@ -802,7 +982,6 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
baseUrl: { type: 'string', description: 'Grafana instance URL' },
apiKey: { type: 'string', description: 'Service Account Token' },
organizationId: { type: 'string', description: 'Organization ID (optional)' },
// Dashboard inputs
dashboardUid: { type: 'string', description: 'Dashboard UID' },
title: { type: 'string', description: 'Dashboard or folder title' },
folderUid: { type: 'string', description: 'Folder UID' },
@@ -817,7 +996,8 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
},
dashboardUIDs: { type: 'string', description: 'Filter by dashboard UIDs (comma-separated)' },
page: { type: 'number', description: 'Page number for pagination' },
// Alert inputs
limit: { type: 'number', description: 'Maximum number of results to return' },
starred: { type: 'boolean', description: 'Only return starred dashboards' },
alertRuleUid: { type: 'string', description: 'Alert rule UID' },
alertRuleUidNew: { type: 'string', description: 'Custom UID for newly created alert rule' },
alertTitle: { type: 'string', description: 'Alert rule title' },
@@ -848,7 +1028,6 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
annotations: { type: 'string', description: 'JSON of alert annotations' },
labels: { type: 'string', description: 'JSON of alert labels' },
overwrite: { type: 'boolean', description: 'Overwrite existing dashboard on version conflict' },
// Annotation inputs
text: { type: 'string', description: 'Annotation text' },
annotationId: { type: 'number', description: 'Annotation ID' },
annotationTags: { type: 'string', description: 'Annotation tags (comma-separated)' },
@@ -860,30 +1039,52 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
to: { type: 'number', description: 'Filter to time' },
alertId: { type: 'number', description: 'Filter annotations by alert ID' },
userId: { type: 'number', description: 'Filter annotations by creator user ID' },
// Folder inputs
annotationType: {
type: 'string',
description: 'Filter annotations by type (alert or annotation)',
},
folderTitle: { type: 'string', description: 'Folder title for newly created folder' },
folderUidNew: { type: 'string', description: 'Custom UID for newly created folder' },
parentUidList: { type: 'string', description: 'Parent folder UID to list children of' },
parentUidNew: { type: 'string', description: 'Parent folder UID for newly created folder' },
// Contact point inputs
manageFolderUid: { type: 'string', description: 'UID of the folder to get, update, or delete' },
updateFolderTitle: { type: 'string', description: 'New title for the folder being updated' },
forceDeleteRules: {
type: 'boolean',
description: 'Delete alert rules stored in the folder when deleting it',
},
contactPointName: { type: 'string', description: 'Filter contact points by name' },
// Data source inputs
contactPointNameNew: { type: 'string', description: 'Name for the new contact point' },
contactPointType: {
type: 'string',
description: 'Receiver type for the new contact point (e.g., slack, email)',
},
contactPointSettings: {
type: 'string',
description: 'JSON of receiver-specific settings for the new contact point',
},
disableResolveMessage: {
type: 'boolean',
description: 'Do not send a notification when the alert resolves',
},
dataSourceId: { type: 'string', description: 'Data source ID or UID' },
dataSourceUid: { type: 'string', description: 'Data source UID for health checks' },
},
outputs: {
// Health outputs
version: { type: 'string', description: 'Grafana version' },
database: { type: 'string', description: 'Database health status' },
status: { type: 'string', description: 'Health status' },
// Dashboard outputs
commit: { type: 'string', description: 'Git commit hash of the Grafana build' },
status: { type: 'string', description: 'Health status (e.g., data source health)' },
dashboard: { type: 'json', description: 'Dashboard JSON' },
meta: { type: 'json', description: 'Dashboard metadata' },
dashboards: { type: 'json', description: 'List of dashboards' },
uid: { type: 'string', description: 'Created/updated UID' },
url: { type: 'string', description: 'Dashboard URL' },
// Alert outputs
rules: { type: 'json', description: 'Alert rules list' },
contactPoints: { type: 'json', description: 'Contact points list' },
name: { type: 'string', description: 'Name of the created contact point' },
type: { type: 'string', description: 'Type of the created contact point' },
settings: { type: 'json', description: 'Contact point receiver settings' },
condition: { type: 'string', description: 'Alert condition refId' },
for: { type: 'string', description: 'Duration the condition must hold before firing' },
keepFiringFor: {
@@ -904,14 +1105,10 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
notification_settings: { type: 'json', description: 'Per-rule notification settings' },
record: { type: 'json', description: 'Recording rule configuration' },
updated: { type: 'string', description: 'Last update timestamp' },
// Annotation outputs
annotations: { type: 'json', description: 'Annotations list' },
id: { type: 'number', description: 'Annotation ID' },
// Data source outputs
dataSources: { type: 'json', description: 'Data sources list' },
// Folder outputs
folders: { type: 'json', description: 'Folders list' },
// Common
message: { type: 'string', description: 'Status message' },
},
}
@@ -1007,7 +1204,7 @@ export const GrafanaBlockMeta = {
name: 'audit-dashboards',
description: 'List Grafana dashboards and folders and report data sources each depends on.',
content:
'# Audit Dashboards\n\nInventory dashboards and the data sources they rely on.\n\n## Steps\n1. List folders and dashboards to build the full inventory.\n2. Get details for each dashboard of interest to read its panels and referenced data sources.\n3. List data sources and cross-reference to flag dashboards pointing at missing or deprecated sources.\n\n## Output\nReturn an inventory grouped by folder, each dashboard with its UID and the data sources it uses, plus a flagged list of dashboards with broken or unknown data source references.',
'# Audit Dashboards\n\nInventory dashboards and the data sources they rely on.\n\n## Steps\n1. List folders and dashboards to build the full inventory.\n2. Get details for each dashboard of interest to read its panels and referenced data sources.\n3. List data sources, then check the health of each one to flag dashboards pointing at unreachable or deprecated sources.\n\n## Output\nReturn an inventory grouped by folder, each dashboard with its UID and the data sources it uses, plus a flagged list of dashboards whose data sources failed their health check.',
},
{
name: 'provision-monitoring-folder',
@@ -1016,5 +1213,12 @@ export const GrafanaBlockMeta = {
content:
'# Provision Monitoring Folder\n\nSet up an organized monitoring home for a new service or team.\n\n## Steps\n1. Create a folder with a descriptive title for the service or team.\n2. List data sources and pick the one the new dashboard should query.\n3. Create a dashboard inside the folder with starter panels for the key metrics.\n4. Get the dashboard back to confirm it was created in the right folder.\n\n## Output\nReturn the folder UID and the new dashboard UID and link. Note the data source the dashboard was wired to.',
},
{
name: 'provision-alerting',
description:
'Stand up a Grafana alert rule and the contact point it notifies for a new service.',
content:
'# Provision Alerting\n\nWire up end-to-end alerting for a service: a notification target plus the rule that fires to it.\n\n## Steps\n1. List existing contact points to avoid duplicating one.\n2. Create a contact point for the destination (Slack, email, or PagerDuty) with its settings.\n3. Create an alert rule in the target folder with the query data, condition, and for-duration.\n4. Get the alert rule back to confirm it was created and is not paused.\n\n## Output\nReturn the new contact point UID and alert rule UID, with the data source and threshold the rule evaluates.',
},
],
} as const satisfies BlockMeta
+27 -3
View File
@@ -1,5 +1,5 @@
{
"updatedAt": "2026-06-15",
"updatedAt": "2026-06-16",
"integrations": [
{
"type": "onepassword",
@@ -6812,7 +6812,7 @@
"name": "Grafana",
"description": "Interact with Grafana dashboards, alerts, and annotations",
"longDescription": "Integrate Grafana into workflows. Manage dashboards, alerts, annotations, data sources, folders, and monitor health status.",
"bgColor": "#FFFFFF",
"bgColor": "#F46800",
"iconName": "GrafanaIcon",
"docsUrl": "https://docs.sim.ai/integrations/grafana",
"operations": [
@@ -6860,6 +6860,10 @@
"name": "List Contact Points",
"description": "List all alert notification contact points"
},
{
"name": "Create Contact Point",
"description": "Create a notification contact point (e.g., Slack, email, PagerDuty)"
},
{
"name": "Create Annotation",
"description": "Create an annotation on a dashboard or as a global annotation"
@@ -6884,6 +6888,10 @@
"name": "Get Data Source",
"description": "Get a data source by its ID or UID"
},
{
"name": "Check Data Source Health",
"description": "Test connectivity to a data source by its UID"
},
{
"name": "List Folders",
"description": "List all folders in Grafana"
@@ -6891,9 +6899,25 @@
{
"name": "Create Folder",
"description": "Create a new folder in Grafana"
},
{
"name": "Get Folder",
"description": "Get a folder by its UID"
},
{
"name": "Update Folder",
"description": "Update (rename) a folder. Fetches the current folder and merges your changes."
},
{
"name": "Delete Folder",
"description": "Delete a folder by its UID"
},
{
"name": "Get Health",
"description": "Check the health of the Grafana instance (version, database status)"
}
],
"operationCount": 19,
"operationCount": 25,
"triggers": [],
"triggerCount": 0,
"authType": "api-key",
@@ -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' },
},
}
+1 -1
View File
@@ -172,7 +172,7 @@ export const createAlertRuleTool: ToolConfig<
if (params.organizationId) body.orgID = Number(params.organizationId)
if (params.condition) body.condition = params.condition
if (params.uid) body.uid = params.uid
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
@@ -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)' },
},
}
+1 -1
View File
@@ -42,7 +42,7 @@ export const deleteAlertRuleTool: ToolConfig<
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`,
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`,
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
+1 -1
View File
@@ -42,7 +42,7 @@ export const deleteDashboardTool: ToolConfig<
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`,
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`,
method: 'DELETE',
headers: (params) => {
const headers: Record<string, string> = {
+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' },
},
}
+1 -1
View File
@@ -42,7 +42,7 @@ export const getAlertRuleTool: ToolConfig<GrafanaGetAlertRuleParams, GrafanaGetA
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`,
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
+1 -1
View File
@@ -37,7 +37,7 @@ export const getDashboardTool: ToolConfig<GrafanaGetDashboardParams, GrafanaGetD
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`,
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
@@ -44,8 +44,6 @@ export const getDataSourceTool: ToolConfig<
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const id = params.dataSourceId.trim()
// Numeric DB id route only matches purely-numeric ids up to int64 length;
// anything else is treated as a UID (Grafana UIDs are short slug strings).
const isNumericId = /^\d+$/.test(id) && id.length <= 18
if (isNumericId) {
return `${baseUrl}/api/datasources/${id}`
+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' },
},
}
+13 -5
View File
@@ -1,13 +1,18 @@
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'
@@ -17,32 +22,35 @@ 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'
// Dashboard tools
export const grafanaGetDashboardTool = getDashboardTool
export const grafanaListDashboardsTool = listDashboardsTool
export const grafanaCreateDashboardTool = createDashboardTool
export const grafanaUpdateDashboardTool = updateDashboardTool
export const grafanaDeleteDashboardTool = deleteDashboardTool
// Alert tools
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
// Annotation tools
export const grafanaCreateAnnotationTool = createAnnotationTool
export const grafanaListAnnotationsTool = listAnnotationsTool
export const grafanaUpdateAnnotationTool = updateAnnotationTool
export const grafanaDeleteAnnotationTool = deleteAnnotationTool
// Data Source tools
export const grafanaListDataSourcesTool = listDataSourcesTool
export const grafanaGetDataSourceTool = getDataSourceTool
export const grafanaCheckDataSourceHealthTool = checkDataSourceHealthTool
// Folder tools
export const grafanaListFoldersTool = listFoldersTool
export const grafanaCreateFolderTool = createFolderTool
export const grafanaGetFolderTool = getFolderTool
export const grafanaUpdateFolderTool = updateFolderTool
export const grafanaDeleteFolderTool = deleteFolderTool
export const grafanaGetHealthTool = getHealthTool
+74 -32
View File
@@ -1,4 +1,3 @@
// Common types for Grafana API tools
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
@@ -43,17 +42,15 @@ export const ALERT_RULE_OUTPUT_FIELDS: Record<string, OutputProperty> = {
},
}
// Common parameters for all Grafana tools
interface GrafanaBaseParams {
apiKey: string
baseUrl: string
organizationId?: string
}
// Health Check types
interface GrafanaHealthCheckParams extends GrafanaBaseParams {}
export interface GrafanaHealthCheckParams extends GrafanaBaseParams {}
interface GrafanaHealthCheckResponse extends ToolResponse {
export interface GrafanaHealthCheckResponse extends ToolResponse {
output: {
commit: string
database: string
@@ -61,18 +58,17 @@ interface GrafanaHealthCheckResponse extends ToolResponse {
}
}
interface GrafanaDataSourceHealthParams extends GrafanaBaseParams {
dataSourceId: string
export interface GrafanaDataSourceHealthParams extends GrafanaBaseParams {
dataSourceUid: string
}
interface GrafanaDataSourceHealthResponse extends ToolResponse {
export interface GrafanaDataSourceHealthResponse extends ToolResponse {
output: {
status: string
message: string
}
}
// Dashboard types
export interface GrafanaGetDashboardParams extends GrafanaBaseParams {
dashboardUid: string
}
@@ -164,7 +160,7 @@ export interface GrafanaCreateDashboardParams extends GrafanaBaseParams {
tags?: string
timezone?: string
refresh?: string
panels?: string // JSON string of panels array
panels?: string
overwrite?: boolean
message?: string
}
@@ -187,7 +183,7 @@ export interface GrafanaUpdateDashboardParams extends GrafanaBaseParams {
tags?: string
timezone?: string
refresh?: string
panels?: string // JSON string of panels array
panels?: string
overwrite?: boolean
message?: string
}
@@ -215,7 +211,6 @@ export interface GrafanaDeleteDashboardResponse extends ToolResponse {
}
}
// Alert Rule types
export interface GrafanaListAlertRulesParams extends GrafanaBaseParams {}
interface GrafanaAlertRule {
@@ -260,18 +255,18 @@ export interface GrafanaCreateAlertRuleParams extends GrafanaBaseParams {
folderUid: string
ruleGroup: string
condition?: string
data: string // JSON string of data array
data: string
forDuration?: string
noDataState?: string
execErrState?: string
annotations?: string // JSON string
labels?: string // JSON string
annotations?: string
labels?: string
uid?: string
isPaused?: boolean
keepFiringFor?: string
missingSeriesEvalsToResolve?: number
notificationSettings?: string // JSON string
record?: string // JSON string
notificationSettings?: string
record?: string
disableProvenance?: boolean
}
@@ -285,17 +280,17 @@ export interface GrafanaUpdateAlertRuleParams extends GrafanaBaseParams {
folderUid?: string
ruleGroup?: string
condition?: string
data?: string // JSON string of data array
data?: string
forDuration?: string
noDataState?: string
execErrState?: string
annotations?: string // JSON string
labels?: string // JSON string
annotations?: string
labels?: string
isPaused?: boolean
keepFiringFor?: string
missingSeriesEvalsToResolve?: number
notificationSettings?: string // JSON string
record?: string // JSON string
notificationSettings?: string
record?: string
disableProvenance?: boolean
}
@@ -313,14 +308,13 @@ export interface GrafanaDeleteAlertRuleResponse extends ToolResponse {
}
}
// Annotation types
export interface GrafanaCreateAnnotationParams extends GrafanaBaseParams {
text: string
tags?: string // comma-separated
tags?: string
dashboardUid?: string
panelId?: number
time?: number // epoch ms
timeEnd?: number // epoch ms
time?: number
timeEnd?: number
}
interface GrafanaAnnotation {
@@ -356,7 +350,7 @@ export interface GrafanaListAnnotationsParams extends GrafanaBaseParams {
panelId?: number
alertId?: number
userId?: number
tags?: string // comma-separated
tags?: string
type?: string
limit?: number
}
@@ -370,7 +364,7 @@ export interface GrafanaListAnnotationsResponse extends ToolResponse {
export interface GrafanaUpdateAnnotationParams extends GrafanaBaseParams {
annotationId: number
text?: string
tags?: string // comma-separated
tags?: string
time?: number
timeEnd?: number
}
@@ -392,7 +386,6 @@ export interface GrafanaDeleteAnnotationResponse extends ToolResponse {
}
}
// Data Source types
export interface GrafanaListDataSourcesParams extends GrafanaBaseParams {}
interface GrafanaDataSource {
@@ -430,7 +423,6 @@ export interface GrafanaGetDataSourceResponse extends ToolResponse {
output: GrafanaDataSource
}
// Folder types
export interface GrafanaListFoldersParams extends GrafanaBaseParams {
limit?: number
page?: number
@@ -477,7 +469,35 @@ export interface GrafanaCreateFolderResponse extends ToolResponse {
output: GrafanaFolder
}
// Contact Points types
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
}
@@ -497,7 +517,25 @@ export interface GrafanaListContactPointsResponse extends ToolResponse {
}
}
// Union type for all Grafana responses
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
@@ -519,4 +557,8 @@ export type GrafanaResponse =
| GrafanaGetDataSourceResponse
| GrafanaListFoldersResponse
| GrafanaCreateFolderResponse
| GrafanaGetFolderResponse
| GrafanaUpdateFolderResponse
| GrafanaDeleteFolderResponse
| GrafanaListContactPointsResponse
| GrafanaCreateContactPointResponse
+2 -2
View File
@@ -137,7 +137,7 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`,
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
@@ -266,7 +266,7 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
headers['X-Disable-Provenance'] = 'true'
}
const updateUrl = `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`
const updateUrl = `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`
const urlValidation = await validateUrlWithDNS(updateUrl, 'baseUrl')
if (!urlValidation.isValid || !urlValidation.resolvedIP) {
return {
+1 -1
View File
@@ -90,7 +90,7 @@ export const updateDashboardTool: ToolConfig<GrafanaUpdateDashboardParams, ToolR
request: {
url: (params) =>
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`,
`${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`,
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
+192
View File
@@ -0,0 +1,192 @@
import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
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: (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: { _existingFolder: data },
}
},
postProcess: async (result, params) => {
const existingFolder = result.output._existingFolder
if (!existingFolder || !existingFolder.uid) {
return { success: false, output: {}, error: 'Failed to fetch existing folder' }
}
const body: Record<string, unknown> = {
title: params.title ?? existingFolder.title,
version: existingFolder.version,
overwrite: true,
}
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Authorization: `Bearer ${params.apiKey}`,
}
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
const updateUrl = `${params.baseUrl.replace(/\/$/, '')}/api/folders/${params.folderUid.trim()}`
const urlValidation = await validateUrlWithDNS(updateUrl, 'baseUrl')
if (!urlValidation.isValid || !urlValidation.resolvedIP) {
return {
success: false,
output: {},
error: `Invalid Grafana baseUrl: ${urlValidation.error}`,
}
}
const updateResponse = await secureFetchWithPinnedIP(updateUrl, urlValidation.resolvedIP, {
method: 'PUT',
headers,
body: JSON.stringify(body),
})
if (!updateResponse.ok) {
const errorText = await updateResponse.text()
return { success: false, output: {}, error: `Failed to update folder: ${errorText}` }
}
const data = (await updateResponse.json()) as Record<string, unknown>
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 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 },
},
}
+12
View File
@@ -1366,16 +1366,21 @@ import {
listMattersTool,
} from '@/tools/google_vault'
import {
grafanaCheckDataSourceHealthTool,
grafanaCreateAlertRuleTool,
grafanaCreateAnnotationTool,
grafanaCreateContactPointTool,
grafanaCreateDashboardTool,
grafanaCreateFolderTool,
grafanaDeleteAlertRuleTool,
grafanaDeleteAnnotationTool,
grafanaDeleteDashboardTool,
grafanaDeleteFolderTool,
grafanaGetAlertRuleTool,
grafanaGetDashboardTool,
grafanaGetDataSourceTool,
grafanaGetFolderTool,
grafanaGetHealthTool,
grafanaListAlertRulesTool,
grafanaListAnnotationsTool,
grafanaListContactPointsTool,
@@ -1385,6 +1390,7 @@ import {
grafanaUpdateAlertRuleTool,
grafanaUpdateAnnotationTool,
grafanaUpdateDashboardTool,
grafanaUpdateFolderTool,
} from '@/tools/grafana'
import {
grainCreateHookTool,
@@ -3998,14 +4004,20 @@ export const tools: Record<string, ToolConfig> = {
grafana_update_alert_rule: grafanaUpdateAlertRuleTool,
grafana_delete_alert_rule: grafanaDeleteAlertRuleTool,
grafana_list_contact_points: grafanaListContactPointsTool,
grafana_create_contact_point: grafanaCreateContactPointTool,
grafana_create_annotation: grafanaCreateAnnotationTool,
grafana_list_annotations: grafanaListAnnotationsTool,
grafana_update_annotation: grafanaUpdateAnnotationTool,
grafana_delete_annotation: grafanaDeleteAnnotationTool,
grafana_list_data_sources: grafanaListDataSourcesTool,
grafana_get_data_source: grafanaGetDataSourceTool,
grafana_check_data_source_health: grafanaCheckDataSourceHealthTool,
grafana_list_folders: grafanaListFoldersTool,
grafana_create_folder: grafanaCreateFolderTool,
grafana_get_folder: grafanaGetFolderTool,
grafana_update_folder: grafanaUpdateFolderTool,
grafana_delete_folder: grafanaDeleteFolderTool,
grafana_get_health: grafanaGetHealthTool,
google_search: googleSearchTool,
greenhouse_list_candidates: greenhouseListCandidatesTool,
greenhouse_get_candidate: greenhouseGetCandidateTool,