mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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 |
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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)' },
|
||||
},
|
||||
}
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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' },
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -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 },
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user