Skip to content

feat(mcp): dynamic tool discovery via notifications/tools/list_changed#3812

Merged
teknium1 merged 1 commit intomainfrom
hermes/hermes-3d9ccfec
Mar 29, 2026
Merged

feat(mcp): dynamic tool discovery via notifications/tools/list_changed#3812
teknium1 merged 1 commit intomainfrom
hermes/hermes-3d9ccfec

Conversation

@teknium1
Copy link
Copy Markdown
Contributor

Summary

Implement MCP spec's notifications/tools/list_changed notification handler. When a connected MCP server sends this notification (e.g., GitHub MCP with GITHUB_DYNAMIC_TOOLSETS=1), Hermes automatically re-fetches the tool list, deregisters removed tools, and registers new ones — without requiring a gateway restart or /mcp refresh.

Changes

tools/registry.py

  • ToolRegistry.deregister(name) — removes a tool and cleans up its toolset check if it was the last tool in that toolset. Used by the nuke-and-repave refresh strategy.

tools/mcp_tool.py

  • Notification type imports (ToolListChangedNotification, ServerNotification, etc.) with graceful degradation for older SDK versions
  • _check_message_handler_support() — inspects ClientSession constructor to verify the SDK version accepts message_handler
  • _register_server_tools() — extracted from _discover_and_register_server() as a shared helper used by both initial discovery and dynamic refresh. Handles filtering, collision guards, utility tools, toolset creation, and hermes-* injection.
  • MCPServerTask._make_message_handler() — builds a notification callback that dispatches on type; ToolListChangedNotification triggers refresh, prompt/resource changes are logged stubs
  • MCPServerTask._refresh_tools() — nuke-and-repave under _refresh_lock: fetch new tools, remove old from registry + hermes-* toolsets, re-register fresh
  • message_handler wired into all 3 ClientSession construction sites (stdio, new HTTP, deprecated HTTP)

tests/tools/test_mcp_dynamic_discovery.py (new, 8 tests)

  • Registration → hermes-* injection
  • Full refresh cycle (old removed, new registered)
  • Message handler dispatch
  • deregister() edge cases

Backward compatibility

  • If the MCP SDK lacks notification types or message_handler support, the feature silently degrades to existing static-discovery behavior
  • No new config keys needed — activates automatically when a server sends the notification
  • mcp_servers config format is unchanged

Salvaged from PR #1794 by @shivvor2.

When a connected MCP server sends a ToolListChangedNotification (per the
MCP spec), Hermes now automatically re-fetches the tool list, deregisters
removed tools, and registers new ones — without requiring a restart.

This enables MCP servers with dynamic toolsets (e.g. GitHub MCP with
GITHUB_DYNAMIC_TOOLSETS=1) to add/remove tools at runtime.

Changes:
- registry.py: add ToolRegistry.deregister() for nuke-and-repave refresh
- mcp_tool.py: extract _register_server_tools() from
  _discover_and_register_server() as a shared helper for both initial
  discovery and dynamic refresh
- mcp_tool.py: add _make_message_handler() and _refresh_tools() on
  MCPServerTask, wired into all 3 ClientSession sites (stdio, new HTTP,
  deprecated HTTP)
- Graceful degradation: silently falls back to static discovery when the
  MCP SDK lacks notification types or message_handler support
- 8 new tests covering registration, refresh, handler dispatch, and
  deregister

Salvaged from PR #1794 by shivvor2.
@teknium1 teknium1 merged commit d5d22fe into main Mar 29, 2026
2 of 3 checks passed
@teknium1 teknium1 mentioned this pull request Mar 29, 2026
14 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant