Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
102 changes: 99 additions & 3 deletions cmd/mcp-grafana/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func maybeAddTools(s *server.MCPServer, tf func(*server.MCPServer), enabledTools
// 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 @@ -57,6 +58,7 @@ type grafanaConfig struct {

func (dt *disabledTools) addFlags() {
flag.StringVar(&dt.enabledTools, "enabled-tools", "search,datasource,incident,prometheus,loki,alerting,dashboard,folder,oncall,asserts,sift,admin,pyroscope,navigation", "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", 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")
Expand Down Expand Up @@ -102,8 +104,88 @@ func (dt *disabledTools) addTools(s *server.MCPServer) {
maybeAddTools(s, tools.AddNavigationTools, enabledTools, dt.navigation, "navigation")
}

// 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
addFunc func(*server.MCPServer)
}{
{"search", "Tools for searching dashboards, folders, and other Grafana resources", tools.AddSearchTools},
{"datasource", "Tools for listing and fetching datasource details", tools.AddDatasourceTools},
{"incident", "Tools for managing Grafana Incident (create, update, search incidents)", tools.AddIncidentTools},
{"prometheus", "Tools for querying Prometheus metrics and metadata", tools.AddPrometheusTools},
{"loki", "Tools for querying Loki logs and labels", tools.AddLokiTools},
{"alerting", "Tools for managing alert rules and notification contact points", tools.AddAlertingTools},
{"dashboard", "Tools for managing Grafana dashboards (get, update, extract queries)", tools.AddDashboardTools},
{"folder", "Tools for managing Grafana folders", tools.AddFolderTools},
{"oncall", "Tools for managing OnCall schedules, shifts, teams, and users", tools.AddOnCallTools},
{"asserts", "Tools for Grafana Asserts cloud functionality", tools.AddAssertsTools},
{"sift", "Tools for Sift investigations (analyze logs/traces, find errors, detect slow requests)", tools.AddSiftTools},
{"admin", "Tools for administrative tasks (list teams, manage users)", tools.AddAdminTools},
{"pyroscope", "Tools for profiling applications with Pyroscope", tools.AddPyroscopeTools},
{"navigation", "Tools for generating deeplink URLs to Grafana resources", 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,
AddFunc: toolset.addFunc,
})
}
}

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

return dtm
}

func newServer(dt disabledTools) *server.MCPServer {
s := server.NewMCPServer("mcp-grafana", mcpgrafana.Version(), server.WithInstructions(`
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
`
} else {
instructions = `
This server provides access to your Grafana instance and the surrounding ecosystem.

Available Capabilities:
Expand All @@ -117,8 +199,22 @@ func newServer(dt disabledTools) *server.MCPServer {
- Admin: List teams and perform administrative tasks.
- Pyroscope: Profile applications and fetch profiling data.
- Navigation: Generate deeplink URLs for Grafana resources like dashboards, panels, and Explore queries.
`))
dt.addTools(s)
`
}

// Create server with tool capabilities enabled for dynamic tool discovery
s := server.NewMCPServer("mcp-grafana", mcpgrafana.Version(),
server.WithInstructions(instructions),
server.WithToolCapabilities(true))

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
}

Expand Down
160 changes: 160 additions & 0 deletions dynamic_tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package mcpgrafana

import (
"context"
"fmt"
"log/slog"
"sync"

"github.com/mark3labs/mcp-go/server"
)

// Toolset represents a category of related tools that can be dynamically enabled or disabled
type Toolset struct {
Name string
Description string
Tools []Tool
AddFunc func(*server.MCPServer)
}

// DynamicToolManager manages dynamic tool registration and discovery
type DynamicToolManager struct {
server *server.MCPServer
toolsets map[string]*Toolset
enabled map[string]bool
mu sync.RWMutex
}

// NewDynamicToolManager creates a new dynamic tool manager
func NewDynamicToolManager(srv *server.MCPServer) *DynamicToolManager {
return &DynamicToolManager{
server: srv,
toolsets: make(map[string]*Toolset),
enabled: make(map[string]bool),
}
}

// RegisterToolset registers a toolset for dynamic discovery
func (dtm *DynamicToolManager) RegisterToolset(toolset *Toolset) {
dtm.mu.Lock()
defer dtm.mu.Unlock()
dtm.toolsets[toolset.Name] = toolset
slog.Debug("Registered toolset", "name", toolset.Name, "description", toolset.Description)
}

// EnableToolset enables a specific toolset by name
func (dtm *DynamicToolManager) EnableToolset(ctx context.Context, name string) error {
dtm.mu.Lock()
defer dtm.mu.Unlock()

toolset, exists := dtm.toolsets[name]
if !exists {
return fmt.Errorf("toolset not found: %s", name)
}

if dtm.enabled[name] {
slog.Debug("Toolset already enabled", "name", name)
return nil
}

// Add tools using the toolset's AddFunc
// Note: The mcp-go library automatically sends a tools/list_changed notification
// when AddTool is called (via the Register method), so we don't need to manually
// send notifications here. This happens because WithToolCapabilities(true) was set
// during server initialization.
if toolset.AddFunc != nil {
toolset.AddFunc(dtm.server)
}

dtm.enabled[name] = true
slog.Info("Enabled toolset", "name", name)
return nil
}

// DisableToolset disables a specific toolset
// Note: mcp-go doesn't support removing tools at runtime, so this just marks it as disabled
func (dtm *DynamicToolManager) DisableToolset(name string) error {
dtm.mu.Lock()
defer dtm.mu.Unlock()

if _, exists := dtm.toolsets[name]; !exists {
return fmt.Errorf("toolset not found: %s", name)
}

dtm.enabled[name] = false
slog.Info("Disabled toolset", "name", name)
return nil
}

// ListToolsets returns information about all available toolsets
func (dtm *DynamicToolManager) ListToolsets() []ToolsetInfo {
dtm.mu.RLock()
defer dtm.mu.RUnlock()

toolsets := make([]ToolsetInfo, 0, len(dtm.toolsets))
for name, toolset := range dtm.toolsets {
toolsets = append(toolsets, ToolsetInfo{
Name: name,
Description: toolset.Description,
Enabled: dtm.enabled[name],
})
}
return toolsets
}

// ToolsetInfo provides information about a toolset
type ToolsetInfo struct {
Name string `json:"name" jsonschema:"required,description=The name of the toolset"`
Description string `json:"description" jsonschema:"description=Description of what the toolset provides"`
Enabled bool `json:"enabled" jsonschema:"description=Whether the toolset is currently enabled"`
}
Comment on lines +105 to +110
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be worth including ToolNames []string here, as a more concrete hint of what tools this contains?


// AddDynamicDiscoveryTools adds the list and enable toolset tools to the server
func AddDynamicDiscoveryTools(dtm *DynamicToolManager, srv *server.MCPServer) {
type ListToolsetsRequest struct{}

listToolsetsHandler := func(ctx context.Context, request ListToolsetsRequest) ([]ToolsetInfo, error) {
return dtm.ListToolsets(), nil
}

listToolsetsTool := MustTool(
"grafana_list_toolsets",
"List all available Grafana toolsets that can be enabled dynamically. Each toolset provides a category of related functionality.",
listToolsetsHandler,
)
listToolsetsTool.Register(srv)

// Tool to enable a specific toolset
type EnableToolsetRequest struct {
Toolset string `json:"toolset" jsonschema:"required,description=The name of the toolset to enable (e.g. 'prometheus' 'loki' 'dashboard' 'incident')"`
}

enableToolsetHandler := func(ctx context.Context, request EnableToolsetRequest) (string, error) {
if err := dtm.EnableToolset(ctx, request.Toolset); err != nil {
return "", err
}

// Get toolset info to provide better guidance
toolsetInfo := dtm.getToolsetInfo(request.Toolset)
if toolsetInfo == nil {
return fmt.Sprintf("Successfully enabled toolset: %s. The tools are now available for use.", request.Toolset), nil
}

return fmt.Sprintf("Successfully enabled toolset: %s\n\nDescription: %s\n\nNote: All tools are already registered and available. You can now use the tools from this toolset directly.",
request.Toolset, toolsetInfo.Description), nil
}

enableToolsetTool := MustTool(
"grafana_enable_toolset",
"Enable a specific Grafana toolset to make its tools available. Use grafana_list_toolsets to see available toolsets.",
enableToolsetHandler,
)
enableToolsetTool.Register(srv)
}

// getToolsetInfo returns information about a specific toolset
func (dtm *DynamicToolManager) getToolsetInfo(name string) *Toolset {
dtm.mu.RLock()
defer dtm.mu.RUnlock()
return dtm.toolsets[name]
}
Loading