improvement(grafana): align tools and block with Grafana API spec (#4574)

* improvement(grafana): align tools and block with official Grafana API spec

Validates and corrects the Grafana integration against the official API
docs: fixes wire-format field naming for provisioned alert rules
(missing_series_evals_to_resolve, keepFiringFor, orgID), adds
X-Disable-Provenance support, expands alert-rule params (isPaused,
notificationSettings, record, annotations, labels), corrects defaults
(execErrState=Error, dashboard overwrite=false), and centralizes alert-rule
output mapping in a shared utils module.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(grafana): correct wire-format casing for provisioned alert rule fields

Grafana's ProvisionedAlertRule schema (verified against upstream Go source
and swagger spec) uses keep_firing_for (snake_case) and
missingSeriesEvalsToResolve (camelCase) — the opposite of what prior audit
rounds assumed. POST/PUT bodies now send the correct field names; mapAlertRule
reads the correct primary names with the old casings kept as fallbacks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(grafana): address PR review feedback

- Drop hardcoded orgID: 1 fallback; only send orgID when organizationId is
  provided, so token-scoped org context drives rule placement.
- Surface invalid JSON for notificationSettings/record on alert rule
  create/update instead of silently dropping the input.
- Fix execErrState description in update_alert_rule to include Error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(grafana): surface invalid JSON for annotations/labels/data on alert rules

Match the behavior of other JSON params (data, notificationSettings, record):
return a descriptive error instead of silently falling back to {} (create)
or keeping the existing value (update).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(grafana): expose alert-rule output fields in generated docs

Move ALERT_RULE_OUTPUT_FIELDS from utils.ts to types.ts and rename to
SCREAMING_SNAKE_CASE so scripts/generate-docs.ts (which only resolves const
references from types.ts matching [A-Z][A-Z_0-9]+) can inline the per-field
rows into the generated alert-rule output tables.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-05-12 19:42:20 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 773cd84e3f
commit c21bb915f7
18 changed files with 1101 additions and 524 deletions
+131 -26
View File
@@ -71,9 +71,11 @@ Search and list all dashboards
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `query` | string | No | Search query to filter dashboards by title |
| `tag` | string | No | Filter by tag \(comma-separated for multiple tags\) |
| `folderIds` | string | No | Filter by folder IDs \(comma-separated, e.g., 1,2,3\) |
| `folderUIDs` | string | No | Filter by folder UIDs \(comma-separated, e.g., abc123,def456\) |
| `dashboardUIDs` | string | No | Filter by dashboard UIDs \(comma-separated, e.g., abc123,def456\) |
| `starred` | boolean | No | Only return starred dashboards |
| `limit` | number | No | Maximum number of dashboards to return |
| `limit` | number | No | Maximum number of dashboards to return \(default 1000\) |
| `page` | number | No | Page number for pagination \(1-based\) |
#### Output
@@ -136,7 +138,7 @@ Update an existing dashboard. Fetches the current dashboard and merges your chan
| `timezone` | string | No | Dashboard timezone \(e.g., browser, utc\) |
| `refresh` | string | No | Auto-refresh interval \(e.g., 5s, 1m, 5m\) |
| `panels` | string | No | JSON array of panel configurations |
| `overwrite` | boolean | No | Overwrite even if there is a version conflict |
| `overwrite` | boolean | No | Overwrite even if there is a version conflict \(defaults to false to surface 412 conflicts\) |
| `message` | string | No | Commit message for this version |
#### Output
@@ -188,13 +190,26 @@ List all alert rules in the Grafana instance
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `rules` | array | List of alert rules |
| ↳ `id` | number | Alert rule numeric ID |
| ↳ `uid` | string | Alert rule UID |
| ↳ `title` | string | Alert rule title |
| ↳ `condition` | string | Alert condition |
| ↳ `folderUID` | string | Parent folder UID |
| ↳ `ruleGroup` | string | Rule group name |
| ↳ `condition` | string | RefId of the query used as the alert condition |
| ↳ `data` | json | Alert rule query/expression data array |
| ↳ `updated` | string | Last update timestamp |
| ↳ `noDataState` | string | State when no data is returned |
| ↳ `execErrState` | string | State on execution error |
| ↳ `for` | string | Duration the condition must hold before firing |
| ↳ `keepFiringFor` | string | Duration to keep firing after condition stops |
| ↳ `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving |
| ↳ `annotations` | json | Alert annotations |
| ↳ `labels` | json | Alert labels |
| ↳ `isPaused` | boolean | Whether the rule is paused |
| ↳ `folderUID` | string | Parent folder UID |
| ↳ `ruleGroup` | string | Rule group name |
| ↳ `orgID` | number | Organization ID |
| ↳ `provenance` | string | Provisioning source \(empty if API-managed\) |
| ↳ `notification_settings` | json | Per-rule notification settings \(overrides\) |
| ↳ `record` | json | Recording rule configuration \(recording rules only\) |
### `grafana_get_alert_rule`
@@ -213,16 +228,26 @@ Get a specific alert rule by its UID
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `id` | number | Alert rule numeric ID |
| `uid` | string | Alert rule UID |
| `title` | string | Alert rule title |
| `condition` | string | Alert condition |
| `data` | json | Alert rule query data |
| `folderUID` | string | Parent folder UID |
| `ruleGroup` | string | Rule group name |
| `condition` | string | RefId of the query used as the alert condition |
| `data` | json | Alert rule query/expression data array |
| `updated` | string | Last update timestamp |
| `noDataState` | string | State when no data is returned |
| `execErrState` | string | State on execution error |
| `for` | string | Duration the condition must hold before firing |
| `keepFiringFor` | string | Duration to keep firing after condition stops |
| `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving |
| `annotations` | json | Alert annotations |
| `labels` | json | Alert labels |
| `isPaused` | boolean | Whether the rule is paused |
| `folderUID` | string | Parent folder UID |
| `ruleGroup` | string | Rule group name |
| `orgID` | number | Organization ID |
| `provenance` | string | Provisioning source \(empty if API-managed\) |
| `notification_settings` | json | Per-rule notification settings \(overrides\) |
| `record` | json | Recording rule configuration \(recording rules only\) |
### `grafana_create_alert_rule`
@@ -238,22 +263,45 @@ Create a new alert rule
| `title` | string | Yes | The title of the alert rule |
| `folderUid` | string | Yes | The UID of the folder to create the alert in \(e.g., folder-abc123\) |
| `ruleGroup` | string | Yes | The name of the rule group |
| `condition` | string | Yes | The refId of the query or expression to use as the alert condition |
| `condition` | string | No | The refId of the query or expression to use as the alert condition \(required for alerting rules; omit for recording rules\) |
| `data` | string | Yes | JSON array of query/expression data objects |
| `forDuration` | string | No | Duration to wait before firing \(e.g., 5m, 1h\) |
| `noDataState` | string | No | State when no data is returned \(NoData, Alerting, OK\) |
| `execErrState` | string | No | State on execution error \(Alerting, OK\) |
| `execErrState` | string | No | State on execution error \(Error, Alerting, OK\) |
| `annotations` | string | No | JSON object of annotations |
| `labels` | string | No | JSON object of labels |
| `uid` | string | No | Optional custom UID for the alert rule |
| `isPaused` | boolean | No | Whether the rule is paused on creation |
| `keepFiringFor` | string | No | Duration to keep firing after the condition stops \(e.g., 5m\) |
| `missingSeriesEvalsToResolve` | number | No | Number of missing series evaluations before resolving |
| `notificationSettings` | string | No | JSON object of per-rule notification settings \(overrides\) |
| `record` | string | No | JSON object configuring this as a recording rule \(omit for alerting rules\) |
| `disableProvenance` | boolean | No | Set X-Disable-Provenance header so the rule remains editable in the Grafana UI |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `uid` | string | The UID of the created alert rule |
| `id` | number | Alert rule numeric ID |
| `uid` | string | Alert rule UID |
| `title` | string | Alert rule title |
| `condition` | string | RefId of the query used as the alert condition |
| `data` | json | Alert rule query/expression data array |
| `updated` | string | Last update timestamp |
| `noDataState` | string | State when no data is returned |
| `execErrState` | string | State on execution error |
| `for` | string | Duration the condition must hold before firing |
| `keepFiringFor` | string | Duration to keep firing after condition stops |
| `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving |
| `annotations` | json | Alert annotations |
| `labels` | json | Alert labels |
| `isPaused` | boolean | Whether the rule is paused |
| `folderUID` | string | Parent folder UID |
| `ruleGroup` | string | Rule group name |
| `orgID` | number | Organization ID |
| `provenance` | string | Provisioning source \(empty if API-managed\) |
| `notification_settings` | json | Per-rule notification settings \(overrides\) |
| `record` | json | Recording rule configuration \(recording rules only\) |
### `grafana_update_alert_rule`
@@ -274,18 +322,40 @@ Update an existing alert rule. Fetches the current rule and merges your changes.
| `data` | string | No | New JSON array of query/expression data objects |
| `forDuration` | string | No | Duration to wait before firing \(e.g., 5m, 1h\) |
| `noDataState` | string | No | State when no data is returned \(NoData, Alerting, OK\) |
| `execErrState` | string | No | State on execution error \(Alerting, OK\) |
| `execErrState` | string | No | State on execution error \(Error, Alerting, OK\) |
| `annotations` | string | No | JSON object of annotations |
| `labels` | string | No | JSON object of labels |
| `isPaused` | boolean | No | Whether the rule is paused |
| `keepFiringFor` | string | No | Duration to keep firing after the condition stops \(e.g., 5m\) |
| `missingSeriesEvalsToResolve` | number | No | Number of missing series evaluations before resolving |
| `notificationSettings` | string | No | JSON object of per-rule notification settings \(overrides\) |
| `record` | string | No | JSON object configuring this as a recording rule |
| `disableProvenance` | boolean | No | Set X-Disable-Provenance header so the rule remains editable in the Grafana UI |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `uid` | string | The UID of the updated alert rule |
| `id` | number | Alert rule numeric ID |
| `uid` | string | Alert rule UID |
| `title` | string | Alert rule title |
| `condition` | string | RefId of the query used as the alert condition |
| `data` | json | Alert rule query/expression data array |
| `updated` | string | Last update timestamp |
| `noDataState` | string | State when no data is returned |
| `execErrState` | string | State on execution error |
| `for` | string | Duration the condition must hold before firing |
| `keepFiringFor` | string | Duration to keep firing after condition stops |
| `missingSeriesEvalsToResolve` | number | Number of missing series evaluations before resolving |
| `annotations` | json | Alert annotations |
| `labels` | json | Alert labels |
| `isPaused` | boolean | Whether the rule is paused |
| `folderUID` | string | Parent folder UID |
| `ruleGroup` | string | Rule group name |
| `orgID` | number | Organization ID |
| `provenance` | string | Provisioning source \(empty if API-managed\) |
| `notification_settings` | json | Per-rule notification settings \(overrides\) |
| `record` | json | Recording rule configuration \(recording rules only\) |
### `grafana_delete_alert_rule`
@@ -317,6 +387,7 @@ List all alert notification contact points
| `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 | No | Filter contact points by exact name match |
#### Output
@@ -327,6 +398,8 @@ List all alert notification contact points
| ↳ `name` | string | Contact point name |
| ↳ `type` | string | Notification type \(email, slack, etc.\) |
| ↳ `settings` | object | Type-specific settings |
| ↳ `disableResolveMessage` | boolean | Whether resolve messages are disabled |
| ↳ `provenance` | string | Provisioning source \(empty if API-managed\) |
### `grafana_create_annotation`
@@ -341,7 +414,7 @@ Create an annotation on a dashboard or as a global annotation
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `text` | string | Yes | The text content of the annotation |
| `tags` | string | No | Comma-separated list of tags |
| `dashboardUid` | string | Yes | UID of the dashboard to add the annotation to \(e.g., abc123def\) |
| `dashboardUid` | string | No | UID of the dashboard to add the annotation to \(e.g., abc123def\). Omit to create a global organization annotation. |
| `panelId` | number | No | ID of the panel to add the annotation to \(e.g., 1, 2\) |
| `time` | number | No | Start time in epoch milliseconds \(e.g., 1704067200000, defaults to now\) |
| `timeEnd` | number | No | End time in epoch milliseconds for range annotations \(e.g., 1704153600000\) |
@@ -366,8 +439,11 @@ Query annotations by time range, dashboard, or tags
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `from` | number | No | Start time in epoch milliseconds \(e.g., 1704067200000\) |
| `to` | number | No | End time in epoch milliseconds \(e.g., 1704153600000\) |
| `dashboardUid` | string | Yes | Dashboard UID to query annotations from \(e.g., abc123def\) |
| `dashboardUid` | string | No | Dashboard UID to query annotations from \(e.g., abc123def\). Omit to query annotations across the organization. |
| `dashboardId` | number | No | Legacy numeric dashboard ID filter \(prefer dashboardUid\) |
| `panelId` | number | No | Filter by panel ID \(e.g., 1, 2\) |
| `alertId` | number | No | Filter by alert ID |
| `userId` | number | No | Filter by ID of the user who created the annotation |
| `tags` | string | No | Comma-separated list of tags to filter by |
| `type` | string | No | Filter by type \(alert or annotation\) |
| `limit` | number | No | Maximum number of annotations to return |
@@ -378,17 +454,19 @@ Query annotations by time range, dashboard, or tags
| --------- | ---- | ----------- |
| `annotations` | array | List of annotations |
| ↳ `id` | number | Annotation ID |
| ↳ `alertId` | number | Associated alert ID \(0 if not alert-driven\) |
| ↳ `dashboardId` | number | Dashboard ID |
| ↳ `dashboardUID` | string | Dashboard UID |
| ↳ `created` | number | Creation timestamp in epoch ms |
| ↳ `updated` | number | Last update timestamp in epoch ms |
| ↳ `panelId` | number | Panel ID within the dashboard |
| ↳ `userId` | number | ID of the user who created the annotation |
| ↳ `userName` | string | Username of the user who created the annotation |
| ↳ `newState` | string | New alert state \(alert annotations only\) |
| ↳ `prevState` | string | Previous alert state \(alert annotations only\) |
| ↳ `time` | number | Start time in epoch ms |
| ↳ `timeEnd` | number | End time in epoch ms |
| ↳ `text` | string | Annotation text |
| ↳ `metric` | string | Metric associated with the annotation |
| ↳ `tags` | array | Annotation tags |
| ↳ `login` | string | Login of the user who created the annotation |
| ↳ `email` | string | Email of the user who created the annotation |
| ↳ `avatarUrl` | string | Avatar URL of the user |
| ↳ `data` | json | Additional annotation data object from Grafana |
### `grafana_update_annotation`
@@ -403,7 +481,7 @@ Update an existing annotation
| `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\) |
| `annotationId` | number | Yes | The ID of the annotation to update |
| `text` | string | Yes | New text content for the annotation |
| `text` | string | No | New text content for the annotation \(PATCH supports partial updates\) |
| `tags` | string | No | Comma-separated list of new tags |
| `time` | number | No | New start time in epoch milliseconds \(e.g., 1704067200000\) |
| `timeEnd` | number | No | New end time in epoch milliseconds \(e.g., 1704153600000\) |
@@ -453,10 +531,22 @@ List all data sources configured in Grafana
| `dataSources` | array | List of data sources |
| ↳ `id` | number | Data source ID |
| ↳ `uid` | string | Data source UID |
| ↳ `orgId` | number | Organization ID |
| ↳ `name` | string | Data source name |
| ↳ `type` | string | Data source type \(prometheus, mysql, etc.\) |
| ↳ `typeLogoUrl` | string | Logo URL for the data source type |
| ↳ `access` | string | Access mode \(proxy or direct\) |
| ↳ `url` | string | Data source URL |
| ↳ `user` | string | Username used to connect |
| ↳ `database` | string | Database name \(if applicable\) |
| ↳ `basicAuth` | boolean | Whether basic auth is enabled |
| ↳ `basicAuthUser` | string | Basic auth username |
| ↳ `withCredentials` | boolean | Whether to send credentials with cross-origin requests |
| ↳ `isDefault` | boolean | Whether this is the default data source |
| ↳ `jsonData` | object | Type-specific JSON configuration |
| ↳ `secureJsonFields` | object | Map of secure fields that are set \(values are not returned\) |
| ↳ `version` | number | Data source version |
| ↳ `readOnly` | boolean | Whether the data source is read-only |
### `grafana_get_data_source`
@@ -477,12 +567,22 @@ Get a data source by its ID or UID
| --------- | ---- | ----------- |
| `id` | number | Data source ID |
| `uid` | string | Data source UID |
| `orgId` | number | Organization ID |
| `name` | string | Data source name |
| `type` | string | Data source type |
| `typeLogoUrl` | string | Logo URL for the data source type |
| `access` | string | Access mode \(proxy or direct\) |
| `url` | string | Data source connection URL |
| `user` | string | Username used to connect |
| `database` | string | Database name \(if applicable\) |
| `basicAuth` | boolean | Whether basic auth is enabled |
| `basicAuthUser` | string | Basic auth username |
| `withCredentials` | boolean | Whether to send credentials with cross-origin requests |
| `isDefault` | boolean | Whether this is the default data source |
| `jsonData` | json | Additional data source configuration |
| `secureJsonFields` | object | Map of secure fields that are set \(values are not returned\) |
| `version` | number | Data source version |
| `readOnly` | boolean | Whether the data source is read-only |
### `grafana_list_folders`
@@ -497,6 +597,7 @@ List all folders in Grafana
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `limit` | number | No | Maximum number of folders to return |
| `page` | number | No | Page number for pagination |
| `parentUid` | string | No | List children of this folder UID \(requires nested folders enabled\) |
#### Output
@@ -506,16 +607,18 @@ List all folders in Grafana
| ↳ `id` | number | Folder ID |
| ↳ `uid` | string | Folder UID |
| ↳ `title` | string | Folder title |
| ↳ `url` | string | Folder URL path |
| ↳ `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 |
| ↳ `canDelete` | boolean | Whether the current user can delete 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 |
| ↳ `version` | number | Folder version number |
### `grafana_create_folder`
@@ -530,6 +633,7 @@ Create a new folder in Grafana
| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) |
| `title` | string | Yes | The title of the new folder |
| `uid` | string | No | Optional UID for the folder \(auto-generated if not provided\) |
| `parentUid` | string | No | Parent folder UID for nested folders \(requires nested folders enabled\) |
#### Output
@@ -539,11 +643,12 @@ Create a new folder in Grafana
| `uid` | string | The UID of the created folder |
| `title` | string | The title of the created 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 |
| `canDelete` | boolean | Whether the current user can delete 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 |
+253 -23
View File
@@ -126,6 +126,33 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`,
placeholder: 'tag1, tag2 (comma-separated)',
condition: { field: 'operation', value: 'grafana_list_dashboards' },
},
{
id: 'folderUIDs',
title: 'Folder UIDs',
type: 'short-input',
placeholder: 'uid1, uid2 (comma-separated)',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_dashboards' },
},
{
id: 'dashboardUIDs',
title: 'Dashboard UIDs',
type: 'short-input',
placeholder: 'uid1, uid2 (comma-separated)',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_dashboards' },
},
{
id: 'page',
title: 'Page',
type: 'short-input',
placeholder: 'Page number (1-based)',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_list_dashboards', 'grafana_list_folders'],
},
},
// Create/Update Dashboard
{
@@ -156,13 +183,15 @@ Return ONLY the title - no explanations, no quotes, no extra text.`,
id: 'folderUid',
title: 'Folder UID',
type: 'short-input',
placeholder: 'Optional - folder to create dashboard in',
placeholder: 'Folder UID (required for alert rules, optional for dashboards)',
required: { field: 'operation', value: 'grafana_create_alert_rule' },
condition: {
field: 'operation',
value: [
'grafana_create_dashboard',
'grafana_update_dashboard',
'grafana_create_alert_rule',
'grafana_update_alert_rule',
],
},
},
@@ -229,6 +258,16 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
value: ['grafana_create_dashboard', 'grafana_update_dashboard'],
},
},
{
id: 'overwrite',
title: 'Overwrite on Conflict',
type: 'switch',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_dashboard', 'grafana_update_dashboard'],
},
},
// Alert Rule operations
{
@@ -268,16 +307,6 @@ Return ONLY the alert title - no explanations, no quotes, no extra text.`,
placeholder: 'Describe the alert...',
},
},
{
id: 'folderUid',
title: 'Folder UID',
type: 'short-input',
placeholder: 'Folder UID for the alert rule',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'ruleGroup',
title: 'Rule Group',
@@ -380,10 +409,105 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
title: 'Error State',
type: 'dropdown',
options: [
{ label: 'Error', id: 'Error' },
{ label: 'Alerting', id: 'Alerting' },
{ label: 'OK', id: 'OK' },
],
value: () => 'Alerting',
value: () => 'Error',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'annotations',
title: 'Annotations (JSON)',
type: 'long-input',
placeholder: 'JSON object of alert annotations (e.g., {"summary":"..."})',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'labels',
title: 'Labels (JSON)',
type: 'long-input',
placeholder: 'JSON object of alert labels (e.g., {"severity":"critical"})',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'isPaused',
title: 'Paused',
type: 'switch',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'keepFiringFor',
title: 'Keep Firing For',
type: 'short-input',
placeholder: 'e.g., 5m',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'missingSeriesEvalsToResolve',
title: 'Missing Series Evals to Resolve',
type: 'short-input',
placeholder: 'e.g., 2',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'notificationSettings',
title: 'Notification Settings (JSON)',
type: 'long-input',
placeholder: 'JSON object of per-rule notification overrides',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'record',
title: 'Recording Rule (JSON)',
type: 'long-input',
placeholder: 'JSON object configuring this as a recording rule',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
},
},
{
id: 'alertRuleUidNew',
title: 'Custom Alert Rule UID',
type: 'short-input',
placeholder: 'Optional - auto-generated if not provided',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_create_alert_rule' },
},
{
id: 'disableProvenance',
title: 'Disable Provenance',
type: 'switch',
mode: 'advanced',
condition: {
field: 'operation',
value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'],
@@ -396,7 +520,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`,
title: 'Annotation Text',
type: 'long-input',
placeholder: 'Enter annotation text...',
required: true,
required: { field: 'operation', value: 'grafana_create_annotation' },
condition: {
field: 'operation',
value: ['grafana_create_annotation', 'grafana_update_annotation'],
@@ -436,8 +560,7 @@ Return ONLY the annotation text - no explanations, no quotes, no extra text.`,
id: 'annotationDashboardUid',
title: 'Dashboard UID',
type: 'short-input',
placeholder: 'Enter dashboard UID',
required: true,
placeholder: 'Optional - omit for organization-wide annotations',
condition: {
field: 'operation',
value: ['grafana_create_annotation', 'grafana_list_annotations'],
@@ -453,6 +576,22 @@ Return ONLY the annotation text - no explanations, no quotes, no extra text.`,
value: ['grafana_create_annotation', 'grafana_list_annotations'],
},
},
{
id: 'alertId',
title: 'Alert ID',
type: 'short-input',
placeholder: 'Filter by alert ID',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_annotations' },
},
{
id: 'userId',
title: 'User ID',
type: 'short-input',
placeholder: 'Filter by creator user ID',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_annotations' },
},
{
id: 'time',
title: 'Time (epoch ms)',
@@ -583,6 +722,30 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
placeholder: 'Optional - auto-generated if not provided',
condition: { field: 'operation', value: 'grafana_create_folder' },
},
{
id: 'parentUidNew',
title: 'Parent Folder UID',
type: 'short-input',
placeholder: 'Optional - for nested folders',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_create_folder' },
},
{
id: 'parentUidList',
title: 'Parent Folder UID',
type: 'short-input',
placeholder: 'List children of this folder UID',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_folders' },
},
{
id: 'contactPointName',
title: 'Contact Point Name',
type: 'short-input',
placeholder: 'Filter by exact name',
mode: 'advanced',
condition: { field: 'operation', value: 'grafana_list_contact_points' },
},
],
tools: {
access: [
@@ -607,22 +770,30 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
'grafana_create_folder',
],
config: {
tool: (params) => {
if (params.alertTitle) params.title = params.alertTitle
if (params.folderTitle) params.title = params.folderTitle
if (params.folderUidNew) params.uid = params.folderUidNew
if (params.annotationTags) params.tags = params.annotationTags
if (params.annotationDashboardUid) params.dashboardUid = params.annotationDashboardUid
return params.operation
},
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)
}
return result
},
},
@@ -641,8 +812,15 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
message: { type: 'string', description: 'Commit message' },
query: { type: 'string', description: 'Search query' },
tag: { type: 'string', description: 'Filter by tag' },
folderUIDs: {
type: 'string',
description: 'Filter dashboards by folder UIDs (comma-separated)',
},
dashboardUIDs: { type: 'string', description: 'Filter by dashboard UIDs (comma-separated)' },
page: { type: 'number', description: 'Page number for pagination' },
// Alert inputs
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' },
ruleGroup: { type: 'string', description: 'Rule group name' },
condition: { type: 'string', description: 'Alert condition refId' },
@@ -650,14 +828,46 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
forDuration: { type: 'string', description: 'Duration before firing' },
noDataState: { type: 'string', description: 'State on no data' },
execErrState: { type: 'string', description: 'State on error' },
isPaused: { type: 'boolean', description: 'Whether the alert rule is paused' },
keepFiringFor: {
type: 'string',
description: 'Duration to keep firing after the condition stops',
},
missingSeriesEvalsToResolve: {
type: 'number',
description: 'Missing series evaluations before resolving',
},
notificationSettings: {
type: 'string',
description: 'JSON of per-rule notification settings',
},
record: { type: 'string', description: 'JSON of recording rule configuration' },
disableProvenance: {
type: 'boolean',
description: 'Disable provenance tracking so the rule remains UI-editable',
},
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)' },
annotationDashboardUid: { type: 'string', description: 'Annotation dashboard UID' },
panelId: { type: 'number', description: 'Panel ID' },
time: { type: 'number', description: 'Start time (epoch ms)' },
timeEnd: { type: 'number', description: 'End time (epoch ms)' },
from: { type: 'number', description: 'Filter from time' },
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
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
contactPointName: { type: 'string', description: 'Filter contact points by name' },
// Data source inputs
dataSourceId: { type: 'string', description: 'Data source ID or UID' },
},
@@ -675,6 +885,26 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`,
// Alert outputs
rules: { type: 'json', description: 'Alert rules list' },
contactPoints: { type: 'json', description: 'Contact points list' },
condition: { type: 'string', description: 'Alert condition refId' },
for: { type: 'string', description: 'Duration the condition must hold before firing' },
keepFiringFor: {
type: 'string',
description: 'Duration to keep firing after the condition stops',
},
missingSeriesEvalsToResolve: {
type: 'number',
description: 'Missing series evaluations before resolving',
},
isPaused: { type: 'boolean', description: 'Whether the alert rule is paused' },
folderUID: { type: 'string', description: 'Parent folder UID' },
ruleGroup: { type: 'string', description: 'Rule group name' },
orgID: { type: 'number', description: 'Organization ID' },
provenance: { type: 'string', description: 'Provisioning source' },
noDataState: { type: 'string', description: 'State on no data' },
execErrState: { type: 'string', description: 'State on execution error' },
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' },
+85 -52
View File
@@ -2,6 +2,8 @@ import type {
GrafanaCreateAlertRuleParams,
GrafanaCreateAlertRuleResponse,
} from '@/tools/grafana/types'
import { ALERT_RULE_OUTPUT_FIELDS } from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig } from '@/tools/types'
export const createAlertRuleTool: ToolConfig<
@@ -52,9 +54,10 @@ export const createAlertRuleTool: ToolConfig<
},
condition: {
type: 'string',
required: true,
required: false,
visibility: 'user-or-llm',
description: 'The refId of the query or expression to use as the alert condition',
description:
'The refId of the query or expression to use as the alert condition (required for alerting rules; omit for recording rules)',
},
data: {
type: 'string',
@@ -78,7 +81,7 @@ export const createAlertRuleTool: ToolConfig<
type: 'string',
required: false,
visibility: 'user-only',
description: 'State on execution error (Alerting, OK)',
description: 'State on execution error (Error, Alerting, OK)',
},
annotations: {
type: 'string',
@@ -92,6 +95,48 @@ export const createAlertRuleTool: ToolConfig<
visibility: 'user-or-llm',
description: 'JSON object of labels',
},
uid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional custom UID for the alert rule',
},
isPaused: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether the rule is paused on creation',
},
keepFiringFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Duration to keep firing after the condition stops (e.g., 5m)',
},
missingSeriesEvalsToResolve: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Number of missing series evaluations before resolving',
},
notificationSettings: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'JSON object of per-rule notification settings (overrides)',
},
record: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object configuring this as a recording rule (omit for alerting rules)',
},
disableProvenance: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI',
},
},
request: {
@@ -105,32 +150,43 @@ export const createAlertRuleTool: ToolConfig<
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
if (params.disableProvenance) {
headers['X-Disable-Provenance'] = 'true'
}
return headers
},
body: (params) => {
let dataArray: any[] = []
let dataArray: unknown[] = []
try {
dataArray = JSON.parse(params.data)
} catch {
throw new Error('Invalid JSON for data parameter')
}
const body: Record<string, any> = {
const body: Record<string, unknown> = {
title: params.title,
folderUID: params.folderUid,
ruleGroup: params.ruleGroup,
condition: params.condition,
data: dataArray,
for: params.forDuration || '5m',
noDataState: params.noDataState || 'NoData',
execErrState: params.execErrState || 'Alerting',
}
if (params.organizationId) body.orgID = Number(params.organizationId)
if (params.condition) body.condition = params.condition
if (params.uid) body.uid = params.uid
if (params.forDuration) body.for = params.forDuration
if (params.noDataState) body.noDataState = params.noDataState
if (params.execErrState) body.execErrState = params.execErrState
if (params.isPaused !== undefined) body.isPaused = params.isPaused
if (params.keepFiringFor) body.keep_firing_for = params.keepFiringFor
if (params.missingSeriesEvalsToResolve !== undefined) {
body.missingSeriesEvalsToResolve = params.missingSeriesEvalsToResolve
}
if (params.annotations) {
try {
body.annotations = JSON.parse(params.annotations)
} catch {
body.annotations = {}
throw new Error('Invalid JSON for annotations parameter')
}
}
@@ -138,7 +194,23 @@ export const createAlertRuleTool: ToolConfig<
try {
body.labels = JSON.parse(params.labels)
} catch {
body.labels = {}
throw new Error('Invalid JSON for labels parameter')
}
}
if (params.notificationSettings) {
try {
body.notification_settings = JSON.parse(params.notificationSettings)
} catch {
throw new Error('Invalid JSON for notificationSettings parameter')
}
}
if (params.record) {
try {
body.record = JSON.parse(params.record)
} catch {
throw new Error('Invalid JSON for record parameter')
}
}
@@ -148,47 +220,8 @@ export const createAlertRuleTool: ToolConfig<
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
uid: data.uid,
title: data.title,
condition: data.condition,
data: data.data,
updated: data.updated,
noDataState: data.noDataState,
execErrState: data.execErrState,
for: data.for,
annotations: data.annotations || {},
labels: data.labels || {},
isPaused: data.isPaused || false,
folderUID: data.folderUID,
ruleGroup: data.ruleGroup,
orgId: data.orgId,
namespace_uid: data.namespace_uid,
namespace_id: data.namespace_id,
provenance: data.provenance || '',
},
}
return { success: true, output: mapAlertRule(data) }
},
outputs: {
uid: {
type: 'string',
description: 'The UID of the created alert rule',
},
title: {
type: 'string',
description: 'Alert rule title',
},
folderUID: {
type: 'string',
description: 'Parent folder UID',
},
ruleGroup: {
type: 'string',
description: 'Rule group name',
},
},
outputs: ALERT_RULE_OUTPUT_FIELDS,
}
+9 -16
View File
@@ -46,9 +46,10 @@ export const createAnnotationTool: ToolConfig<
},
dashboardUid: {
type: 'string',
required: true,
required: false,
visibility: 'user-or-llm',
description: 'UID of the dashboard to add the annotation to (e.g., abc123def)',
description:
'UID of the dashboard to add the annotation to (e.g., abc123def). Omit to create a global organization annotation.',
},
panelId: {
type: 'number',
@@ -84,11 +85,15 @@ export const createAnnotationTool: ToolConfig<
return headers
},
body: (params) => {
const body: Record<string, any> = {
const body: Record<string, unknown> = {
text: params.text,
time: params.time || Date.now(),
}
if (params.time) body.time = params.time
if (params.timeEnd) body.timeEnd = params.timeEnd
if (params.dashboardUid) body.dashboardUID = params.dashboardUid
if (params.panelId) body.panelId = params.panelId
if (params.tags) {
body.tags = params.tags
.split(',')
@@ -96,18 +101,6 @@ export const createAnnotationTool: ToolConfig<
.filter((t) => t)
}
if (params.dashboardUid) {
body.dashboardUID = params.dashboardUid
}
if (params.panelId) {
body.panelId = params.panelId
}
if (params.timeEnd) {
body.timeEnd = params.timeEnd
}
return body
},
},
+44 -39
View File
@@ -39,6 +39,12 @@ export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCrea
visibility: 'user-or-llm',
description: 'Optional UID for the folder (auto-generated if not provided)',
},
parentUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Parent folder UID for nested folders (requires nested folders enabled)',
},
},
request: {
@@ -55,13 +61,12 @@ export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCrea
return headers
},
body: (params) => {
const body: Record<string, any> = {
const body: Record<string, unknown> = {
title: params.title,
}
if (params.uid) {
body.uid = params.uid
}
if (params.uid) body.uid = params.uid
if (params.parentUid) body.parentUid = params.parentUid
return body
},
@@ -73,80 +78,80 @@ export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCrea
return {
success: true,
output: {
id: data.id,
uid: data.uid,
title: data.title,
url: data.url,
hasAcl: data.hasAcl || false,
canSave: data.canSave || false,
canEdit: data.canEdit || false,
canAdmin: data.canAdmin || false,
canDelete: data.canDelete || false,
createdBy: data.createdBy || '',
created: data.created || '',
updatedBy: data.updatedBy || '',
updated: data.updated || '',
version: data.version || 0,
id: (data.id as number) ?? null,
uid: (data.uid as string) ?? null,
title: (data.title as string) ?? null,
url: (data.url as string) ?? null,
parentUid: (data.parentUid as string) ?? null,
parents: (data.parents as { uid: string; title: string; url: string }[]) ?? [],
hasAcl: (data.hasAcl as boolean) ?? null,
canSave: (data.canSave as boolean) ?? null,
canEdit: (data.canEdit as boolean) ?? null,
canAdmin: (data.canAdmin as boolean) ?? null,
createdBy: (data.createdBy as string) ?? null,
created: (data.created as string) ?? null,
updatedBy: (data.updatedBy as string) ?? null,
updated: (data.updated as string) ?? null,
version: (data.version as number) ?? null,
},
}
},
outputs: {
id: {
type: 'number',
description: 'The numeric ID of the created folder',
},
uid: {
id: { type: 'number', description: 'The numeric ID of the created folder' },
uid: { type: 'string', description: 'The UID of the created folder' },
title: { type: 'string', description: 'The title of the created folder' },
url: { type: 'string', description: 'The URL path to the folder', optional: true },
parentUid: {
type: 'string',
description: 'The UID of the created folder',
description: 'Parent folder UID (nested folders only)',
optional: true,
},
title: {
type: 'string',
description: 'The title of the created folder',
},
url: {
type: 'string',
description: 'The URL path to the folder',
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',
},
canDelete: {
type: 'boolean',
description: 'Whether the current user can delete 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',
},
version: { type: 'number', description: 'Version number of the folder', optional: true },
},
}
+8 -66
View File
@@ -1,4 +1,9 @@
import type { GrafanaGetAlertRuleParams, GrafanaGetAlertRuleResponse } from '@/tools/grafana/types'
import {
ALERT_RULE_OUTPUT_FIELDS,
type GrafanaGetAlertRuleParams,
type GrafanaGetAlertRuleResponse,
} from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig } from '@/tools/types'
export const getAlertRuleTool: ToolConfig<GrafanaGetAlertRuleParams, GrafanaGetAlertRuleResponse> =
@@ -53,71 +58,8 @@ export const getAlertRuleTool: ToolConfig<GrafanaGetAlertRuleParams, GrafanaGetA
transformResponse: async (response: Response) => {
const data = await response.json()
return {
success: true,
output: {
uid: data.uid,
title: data.title,
condition: data.condition,
data: data.data,
updated: data.updated,
noDataState: data.noDataState,
execErrState: data.execErrState,
for: data.for,
annotations: data.annotations || {},
labels: data.labels || {},
isPaused: data.isPaused || false,
folderUID: data.folderUID,
ruleGroup: data.ruleGroup,
orgId: data.orgId,
namespace_uid: data.namespace_uid,
namespace_id: data.namespace_id,
provenance: data.provenance || '',
},
}
return { success: true, output: mapAlertRule(data) }
},
outputs: {
uid: {
type: 'string',
description: 'Alert rule UID',
},
title: {
type: 'string',
description: 'Alert rule title',
},
condition: {
type: 'string',
description: 'Alert condition',
},
data: {
type: 'json',
description: 'Alert rule query data',
},
folderUID: {
type: 'string',
description: 'Parent folder UID',
},
ruleGroup: {
type: 'string',
description: 'Rule group name',
},
noDataState: {
type: 'string',
description: 'State when no data is returned',
},
execErrState: {
type: 'string',
description: 'State on execution error',
},
annotations: {
type: 'json',
description: 'Alert annotations',
},
labels: {
type: 'json',
description: 'Alert labels',
},
},
outputs: ALERT_RULE_OUTPUT_FIELDS,
}
+48 -49
View File
@@ -43,12 +43,14 @@ export const getDataSourceTool: ToolConfig<
request: {
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
// Check if it looks like a UID (contains non-numeric characters) or ID
const isUid = /[^0-9]/.test(params.dataSourceId)
if (isUid) {
return `${baseUrl}/api/datasources/uid/${params.dataSourceId}`
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}`
}
return `${baseUrl}/api/datasources/${params.dataSourceId}`
return `${baseUrl}/api/datasources/uid/${id}`
},
method: 'GET',
headers: (params) => {
@@ -69,57 +71,54 @@ export const getDataSourceTool: ToolConfig<
return {
success: true,
output: {
id: data.id,
uid: data.uid,
orgId: data.orgId,
name: data.name,
type: data.type,
typeName: data.typeName,
typeLogoUrl: data.typeLogoUrl,
access: data.access,
url: data.url,
user: data.user,
database: data.database,
basicAuth: data.basicAuth || false,
isDefault: data.isDefault || false,
jsonData: data.jsonData || {},
readOnly: data.readOnly || false,
id: (data.id as number) ?? null,
uid: (data.uid as string) ?? null,
orgId: (data.orgId as number) ?? null,
name: (data.name as string) ?? null,
type: (data.type as string) ?? null,
typeLogoUrl: (data.typeLogoUrl as string) ?? null,
access: (data.access as string) ?? null,
url: (data.url as string) ?? null,
user: (data.user as string) ?? null,
database: (data.database as string) ?? null,
basicAuth: (data.basicAuth as boolean) ?? false,
basicAuthUser: (data.basicAuthUser as string) ?? null,
withCredentials: (data.withCredentials as boolean) ?? null,
isDefault: (data.isDefault as boolean) ?? false,
jsonData: (data.jsonData as Record<string, unknown>) ?? {},
secureJsonFields: (data.secureJsonFields as Record<string, boolean>) ?? {},
version: (data.version as number) ?? null,
readOnly: (data.readOnly as boolean) ?? false,
},
}
},
outputs: {
id: {
type: 'number',
description: 'Data source ID',
},
uid: {
type: 'string',
description: 'Data source UID',
},
name: {
type: 'string',
description: 'Data source name',
},
type: {
type: 'string',
description: 'Data source type',
},
url: {
type: 'string',
description: 'Data source connection URL',
},
database: {
type: 'string',
description: 'Database name (if applicable)',
},
isDefault: {
id: { type: 'number', description: 'Data source ID' },
uid: { type: 'string', description: 'Data source UID' },
orgId: { type: 'number', description: 'Organization ID' },
name: { type: 'string', description: 'Data source name' },
type: { type: 'string', description: 'Data source type' },
typeLogoUrl: { type: 'string', description: 'Logo URL for the data source type' },
access: { type: 'string', description: 'Access mode (proxy or direct)' },
url: { type: 'string', description: 'Data source connection URL' },
user: { type: 'string', description: 'Username used to connect' },
database: { type: 'string', description: 'Database name (if applicable)' },
basicAuth: { type: 'boolean', description: 'Whether basic auth is enabled' },
basicAuthUser: { type: 'string', description: 'Basic auth username', optional: true },
withCredentials: {
type: 'boolean',
description: 'Whether this is the default data source',
description: 'Whether to send credentials with cross-origin requests',
optional: true,
},
jsonData: {
type: 'json',
description: 'Additional data source configuration',
isDefault: { type: 'boolean', description: 'Whether this is the default data source' },
jsonData: { type: 'json', description: 'Additional data source configuration' },
secureJsonFields: {
type: 'object',
description: 'Map of secure fields that are set (values are not returned)',
optional: true,
},
version: { type: 'number', description: 'Data source version', optional: true },
readOnly: { type: 'boolean', description: 'Whether the data source is read-only' },
},
}
+7 -31
View File
@@ -1,7 +1,9 @@
import type {
GrafanaListAlertRulesParams,
GrafanaListAlertRulesResponse,
import {
ALERT_RULE_OUTPUT_FIELDS,
type GrafanaListAlertRulesParams,
type GrafanaListAlertRulesResponse,
} from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig } from '@/tools/types'
export const listAlertRulesTool: ToolConfig<
@@ -56,25 +58,7 @@ export const listAlertRulesTool: ToolConfig<
success: true,
output: {
rules: Array.isArray(data)
? data.map((rule: any) => ({
uid: rule.uid,
title: rule.title,
condition: rule.condition,
data: rule.data,
updated: rule.updated,
noDataState: rule.noDataState,
execErrState: rule.execErrState,
for: rule.for,
annotations: rule.annotations || {},
labels: rule.labels || {},
isPaused: rule.isPaused || false,
folderUID: rule.folderUID,
ruleGroup: rule.ruleGroup,
orgId: rule.orgId,
namespace_uid: rule.namespace_uid,
namespace_id: rule.namespace_id,
provenance: rule.provenance || '',
}))
? data.map((rule: Record<string, unknown>) => mapAlertRule(rule))
: [],
},
}
@@ -86,15 +70,7 @@ export const listAlertRulesTool: ToolConfig<
description: 'List of alert rules',
items: {
type: 'object',
properties: {
uid: { type: 'string', description: 'Alert rule UID' },
title: { type: 'string', description: 'Alert rule title' },
condition: { type: 'string', description: 'Alert condition' },
folderUID: { type: 'string', description: 'Parent folder UID' },
ruleGroup: { type: 'string', description: 'Rule group name' },
noDataState: { type: 'string', description: 'State when no data is returned' },
execErrState: { type: 'string', description: 'State on execution error' },
},
properties: ALERT_RULE_OUTPUT_FIELDS,
},
},
},
+69 -32
View File
@@ -46,9 +46,16 @@ export const listAnnotationsTool: ToolConfig<
},
dashboardUid: {
type: 'string',
required: true,
required: false,
visibility: 'user-or-llm',
description: 'Dashboard UID to query annotations from (e.g., abc123def)',
description:
'Dashboard UID to query annotations from (e.g., abc123def). Omit to query annotations across the organization.',
},
dashboardId: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Legacy numeric dashboard ID filter (prefer dashboardUid)',
},
panelId: {
type: 'number',
@@ -56,6 +63,18 @@ export const listAnnotationsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Filter by panel ID (e.g., 1, 2)',
},
alertId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Filter by alert ID',
},
userId: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Filter by ID of the user who created the annotation',
},
tags: {
type: 'string',
required: false,
@@ -84,7 +103,10 @@ export const listAnnotationsTool: ToolConfig<
if (params.from) searchParams.set('from', String(params.from))
if (params.to) searchParams.set('to', String(params.to))
if (params.dashboardUid) searchParams.set('dashboardUID', params.dashboardUid)
if (params.dashboardId) searchParams.set('dashboardId', String(params.dashboardId))
if (params.panelId) searchParams.set('panelId', String(params.panelId))
if (params.alertId) searchParams.set('alertId', String(params.alertId))
if (params.userId) searchParams.set('userId', String(params.userId))
if (params.tags) {
params.tags.split(',').forEach((t) => searchParams.append('tags', t.trim()))
}
@@ -109,27 +131,27 @@ export const listAnnotationsTool: ToolConfig<
transformResponse: async (response: Response) => {
const data = await response.json()
// Handle potential nested array structure
const rawAnnotations = Array.isArray(data) ? data.flat() : []
return {
success: true,
output: {
annotations: rawAnnotations.map((a: any) => ({
id: a.id,
dashboardId: a.dashboardId,
dashboardUID: a.dashboardUID,
created: a.created,
updated: a.updated,
time: a.time,
timeEnd: a.timeEnd,
text: a.text,
tags: a.tags || [],
login: a.login,
email: a.email,
avatarUrl: a.avatarUrl,
data: a.data || {},
annotations: rawAnnotations.map((a: Record<string, unknown>) => ({
id: (a.id as number) ?? null,
alertId: (a.alertId as number) ?? null,
dashboardId: (a.dashboardId as number) ?? null,
dashboardUID: (a.dashboardUID as string) ?? null,
panelId: (a.panelId as number) ?? null,
userId: (a.userId as number) ?? null,
userName: (a.userName as string) ?? null,
newState: (a.newState as string) ?? null,
prevState: (a.prevState as string) ?? null,
time: (a.time as number) ?? null,
timeEnd: (a.timeEnd as number) ?? null,
text: (a.text as string) ?? null,
metric: (a.metric as string) ?? null,
tags: (a.tags as string[]) ?? [],
data: (a.data as Record<string, unknown>) ?? {},
})),
},
}
@@ -143,21 +165,36 @@ export const listAnnotationsTool: ToolConfig<
type: 'object',
properties: {
id: { type: 'number', description: 'Annotation ID' },
dashboardId: { type: 'number', description: 'Dashboard ID' },
dashboardUID: { type: 'string', description: 'Dashboard UID' },
created: { type: 'number', description: 'Creation timestamp in epoch ms' },
updated: { type: 'number', description: 'Last update timestamp in epoch ms' },
time: { type: 'number', description: 'Start time in epoch ms' },
timeEnd: { type: 'number', description: 'End time in epoch ms' },
text: { type: 'string', description: 'Annotation text' },
tags: { type: 'array', items: { type: 'string' }, description: 'Annotation tags' },
login: { type: 'string', description: 'Login of the user who created the annotation' },
email: { type: 'string', description: 'Email of the user who created the annotation' },
avatarUrl: { type: 'string', description: 'Avatar URL of the user' },
data: {
type: 'json',
description: 'Additional annotation data object from Grafana',
alertId: { type: 'number', description: 'Associated alert ID (0 if not alert-driven)' },
dashboardId: { type: 'number', description: 'Dashboard ID', optional: true },
dashboardUID: { type: 'string', description: 'Dashboard UID', optional: true },
panelId: { type: 'number', description: 'Panel ID within the dashboard', optional: true },
userId: { type: 'number', description: 'ID of the user who created the annotation' },
userName: {
type: 'string',
description: 'Username of the user who created the annotation',
optional: true,
},
newState: {
type: 'string',
description: 'New alert state (alert annotations only)',
optional: true,
},
prevState: {
type: 'string',
description: 'Previous alert state (alert annotations only)',
optional: true,
},
time: { type: 'number', description: 'Start time in epoch ms' },
timeEnd: { type: 'number', description: 'End time in epoch ms', optional: true },
text: { type: 'string', description: 'Annotation text' },
metric: {
type: 'string',
description: 'Metric associated with the annotation',
optional: true,
},
tags: { type: 'array', items: { type: 'string' }, description: 'Annotation tags' },
data: { type: 'json', description: 'Additional annotation data object from Grafana' },
},
},
},
+28 -8
View File
@@ -32,10 +32,22 @@ export const listContactPointsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)',
},
name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter contact points by exact name match',
},
},
request: {
url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`,
url: (params) => {
const baseUrl = params.baseUrl.replace(/\/$/, '')
const searchParams = new URLSearchParams()
if (params.name) searchParams.set('name', params.name)
const queryString = searchParams.toString()
return `${baseUrl}/api/v1/provisioning/contact-points${queryString ? `?${queryString}` : ''}`
},
method: 'GET',
headers: (params) => {
const headers: Record<string, string> = {
@@ -56,13 +68,13 @@ export const listContactPointsTool: ToolConfig<
success: true,
output: {
contactPoints: Array.isArray(data)
? data.map((cp: any) => ({
uid: cp.uid,
name: cp.name,
type: cp.type,
settings: cp.settings || {},
disableResolveMessage: cp.disableResolveMessage || false,
provenance: cp.provenance || '',
? data.map((cp: Record<string, unknown>) => ({
uid: (cp.uid as string) ?? null,
name: (cp.name as string) ?? null,
type: (cp.type as string) ?? null,
settings: (cp.settings as Record<string, unknown>) ?? {},
disableResolveMessage: (cp.disableResolveMessage as boolean) ?? false,
provenance: (cp.provenance as string) ?? '',
}))
: [],
},
@@ -80,6 +92,14 @@ export const listContactPointsTool: ToolConfig<
name: { type: 'string', description: 'Contact point name' },
type: { type: 'string', description: 'Notification type (email, slack, etc.)' },
settings: { type: 'object', description: 'Type-specific settings' },
disableResolveMessage: {
type: 'boolean',
description: 'Whether resolve messages are disabled',
},
provenance: {
type: 'string',
description: 'Provisioning source (empty if API-managed)',
},
},
},
},
+36 -20
View File
@@ -44,11 +44,17 @@ export const listDashboardsTool: ToolConfig<
visibility: 'user-or-llm',
description: 'Filter by tag (comma-separated for multiple tags)',
},
folderIds: {
folderUIDs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by folder IDs (comma-separated, e.g., 1,2,3)',
description: 'Filter by folder UIDs (comma-separated, e.g., abc123,def456)',
},
dashboardUIDs: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter by dashboard UIDs (comma-separated, e.g., abc123,def456)',
},
starred: {
type: 'boolean',
@@ -60,7 +66,13 @@ export const listDashboardsTool: ToolConfig<
type: 'number',
required: false,
visibility: 'user-only',
description: 'Maximum number of dashboards to return',
description: 'Maximum number of dashboards to return (default 1000)',
},
page: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Page number for pagination (1-based)',
},
},
@@ -74,11 +86,17 @@ export const listDashboardsTool: ToolConfig<
if (params.tag) {
params.tag.split(',').forEach((t) => searchParams.append('tag', t.trim()))
}
if (params.folderIds) {
params.folderIds.split(',').forEach((id) => searchParams.append('folderIds', id.trim()))
if (params.folderUIDs) {
params.folderUIDs.split(',').forEach((uid) => searchParams.append('folderUIDs', uid.trim()))
}
if (params.dashboardUIDs) {
params.dashboardUIDs
.split(',')
.forEach((uid) => searchParams.append('dashboardUIDs', uid.trim()))
}
if (params.starred) searchParams.set('starred', 'true')
if (params.limit) searchParams.set('limit', String(params.limit))
if (params.page) searchParams.set('page', String(params.page))
return `${baseUrl}/api/search?${searchParams.toString()}`
},
@@ -102,21 +120,19 @@ export const listDashboardsTool: ToolConfig<
success: true,
output: {
dashboards: Array.isArray(data)
? data.map((d: any) => ({
id: d.id,
uid: d.uid,
title: d.title,
uri: d.uri,
url: d.url,
slug: d.slug,
type: d.type,
tags: d.tags || [],
isStarred: d.isStarred || false,
folderId: d.folderId,
folderUid: d.folderUid,
folderTitle: d.folderTitle,
folderUrl: d.folderUrl,
sortMeta: d.sortMeta,
? data.map((d: Record<string, unknown>) => ({
id: (d.id as number) ?? null,
uid: (d.uid as string) ?? null,
title: (d.title as string) ?? null,
uri: (d.uri as string) ?? null,
url: (d.url as string) ?? null,
type: (d.type as string) ?? null,
tags: (d.tags as string[]) ?? [],
isStarred: (d.isStarred as boolean) ?? false,
folderId: (d.folderId as number) ?? null,
folderUid: (d.folderUid as string) ?? null,
folderTitle: (d.folderTitle as string) ?? null,
folderUrl: (d.folderUrl as string) ?? null,
}))
: [],
},
+43 -16
View File
@@ -56,22 +56,25 @@ export const listDataSourcesTool: ToolConfig<
success: true,
output: {
dataSources: Array.isArray(data)
? data.map((ds: any) => ({
id: ds.id,
uid: ds.uid,
orgId: ds.orgId,
name: ds.name,
type: ds.type,
typeName: ds.typeName,
typeLogoUrl: ds.typeLogoUrl,
access: ds.access,
url: ds.url,
user: ds.user,
database: ds.database,
basicAuth: ds.basicAuth || false,
isDefault: ds.isDefault || false,
jsonData: ds.jsonData || {},
readOnly: ds.readOnly || false,
? data.map((ds: Record<string, unknown>) => ({
id: (ds.id as number) ?? null,
uid: (ds.uid as string) ?? null,
orgId: (ds.orgId as number) ?? null,
name: (ds.name as string) ?? null,
type: (ds.type as string) ?? null,
typeLogoUrl: (ds.typeLogoUrl as string) ?? null,
access: (ds.access as string) ?? null,
url: (ds.url as string) ?? null,
user: (ds.user as string) ?? null,
database: (ds.database as string) ?? null,
basicAuth: (ds.basicAuth as boolean) ?? false,
basicAuthUser: (ds.basicAuthUser as string) ?? null,
withCredentials: (ds.withCredentials as boolean) ?? null,
isDefault: (ds.isDefault as boolean) ?? false,
jsonData: (ds.jsonData as Record<string, unknown>) ?? {},
secureJsonFields: (ds.secureJsonFields as Record<string, boolean>) ?? {},
version: (ds.version as number) ?? null,
readOnly: (ds.readOnly as boolean) ?? false,
}))
: [],
},
@@ -87,10 +90,34 @@ export const listDataSourcesTool: ToolConfig<
properties: {
id: { type: 'number', description: 'Data source ID' },
uid: { type: 'string', description: 'Data source UID' },
orgId: { type: 'number', description: 'Organization ID' },
name: { type: 'string', description: 'Data source name' },
type: { type: 'string', description: 'Data source type (prometheus, mysql, etc.)' },
typeLogoUrl: { type: 'string', description: 'Logo URL for the data source type' },
access: { type: 'string', description: 'Access mode (proxy or direct)' },
url: { type: 'string', description: 'Data source URL' },
user: { type: 'string', description: 'Username used to connect' },
database: { type: 'string', description: 'Database name (if applicable)' },
basicAuth: { type: 'boolean', description: 'Whether basic auth is enabled' },
basicAuthUser: {
type: 'string',
description: 'Basic auth username',
optional: true,
},
withCredentials: {
type: 'boolean',
description: 'Whether to send credentials with cross-origin requests',
optional: true,
},
isDefault: { type: 'boolean', description: 'Whether this is the default data source' },
jsonData: { type: 'object', description: 'Type-specific JSON configuration' },
secureJsonFields: {
type: 'object',
description: 'Map of secure fields that are set (values are not returned)',
optional: true,
},
version: { type: 'number', description: 'Data source version', optional: true },
readOnly: { type: 'boolean', description: 'Whether the data source is read-only' },
},
},
},
+74 -26
View File
@@ -38,6 +38,12 @@ export const listFoldersTool: ToolConfig<GrafanaListFoldersParams, GrafanaListFo
visibility: 'user-only',
description: 'Page number for pagination',
},
parentUid: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'List children of this folder UID (requires nested folders enabled)',
},
},
request: {
@@ -47,6 +53,7 @@ export const listFoldersTool: ToolConfig<GrafanaListFoldersParams, GrafanaListFo
if (params.limit) searchParams.set('limit', String(params.limit))
if (params.page) searchParams.set('page', String(params.page))
if (params.parentUid) searchParams.set('parentUid', params.parentUid.trim())
const queryString = searchParams.toString()
return `${baseUrl}/api/folders${queryString ? `?${queryString}` : ''}`
@@ -71,20 +78,22 @@ export const listFoldersTool: ToolConfig<GrafanaListFoldersParams, GrafanaListFo
success: true,
output: {
folders: Array.isArray(data)
? data.map((f: any) => ({
id: f.id,
uid: f.uid,
title: f.title,
hasAcl: f.hasAcl || false,
canSave: f.canSave || false,
canEdit: f.canEdit || false,
canAdmin: f.canAdmin || false,
canDelete: f.canDelete || false,
createdBy: f.createdBy || '',
created: f.created || '',
updatedBy: f.updatedBy || '',
updated: f.updated || '',
version: f.version || 0,
? data.map((f: Record<string, unknown>) => ({
id: (f.id as number) ?? null,
uid: (f.uid as string) ?? null,
title: (f.title as string) ?? null,
url: (f.url as string) ?? null,
parentUid: (f.parentUid as string) ?? null,
parents: (f.parents as { uid: string; title: string; url: string }[]) ?? [],
hasAcl: (f.hasAcl as boolean) ?? null,
canSave: (f.canSave as boolean) ?? null,
canEdit: (f.canEdit as boolean) ?? null,
canAdmin: (f.canAdmin as boolean) ?? null,
createdBy: (f.createdBy as string) ?? null,
created: (f.created as string) ?? null,
updatedBy: (f.updatedBy as string) ?? null,
updated: (f.updated as string) ?? null,
version: (f.version as number) ?? null,
}))
: [],
},
@@ -101,19 +110,58 @@ export const listFoldersTool: ToolConfig<GrafanaListFoldersParams, GrafanaListFo
id: { type: 'number', description: 'Folder ID' },
uid: { type: 'string', description: 'Folder UID' },
title: { type: 'string', description: 'Folder title' },
hasAcl: { type: 'boolean', description: 'Whether the folder has custom ACL permissions' },
canSave: { type: 'boolean', description: 'Whether the current user can save the folder' },
canEdit: { type: 'boolean', description: 'Whether the current user can edit the folder' },
canAdmin: { type: 'boolean', description: 'Whether the current user has admin rights' },
canDelete: {
type: 'boolean',
description: 'Whether the current user can delete the folder',
url: { type: 'string', description: 'Folder URL path', optional: true },
parentUid: {
type: 'string',
description: 'Parent folder UID (nested folders only)',
optional: true,
},
createdBy: { type: 'string', description: 'Username of who created the folder' },
created: { type: 'string', description: 'Timestamp when the folder was created' },
updatedBy: { type: 'string', description: 'Username of who last updated the folder' },
updated: { type: 'string', description: 'Timestamp when the folder was last updated' },
version: { type: 'number', description: 'Version number of the folder' },
parents: {
type: 'array',
description: 'Ancestor folder hierarchy (nested folders only)',
optional: true,
},
hasAcl: {
type: 'boolean',
description: 'Whether the folder has custom ACL permissions',
optional: true,
},
canSave: {
type: 'boolean',
description: 'Whether the current user can save the folder',
optional: true,
},
canEdit: {
type: 'boolean',
description: 'Whether the current user can edit the folder',
optional: true,
},
canAdmin: {
type: 'boolean',
description: 'Whether the current user has admin rights',
optional: true,
},
createdBy: {
type: 'string',
description: 'Username of who created the folder',
optional: true,
},
created: {
type: 'string',
description: 'Timestamp when the folder was created',
optional: true,
},
updatedBy: {
type: 'string',
description: 'Username of who last updated the folder',
optional: true,
},
updated: {
type: 'string',
description: 'Timestamp when the folder was last updated',
optional: true,
},
version: { type: 'number', description: 'Folder version number', optional: true },
},
},
},
+136 -58
View File
@@ -1,5 +1,47 @@
// Common types for Grafana API tools
import type { ToolResponse } from '@/tools/types'
import type { OutputProperty, ToolResponse } from '@/tools/types'
/**
* Canonical output schema fields shared across alert rule tools.
*/
export const ALERT_RULE_OUTPUT_FIELDS: Record<string, OutputProperty> = {
id: { type: 'number', description: 'Alert rule numeric ID', optional: true },
uid: { type: 'string', description: 'Alert rule UID' },
title: { type: 'string', description: 'Alert rule title' },
condition: { type: 'string', description: 'RefId of the query used as the alert condition' },
data: { type: 'json', description: 'Alert rule query/expression data array' },
updated: { type: 'string', description: 'Last update timestamp', optional: true },
noDataState: { type: 'string', description: 'State when no data is returned' },
execErrState: { type: 'string', description: 'State on execution error' },
for: { type: 'string', description: 'Duration the condition must hold before firing' },
keepFiringFor: {
type: 'string',
description: 'Duration to keep firing after condition stops',
optional: true,
},
missingSeriesEvalsToResolve: {
type: 'number',
description: 'Number of missing series evaluations before resolving',
optional: true,
},
annotations: { type: 'json', description: 'Alert annotations' },
labels: { type: 'json', description: 'Alert labels' },
isPaused: { type: 'boolean', description: 'Whether the rule is paused' },
folderUID: { type: 'string', description: 'Parent folder UID' },
ruleGroup: { type: 'string', description: 'Rule group name' },
orgID: { type: 'number', description: 'Organization ID' },
provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' },
notification_settings: {
type: 'json',
description: 'Per-rule notification settings (overrides)',
optional: true,
},
record: {
type: 'json',
description: 'Recording rule configuration (recording rules only)',
optional: true,
},
}
// Common parameters for all Grafana tools
interface GrafanaBaseParams {
@@ -69,9 +111,9 @@ interface GrafanaDashboard {
schemaVersion: number
version: number
refresh: string
panels: any[]
templating: any
annotations: any
panels: Record<string, unknown>[]
templating: Record<string, unknown>
annotations: Record<string, unknown>
time: {
from: string
to: string
@@ -88,26 +130,26 @@ export interface GrafanaGetDashboardResponse extends ToolResponse {
export interface GrafanaListDashboardsParams extends GrafanaBaseParams {
query?: string
tag?: string
folderIds?: string
folderUIDs?: string
dashboardUIDs?: string
starred?: boolean
limit?: number
page?: number
}
interface GrafanaDashboardSearchResult {
id: number
uid: string
title: string
uri: string
url: string
slug: string
type: string
id: number | null
uid: string | null
title: string | null
uri: string | null
url: string | null
type: string | null
tags: string[]
isStarred: boolean
folderId: number
folderUid: string
folderTitle: string
folderUrl: string
sortMeta: number
folderId: number | null
folderUid: string | null
folderTitle: string | null
folderUrl: string | null
}
export interface GrafanaListDashboardsResponse extends ToolResponse {
@@ -177,23 +219,26 @@ export interface GrafanaDeleteDashboardResponse extends ToolResponse {
export interface GrafanaListAlertRulesParams extends GrafanaBaseParams {}
interface GrafanaAlertRule {
uid: string
title: string
condition: string
data: any[]
updated: string
noDataState: string
execErrState: string
for: string
id: number | null
uid: string | null
title: string | null
condition: string | null
data: unknown[]
updated: string | null
noDataState: string | null
execErrState: string | null
for: string | null
keepFiringFor: string | null
missingSeriesEvalsToResolve: number | null
annotations: Record<string, string>
labels: Record<string, string>
isPaused: boolean
folderUID: string
ruleGroup: string
orgId: number
namespace_uid: string
namespace_id: number
folderUID: string | null
ruleGroup: string | null
orgID: number | null
provenance: string
notification_settings: Record<string, unknown> | null
record: Record<string, unknown> | null
}
export interface GrafanaListAlertRulesResponse extends ToolResponse {
@@ -214,13 +259,20 @@ export interface GrafanaCreateAlertRuleParams extends GrafanaBaseParams {
title: string
folderUid: string
ruleGroup: string
condition: string
condition?: string
data: string // JSON string of data array
forDuration?: string
noDataState?: string
execErrState?: string
annotations?: string // JSON string
labels?: string // JSON string
uid?: string
isPaused?: boolean
keepFiringFor?: string
missingSeriesEvalsToResolve?: number
notificationSettings?: string // JSON string
record?: string // JSON string
disableProvenance?: boolean
}
export interface GrafanaCreateAlertRuleResponse extends ToolResponse {
@@ -239,6 +291,12 @@ export interface GrafanaUpdateAlertRuleParams extends GrafanaBaseParams {
execErrState?: string
annotations?: string // JSON string
labels?: string // JSON string
isPaused?: boolean
keepFiringFor?: string
missingSeriesEvalsToResolve?: number
notificationSettings?: string // JSON string
record?: string // JSON string
disableProvenance?: boolean
}
interface GrafanaUpdateAlertRuleResponse extends ToolResponse {
@@ -266,19 +324,21 @@ export interface GrafanaCreateAnnotationParams extends GrafanaBaseParams {
}
interface GrafanaAnnotation {
id: number
dashboardId: number
dashboardUID: string
created: number
updated: number
time: number
timeEnd: number
text: string
id: number | null
alertId: number | null
dashboardId: number | null
dashboardUID: string | null
panelId: number | null
userId: number | null
userName: string | null
newState: string | null
prevState: string | null
time: number | null
timeEnd: number | null
text: string | null
metric: string | null
tags: string[]
login: string
email: string
avatarUrl: string
data: any
data: Record<string, unknown>
}
export interface GrafanaCreateAnnotationResponse extends ToolResponse {
@@ -291,8 +351,11 @@ export interface GrafanaCreateAnnotationResponse extends ToolResponse {
export interface GrafanaListAnnotationsParams extends GrafanaBaseParams {
from?: number
to?: number
dashboardId?: number
dashboardUid?: string
panelId?: number
alertId?: number
userId?: number
tags?: string // comma-separated
type?: string
limit?: number
@@ -306,7 +369,7 @@ export interface GrafanaListAnnotationsResponse extends ToolResponse {
export interface GrafanaUpdateAnnotationParams extends GrafanaBaseParams {
annotationId: number
text: string
text?: string
tags?: string // comma-separated
time?: number
timeEnd?: number
@@ -338,15 +401,18 @@ interface GrafanaDataSource {
orgId: number
name: string
type: string
typeName: string
typeLogoUrl: string
access: string
url: string
user: string
database: string
basicAuth: boolean
basicAuthUser?: string
withCredentials?: boolean
isDefault: boolean
jsonData: any
jsonData: Record<string, unknown>
secureJsonFields?: Record<string, boolean>
version?: number
readOnly: boolean
}
@@ -368,22 +434,31 @@ export interface GrafanaGetDataSourceResponse extends ToolResponse {
export interface GrafanaListFoldersParams extends GrafanaBaseParams {
limit?: number
page?: number
parentUid?: string
}
interface GrafanaFolderParent {
uid: string
title: string
url: string
}
interface GrafanaFolder {
id: number
uid: string
title: string
hasAcl: boolean
canSave: boolean
canEdit: boolean
canAdmin: boolean
canDelete: boolean
createdBy: string
created: string
updatedBy: string
updated: string
version: number
url?: string
hasAcl?: boolean
canSave?: boolean
canEdit?: boolean
canAdmin?: boolean
createdBy?: string
created?: string
updatedBy?: string
updated?: string
version?: number
parentUid?: string | null
parents?: GrafanaFolderParent[]
}
export interface GrafanaListFoldersResponse extends ToolResponse {
@@ -395,6 +470,7 @@ export interface GrafanaListFoldersResponse extends ToolResponse {
export interface GrafanaCreateFolderParams extends GrafanaBaseParams {
title: string
uid?: string
parentUid?: string
}
export interface GrafanaCreateFolderResponse extends ToolResponse {
@@ -402,13 +478,15 @@ export interface GrafanaCreateFolderResponse extends ToolResponse {
}
// Contact Points types
export interface GrafanaListContactPointsParams extends GrafanaBaseParams {}
export interface GrafanaListContactPointsParams extends GrafanaBaseParams {
name?: string
}
interface GrafanaContactPoint {
uid: string
name: string
type: string
settings: Record<string, any>
settings: Record<string, unknown>
disableResolveMessage: boolean
provenance: string
}
+89 -47
View File
@@ -1,4 +1,5 @@
import type { GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types'
import { ALERT_RULE_OUTPUT_FIELDS, type GrafanaUpdateAlertRuleParams } from '@/tools/grafana/types'
import { mapAlertRule } from '@/tools/grafana/utils'
import type { ToolConfig, ToolResponse } from '@/tools/types'
// Using ToolResponse for intermediate state since this tool fetches existing data first
@@ -79,7 +80,7 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
type: 'string',
required: false,
visibility: 'user-only',
description: 'State on execution error (Alerting, OK)',
description: 'State on execution error (Error, Alerting, OK)',
},
annotations: {
type: 'string',
@@ -93,6 +94,42 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
visibility: 'user-or-llm',
description: 'JSON object of labels',
},
isPaused: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Whether the rule is paused',
},
keepFiringFor: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Duration to keep firing after the condition stops (e.g., 5m)',
},
missingSeriesEvalsToResolve: {
type: 'number',
required: false,
visibility: 'user-only',
description: 'Number of missing series evaluations before resolving',
},
notificationSettings: {
type: 'string',
required: false,
visibility: 'user-only',
description: 'JSON object of per-rule notification settings (overrides)',
},
record: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'JSON object configuring this as a recording rule',
},
disableProvenance: {
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI',
},
},
request: {
@@ -136,7 +173,7 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
}
// Build the updated rule by merging existing data with new params
const updatedRule: Record<string, any> = {
const updatedRule: Record<string, unknown> = {
...existingRule,
}
@@ -148,12 +185,45 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
if (params.forDuration) updatedRule.for = params.forDuration
if (params.noDataState) updatedRule.noDataState = params.noDataState
if (params.execErrState) updatedRule.execErrState = params.execErrState
if (params.isPaused !== undefined) updatedRule.isPaused = params.isPaused
if (params.keepFiringFor) updatedRule.keep_firing_for = params.keepFiringFor
if (params.missingSeriesEvalsToResolve !== undefined) {
updatedRule.missingSeriesEvalsToResolve = params.missingSeriesEvalsToResolve
}
if (params.notificationSettings) {
try {
updatedRule.notification_settings = JSON.parse(params.notificationSettings)
} catch {
return {
success: false,
output: {},
error: 'Invalid JSON for notificationSettings parameter',
}
}
}
if (params.record) {
try {
updatedRule.record = JSON.parse(params.record)
} catch {
return {
success: false,
output: {},
error: 'Invalid JSON for record parameter',
}
}
}
if (params.data) {
try {
updatedRule.data = JSON.parse(params.data)
} catch {
// Keep existing data if parse fails
return {
success: false,
output: {},
error: 'Invalid JSON for data parameter',
}
}
}
@@ -164,7 +234,11 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
...JSON.parse(params.annotations),
}
} catch {
// Keep existing annotations if parse fails
return {
success: false,
output: {},
error: 'Invalid JSON for annotations parameter',
}
}
}
@@ -175,7 +249,11 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
...JSON.parse(params.labels),
}
} catch {
// Keep existing labels if parse fails
return {
success: false,
output: {},
error: 'Invalid JSON for labels parameter',
}
}
}
@@ -187,6 +265,9 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
if (params.organizationId) {
headers['X-Grafana-Org-Id'] = params.organizationId
}
if (params.disableProvenance) {
headers['X-Disable-Provenance'] = 'true'
}
const updateResponse = await fetch(
`${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`,
@@ -207,47 +288,8 @@ export const updateAlertRuleTool: ToolConfig<GrafanaUpdateAlertRuleParams, ToolR
}
const data = await updateResponse.json()
return {
success: true,
output: {
uid: data.uid,
title: data.title,
condition: data.condition,
data: data.data,
updated: data.updated,
noDataState: data.noDataState,
execErrState: data.execErrState,
for: data.for,
annotations: data.annotations || {},
labels: data.labels || {},
isPaused: data.isPaused || false,
folderUID: data.folderUID,
ruleGroup: data.ruleGroup,
orgId: data.orgId,
namespace_uid: data.namespace_uid,
namespace_id: data.namespace_id,
provenance: data.provenance || '',
},
}
return { success: true, output: mapAlertRule(data) }
},
outputs: {
uid: {
type: 'string',
description: 'The UID of the updated alert rule',
},
title: {
type: 'string',
description: 'Alert rule title',
},
folderUID: {
type: 'string',
description: 'Parent folder UID',
},
ruleGroup: {
type: 'string',
description: 'Rule group name',
},
},
outputs: ALERT_RULE_OUTPUT_FIELDS,
}
+7 -13
View File
@@ -40,9 +40,9 @@ export const updateAnnotationTool: ToolConfig<
},
text: {
type: 'string',
required: true,
required: false,
visibility: 'user-or-llm',
description: 'New text content for the annotation',
description: 'New text content for the annotation (PATCH supports partial updates)',
},
tags: {
type: 'string',
@@ -78,9 +78,11 @@ export const updateAnnotationTool: ToolConfig<
return headers
},
body: (params) => {
const body: Record<string, any> = {
text: params.text,
}
const body: Record<string, unknown> = {}
if (params.text !== undefined) body.text = params.text
if (params.time) body.time = params.time
if (params.timeEnd) body.timeEnd = params.timeEnd
if (params.tags) {
body.tags = params.tags
@@ -89,14 +91,6 @@ export const updateAnnotationTool: ToolConfig<
.filter((t) => t)
}
if (params.time) {
body.time = params.time
}
if (params.timeEnd) {
body.timeEnd = params.timeEnd
}
return body
},
},
+3 -2
View File
@@ -74,7 +74,8 @@ export const updateDashboardTool: ToolConfig<GrafanaUpdateDashboardParams, ToolR
type: 'boolean',
required: false,
visibility: 'user-only',
description: 'Overwrite even if there is a version conflict',
description:
'Overwrite even if there is a version conflict (defaults to false to surface 412 conflicts)',
},
message: {
type: 'string',
@@ -159,7 +160,7 @@ export const updateDashboardTool: ToolConfig<GrafanaUpdateDashboardParams, ToolR
// Build the request body
const body: Record<string, any> = {
dashboard: updatedDashboard,
overwrite: params.overwrite !== false,
overwrite: params.overwrite === true,
}
// Use existing folder if not specified
+31
View File
@@ -0,0 +1,31 @@
/**
* Map a raw Grafana ProvisionedAlertRule JSON object to the canonical output shape
* shared across list/get/create/update alert rule tools.
*/
export function mapAlertRule(rule: Record<string, unknown>) {
return {
id: (rule.id as number) ?? null,
uid: (rule.uid as string) ?? null,
title: (rule.title as string) ?? null,
condition: (rule.condition as string) ?? null,
data: (rule.data as unknown[]) ?? [],
updated: (rule.updated as string) ?? null,
noDataState: (rule.noDataState as string) ?? null,
execErrState: (rule.execErrState as string) ?? null,
for: (rule.for as string) ?? null,
keepFiringFor: (rule.keep_firing_for as string) ?? (rule.keepFiringFor as string) ?? null,
missingSeriesEvalsToResolve:
(rule.missingSeriesEvalsToResolve as number) ??
(rule.missing_series_evals_to_resolve as number) ??
null,
annotations: (rule.annotations as Record<string, string>) ?? {},
labels: (rule.labels as Record<string, string>) ?? {},
isPaused: (rule.isPaused as boolean) ?? false,
folderUID: (rule.folderUID as string) ?? null,
ruleGroup: (rule.ruleGroup as string) ?? null,
orgID: (rule.orgID as number) ?? (rule.orgId as number) ?? null,
provenance: (rule.provenance as string) ?? '',
notification_settings: (rule.notification_settings as Record<string, unknown>) ?? null,
record: (rule.record as Record<string, unknown>) ?? null,
}
}