Skip to content

feet(tools+context): Dynamic tool registration#309

Open
ioanarm wants to merge 15 commits intomainfrom
dynamic-tool-reg
Open

feet(tools+context): Dynamic tool registration#309
ioanarm wants to merge 15 commits intomainfrom
dynamic-tool-reg

Conversation

@ioanarm
Copy link
Contributor

@ioanarm ioanarm commented Oct 3, 2025

Dynamic Toolset Discovery and Loading

Summary

Adds dynamic toolset functionality that allows clients to discover and selectively enable tool categories at runtime, significantly reducing context window usage by only loading tools when needed.

Motivation

MCP tool descriptions can consume substantial context window space. By enabling dynamic discovery, clients only load the tools they actually need, preserving context for more important data.

Implementation

New flag: --dynamic-toolsets enables dynamic toolset mode
Discovery tools: Added grafana_list_toolsets and grafana_enable_toolset meta-tools
Notification support: Sends tools/list_changed notification when toolsets are enabled, triggering client refresh

Integration with --enabled-tools:

  • No flag --> all toolsets discoverable
  • --enabled-tools="" --> no toolsets discoverable
  • --enabled-tools="datasources,dashboards" --> only specified toolsets discoverable

Limitations and client compatibility

Protocol Support:
stdio protocol - Fully supported
SSE (Server-Sent Events) - Fully supported
Streamable HTTP - Fully supported
After pr https://github.com/grafana/mcp-grafana/pull/314/files from @sd2k all protocols are supported.

Client Support
✅ Supported: Cursor, VSCode (support notifications),
❌ Not Supported: Claude Desktop, Claude Code

Known Behavior

Slight delay expected: 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 opt-in via the --dynamic-toolsets flag. Existing static tool registration remains the default behavior.

How it works with Cursor.

Currently Sonnet 4.5 model selected - 1m tokens available for this conversation

Normal behavior all tools available:

  • At the beginning 13.9% of context used
    image

  • How many tools are available 14.1%
    image

  • List my prometheus datasources 14.8%
    image

Dynamic tool discovery enabled with all tools discoverable.

  • At the beginning 5.5% of context used
    image

  • List available toolsets - 5.9%
    image

  • Enable Datasource toolset
    image

  • List Datasources 7%
    image

@ioanarm ioanarm requested a review from a team as a code owner October 3, 2025 08:20
Copy link

@gerboland gerboland left a comment

Choose a reason for hiding this comment

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

nit: please update the README to reflect the new CLI flag & behaviour.

@sd2k sd2k self-requested a review October 6, 2025 10:57
@ioanarm
Copy link
Contributor Author

ioanarm commented Oct 6, 2025

Added a comment about supported protocols.
Thanks @gerboland for trying it.

@gerboland
Copy link

gerboland commented Oct 6, 2025

My Cursor is still struggling with this, while it can enable the datasource toolset, it fails to recognise the tools as callable.
image

I have verified the notifications/tools/list_changed event is being sent by mcp-grafana on toolset change:
image

So I think Cursor is the problem for me. Should it update the list of tools in its MCP settings UI on the notifications/tools/list_changed event?

Copy link
Collaborator

@sd2k sd2k left a comment

Choose a reason for hiding this comment

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

Love the idea!

Comment on lines 105 to 110
// 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"`
}
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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

README.md Outdated
Comment on lines 285 to 287
- ✅ **stdio protocol** - Fully supported
- ❌ **SSE (Server-Sent Events)** - Not yet supported
- ❌ **Streamable HTTP** - Not yet supported
Copy link
Collaborator

Choose a reason for hiding this comment

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

Huh, is this because notifications aren't as well supported over streamable HTTP?

Copy link
Contributor Author

@ioanarm ioanarm Nov 2, 2025

Choose a reason for hiding this comment

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

This is now fixed. After rebasing on top of your PR. https://github.com/grafana/mcp-grafana/pull/314/files 🥳

@ioanarm
Copy link
Contributor Author

ioanarm commented Nov 2, 2025

So I think Cursor is the problem for me. Should it update the list of tools in its MCP settings UI on the notifications/tools/list_changed event?

Yeah it should update on the UI as well. The moment you ask more tools to be enabled and the tool is called, the new tools should appear on the UI.
Can you try with SSE? This works now.

Copy link
Collaborator

@sd2k sd2k left a comment

Choose a reason for hiding this comment

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

Looks cool! Would love to see it in, think we need to fix conflicts and lints/tests but otherwise LGTM

README.md Outdated
Comment on lines 171 to 172
| `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 |
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: let's keep this table aligned

README.md Outdated

**Integration with `--enabled-tools`:**
- No flag → all toolsets are discoverable
- `--enabled-tools=""` → no toolsets are discoverable
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is this ever likely to be useful? I wonder if we should just error in this case 🤷

README.md Outdated

**How it works:**
1. Start the server with `--dynamic-toolsets` flag
2. Use `grafana_list_toolsets` to discover available toolset categories
Copy link
Collaborator

Choose a reason for hiding this comment

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

This list conflates things that the user does (start the server) with things that the LLM does (use those tools); we should make it clear that the user doesn't have to actually do those things, and it's the LLM doing it instead.

README.md Outdated
Comment on lines 171 to 172
| `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 |
Copy link
Collaborator

Choose a reason for hiding this comment

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

GitHub's dynamic tool discovery has 3 tools (enable_toolset, list_available_toolsets, get_toolset_tools) - did you experiment with this instead? I guess get_toolset_tools returns some more detailed info about the tools vs just the name (which we provide in the toolset list).

Either way we can iterate on that, not a blocker!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I haven't no. I don't remember this being a thing when I raised this PR.
I can have a look if we push this but I would do it separately mainly because I need to put time again into that! 😏

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

1 similar comment
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions
Copy link

github-actions bot commented Feb 6, 2026

MCP Token Analysis

Passed

Metric Value
Baseline 13967 tokens
Current 13967 tokens
Change +0 (+0.0%)

@sd2k sd2k changed the title feet(tools+context): Dynamic tool reg feet(tools+context): Dynamic tool registration Feb 9, 2026
Copy link
Collaborator

@sd2k sd2k left a comment

Choose a reason for hiding this comment

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

Very much like this!

Comment on lines +133 to +156
// 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"}, func(mcp *server.MCPServer) { tools.AddIncidentTools(mcp, enableWriteTools) }},
{"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"}, func(mcp *server.MCPServer) { tools.AddAlertingTools(mcp, enableWriteTools) }},
{"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"}, func(mcp *server.MCPServer) { tools.AddDashboardTools(mcp, enableWriteTools) }},
{"folder", "Tools for managing Grafana folders", []string{"create_folder"}, func(mcp *server.MCPServer) { tools.AddFolderTools(mcp, enableWriteTools) }},
{"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"}, func(mcp *server.MCPServer) { tools.AddSiftTools(mcp, enableWriteTools) }},
{"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},
{"annotations", "Tools for managing annotations", []string{"get_annotations", "create_annotation", "create_graphite_annotation", "update_annotation", "patch_annotation", "get_annotation_tags"}, func(mcp *server.MCPServer) { tools.AddAnnotationTools(mcp, enableWriteTools) }},
{"rendering", "Tools for rendering dashboard panels as images", []string{"get_panel_image"}, tools.AddRenderingTools},
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is gonna get out of date very quickly 😅 can we think of a better way of constructing this slice automatically?

Comment on lines +227 to +241
- 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
Copy link
Collaborator

Choose a reason for hiding this comment

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

likewise I think this will get out of date quickly.

Comment on lines +374 to +377
- ✅ **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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any chance these have been updated since we last checked?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants