Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ Scopes define the specific resources that permissions apply to. Each action requ

| Tool | Category | Description | Required RBAC Permissions | Required Scopes |
| --------------------------------- | ----------- | ------------------------------------------------------------------- | --------------------------------------- | --------------------------------------------------- |
| `grafana_list_toolsets` | Meta | List available toolsets for [dynamic discovery](#dynamic-toolset-discovery) | None (meta-tool) | N/A |
| `grafana_enable_toolset` | Meta | Enable a specific toolset [dynamically](#dynamic-toolset-discovery) | None (meta-tool) | N/A |
| `list_teams` | Admin | List all teams | `teams:read` | `teams:*` or `teams:id:1` |
| `list_users_by_org` | Admin | List all users in an organization | `users:read` | `global.users:*` or `global.users:id:123` |
| `search_dashboards` | Search | Search for dashboards | `dashboards:read` | `dashboards:*` or `dashboards:uid:abc123` |
Expand Down Expand Up @@ -235,6 +237,7 @@ The `mcp-grafana` binary supports various command-line flags for configuration:

**Tool Configuration:**
- `--enabled-tools`: Comma-separated list of enabled categories - default: all categories enabled - example: "loki,datasources"
- `--dynamic-toolsets`: Enable dynamic toolset discovery mode (tools loaded on-demand, reduces context window usage)
- `--disable-search`: Disable search tools
- `--disable-datasource`: Disable datasource tools
- `--disable-incident`: Disable incident tools
Expand Down Expand Up @@ -298,6 +301,60 @@ All read operations remain available, allowing you to query dashboards, run Prom
- `--server.tls-cert-file`: Path to TLS certificate file for server HTTPS
- `--server.tls-key-file`: Path to TLS private key file for server HTTPS

### Dynamic Toolset Discovery

For even more efficient context window usage, you can enable **dynamic toolset discovery** mode with the `--dynamic-toolsets` flag. In this mode, tools are not loaded at startup. Instead, clients can discover available toolsets and selectively enable only the ones they need at runtime.

**Benefits:**
- Significantly reduces initial context window usage by not loading tool descriptions upfront
- Tools are loaded on-demand only when needed
- Preserves context space for more important data

**How it works:**
1. Start the server with `--dynamic-toolsets` flag
2. Use `grafana_list_toolsets` to discover available toolset categories
3. Use `grafana_enable_toolset` to load specific toolsets (e.g., "datasource", "dashboard")
4. The client receives a `tools/list_changed` notification and refreshes its tool list

**Integration with `--enabled-tools`:**
- No flag → all toolsets are discoverable
- `--enabled-tools=""` → no toolsets are discoverable
- `--enabled-tools="datasource,dashboard"` → only specified toolsets are discoverable

**Example configuration for Cursor/VS Code:**
```json
{
"mcpServers": {
"grafana": {
"command": "mcp-grafana",
"args": ["--dynamic-toolsets"],
"env": {
"GRAFANA_URL": "http://localhost:3000",
"GRAFANA_SERVICE_ACCOUNT_TOKEN": "<your service account token>"
}
}
}
}
```

**Limitations and Client Compatibility:**

Protocol Support:
- ✅ **stdio protocol** - Fully supported
- ✅ **SSE (Server-Sent Events)** - Fully supported
- ✅ **Streamable HTTP** - Fully supported

Client Support:
- ✅ **Cursor** - Fully supported (supports notifications via stdio, SSE, and streamable-http)
- ✅ **VS Code** - Fully supported (supports notifications via stdio, SSE, and streamable-http)
- ❌ **Claude Desktop** - Not yet supported (no notification support, but open issues exist)
- ❌ **Claude Code** - Not yet supported (no notification support, but open issues exist)

**Known Behavior:**
There may be a few seconds of delay between calling `grafana_enable_toolset` and the tools becoming available in the client, as the client needs to receive and process the `tools/list_changed` notification.

**Note:** This is an opt-in feature via the `--dynamic-toolsets` flag. Existing static tool registration remains the default behavior for maximum compatibility.

## Usage

This MCP server works with both local Grafana instances and Grafana Cloud. For Grafana Cloud, use your instance URL (e.g., `https://myinstance.grafana.net`) instead of `http://localhost:3000` in the configuration examples below.
Expand Down
119 changes: 115 additions & 4 deletions cmd/mcp-grafana/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
// disabledTools indicates whether each category of tools should be disabled.
type disabledTools struct {
enabledTools string
dynamicTools bool

search, datasource, incident,
prometheus, loki, alerting,
Expand All @@ -58,6 +59,7 @@

func (dt *disabledTools) addFlags() {
flag.StringVar(&dt.enabledTools, "enabled-tools", "search,datasource,incident,prometheus,loki,alerting,dashboard,folder,oncall,asserts,sift,admin,pyroscope,navigation,proxied,annotations", "A comma separated list of tools enabled for this server. Can be overwritten entirely or by disabling specific components, e.g. --disable-search.")
flag.BoolVar(&dt.dynamicTools, "dynamic-toolsets", getEnvBool("GRAFANA_DYNAMIC_TOOLSETS", false), "Enable dynamic tool discovery. When enabled, only discovery tools are registered initially, and other toolsets can be enabled on-demand.")
flag.BoolVar(&dt.search, "disable-search", false, "Disable search tools")
flag.BoolVar(&dt.datasource, "disable-datasource", false, "Disable datasource tools")
flag.BoolVar(&dt.incident, "disable-incident", false, "Disable incident tools")
Expand Down Expand Up @@ -107,6 +109,61 @@
maybeAddTools(s, func(mcp *server.MCPServer) { tools.AddAnnotationTools(mcp, enableWriteTools) }, enabledTools, dt.annotations, "annotations")
}

// addToolsDynamically sets up dynamic tool discovery
func (dt *disabledTools) addToolsDynamically(s *server.MCPServer) *mcpgrafana.DynamicToolManager {
dtm := mcpgrafana.NewDynamicToolManager(s)

enabledTools := strings.Split(dt.enabledTools, ",")

isEnabled := func(toolName string) bool {
// If enabledTools is empty string, no tools should be available
if dt.enabledTools == "" {
return false
}
return slices.Contains(enabledTools, toolName)
}

// Define all available toolsets
allToolsets := []struct {
name string
description string
toolNames []string
addFunc func(*server.MCPServer)
}{
{"search", "Tools for searching dashboards, folders, and other Grafana resources", []string{"search_dashboards", "search_folders"}, tools.AddSearchTools},
{"datasource", "Tools for listing and fetching datasource details", []string{"list_datasources", "get_datasource_by_uid", "get_datasource_by_name"}, tools.AddDatasourceTools},
{"incident", "Tools for managing Grafana Incident (create, update, search incidents)", []string{"list_incidents", "create_incident", "add_activity_to_incident", "get_incident"}, tools.AddIncidentTools},

Check failure on line 135 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Lint Go

cannot use tools.AddIncidentTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 135 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Unit

cannot use tools.AddIncidentTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 135 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Integration

cannot use tools.AddIncidentTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal
{"prometheus", "Tools for querying Prometheus metrics and metadata", []string{"list_prometheus_metric_metadata", "query_prometheus", "list_prometheus_metric_names", "list_prometheus_label_names", "list_prometheus_label_values"}, tools.AddPrometheusTools},
{"loki", "Tools for querying Loki logs and labels", []string{"list_loki_label_names", "list_loki_label_values", "query_loki_stats", "query_loki_logs"}, tools.AddLokiTools},
{"alerting", "Tools for managing alert rules and notification contact points", []string{"list_alert_rules", "get_alert_rule_by_uid", "list_contact_points", "create_alert_rule", "update_alert_rule", "delete_alert_rule"}, tools.AddAlertingTools},

Check failure on line 138 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Lint Go

cannot use tools.AddAlertingTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 138 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Unit

cannot use tools.AddAlertingTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 138 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Integration

cannot use tools.AddAlertingTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal
{"dashboard", "Tools for managing Grafana dashboards (get, update, extract queries)", []string{"get_dashboard_by_uid", "update_dashboard", "get_dashboard_panel_queries", "get_dashboard_property", "get_dashboard_summary"}, tools.AddDashboardTools},

Check failure on line 139 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Lint Go

cannot use tools.AddDashboardTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 139 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Unit

cannot use tools.AddDashboardTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 139 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Integration

cannot use tools.AddDashboardTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal
{"folder", "Tools for managing Grafana folders", []string{"create_folder"}, tools.AddFolderTools},

Check failure on line 140 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Lint Go

cannot use tools.AddFolderTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 140 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Unit

cannot use tools.AddFolderTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 140 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Integration

cannot use tools.AddFolderTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal
{"oncall", "Tools for managing OnCall schedules, shifts, teams, and users", []string{"list_oncall_schedules", "get_oncall_shift", "get_current_oncall_users", "list_oncall_teams", "list_oncall_users", "list_alert_groups", "get_alert_group"}, tools.AddOnCallTools},
{"asserts", "Tools for Grafana Asserts cloud functionality", []string{"get_assertions"}, tools.AddAssertsTools},
{"sift", "Tools for Sift investigations (analyze logs/traces, find errors, detect slow requests)", []string{"get_sift_investigation", "get_sift_analysis", "list_sift_investigations", "find_error_pattern_logs", "find_slow_requests"}, tools.AddSiftTools},

Check failure on line 143 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Lint Go

cannot use tools.AddSiftTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal (typecheck)

Check failure on line 143 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Unit

cannot use tools.AddSiftTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal

Check failure on line 143 in cmd/mcp-grafana/main.go

View workflow job for this annotation

GitHub Actions / Test Integration

cannot use tools.AddSiftTools (value of type func(mcp *server.MCPServer, enableWriteTools bool)) as func(*server.MCPServer) value in struct literal
{"admin", "Tools for administrative tasks (list teams, manage users)", []string{"list_teams", "list_users_by_org"}, tools.AddAdminTools},
{"pyroscope", "Tools for profiling applications with Pyroscope", []string{"list_pyroscope_label_names", "list_pyroscope_label_values", "list_pyroscope_profile_types", "fetch_pyroscope_profile"}, tools.AddPyroscopeTools},
{"navigation", "Tools for generating deeplink URLs to Grafana resources", []string{"generate_deeplink"}, tools.AddNavigationTools},
}

// Only register toolsets that are enabled
for _, toolset := range allToolsets {
if isEnabled(toolset.name) {
dtm.RegisterToolset(&mcpgrafana.Toolset{
Name: toolset.name,
Description: toolset.description,
ToolNames: toolset.toolNames,
AddFunc: toolset.addFunc,
})
}
}

// Add the dynamic discovery tools themselves
mcpgrafana.AddDynamicDiscoveryTools(dtm, s)

return dtm
}

func newServer(transport string, dt disabledTools) (*server.MCPServer, *mcpgrafana.ToolManager) {
sm := mcpgrafana.NewSessionManager()

Expand Down Expand Up @@ -145,8 +202,39 @@
},
}
}
s := server.NewMCPServer("mcp-grafana", mcpgrafana.Version(),
server.WithInstructions(`

// Build instructions based on configuration
var instructions string
if dt.dynamicTools {
instructions = `
This server provides access to your Grafana instance and the surrounding ecosystem with dynamic tool discovery.

Getting Started:
1. Use 'grafana_list_toolsets' to see all available toolsets
2. Use 'grafana_enable_toolset' to enable specific functionality you need
3. Once enabled, the toolset's tools will be available for use

Available Toolset Categories:
- search: Search dashboards, folders, and resources
- datasource: Manage datasources
- prometheus: Query Prometheus metrics
- loki: Query Loki logs
- dashboard: Manage dashboards
- folder: Manage folders
- incident: Manage incidents
- alerting: Manage alerts
- oncall: Manage OnCall schedules
- asserts: Grafana Asserts functionality
- sift: Sift investigations
- admin: Administrative tasks
- pyroscope: Application profiling
- navigation: Generate deeplinks
- proxied: Access tools from external MCP servers (like Tempo) through dynamic discovery

Note that some of these capabilities may be disabled. Do not try to use features that are not available via tools.
`
} else {
instructions = `
This server provides access to your Grafana instance and the surrounding ecosystem.

Available Capabilities:
Expand All @@ -163,14 +251,29 @@
- Proxied Tools: Access tools from external MCP servers (like Tempo) through dynamic discovery.

Note that some of these capabilities may be disabled. Do not try to use features that are not available via tools.
`),
`
}

// Determine if we need tool capabilities enabled (for dynamic toolsets or proxied tools)
toolCapabilitiesEnabled := dt.dynamicTools || (!dt.proxied && transport != "stdio")

s := server.NewMCPServer("mcp-grafana", mcpgrafana.Version(),
server.WithInstructions(instructions),
server.WithHooks(hooks),
server.WithToolCapabilities(toolCapabilitiesEnabled),
)

// Initialize ToolManager now that server is created
stm = mcpgrafana.NewToolManager(sm, s, mcpgrafana.WithProxiedTools(!dt.proxied))

dt.addTools(s)
if dt.dynamicTools {
// For dynamic toolsets, start with only discovery tools
// Tools will be added dynamically when toolsets are enabled
dt.addToolsDynamically(s)
} else {
dt.addTools(s)
}

return s, stm
}

Expand Down Expand Up @@ -368,6 +471,14 @@
}
}

// getEnvBool reads a boolean from an environment variable
func getEnvBool(key string, defaultValue bool) bool {
if value, exists := os.LookupEnv(key); exists {
return value == "1" || strings.ToLower(value) == "true"
}
return defaultValue
}

func parseLevel(level string) slog.Level {
var l slog.Level
if err := l.UnmarshalText([]byte(level)); err != nil {
Expand Down
Loading
Loading