hermes - ✅(Solved) Fix [Bug]: hermes mcp configure cannot persist an intentionally empty tool include list [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#12865Fetched 2026-04-20 12:16:35
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Participants

hermes mcp configure lets the operator deselect every tool for a server and saves tools.include: [], but the MCP runtime interprets an empty include set as "no include filter" and registers all tools again on the next load.

Root Cause

hermes mcp configure lets the operator deselect every tool for a server and saves tools.include: [], but the MCP runtime interprets an empty include set as "no include filter" and registers all tools again on the next load.

Fix Action

Fixed

PR fix notes

PR #13096: fix(mcp): treat explicit empty include list as "block all" (#12865)

Description (problem / solution / changelog)

Problem

hermes mcp configure lets the operator deselect every tool for an MCP server and saves tools.include: [] under that server's config. The runtime then silently re-registered every tool on the next load because the filter logic used truthiness to decide whether an include filter was present:

  • _normalize_name_filter collapsed both "key absent / null" and "explicit empty list" into an empty set().
  • _should_register checked if include_set: — empty set is falsy, so the filter was skipped.
  • The UI pre-selection in hermes_cli/mcp_config.py had the same if include and isinstance(include, list) bug, so next time the operator opened the picker every tool was pre-selected despite the saved include: [].

Fixes #12865.

Fix

  • _normalize_name_filter now returns Optional[frozenset[str]]: None when the key is absent / null / malformed, a possibly-empty frozenset when it's explicitly present.
  • _should_register switches from truthiness to is not None, so an empty include filter correctly matches zero tools.
  • The UI pre-selection logic in mcp_config.py switches to isinstance(include, list) only, matching runtime semantics.

Test plan

  • Added test_empty_include_list_blocks_every_tool regression in tests/tools/test_mcp_tool.py.
  • pytest tests/tools/test_mcp_tool.py tests/hermes_cli/test_mcp_config.py tests/hermes_cli/test_mcp_tools_config.py — 213 passed.
  • Reran the minimal Python reproducer from the issue on the fixed branch: include=[] now yields _should_register('a') is False, whereas include=None still returns True.

Changed files

  • hermes_cli/mcp_config.py (modified, +4/-2)
  • tests/tools/test_mcp_tool.py (modified, +16/-0)
  • tools/mcp_tool.py (modified, +20/-8)

Code Example

from tools.mcp_tool import _normalize_name_filter

include_set = _normalize_name_filter([], "mcp_servers.demo.tools.include")
exclude_set = _normalize_name_filter(None, "mcp_servers.demo.tools.exclude")

def should_register(name):
    if include_set:
        return name in include_set
    if exclude_set:
        return name not in exclude_set
    return True

print(include_set)               # actual: set()
print(should_register("a"))     # actual: True
print(should_register("b"))     # actual: True
RAW_BUFFERClick to expand / collapse

Summary

hermes mcp configure lets the operator deselect every tool for a server and saves tools.include: [], but the MCP runtime interprets an empty include set as "no include filter" and registers all tools again on the next load.

Affected code

  • hermes_cli/mcp_config.py:662-669
  • tools/mcp_tool.py:1827-1835
  • coverage gap: tests/hermes_cli/test_mcp_config.py

Why this is a bug

The interactive config UI reports 0/N tools enabled, but runtime registration uses:

  • include_set = _normalize_name_filter(...)
  • if include_set: return tool_name in include_set
  • otherwise fall back to registering everything

When include is the empty list, _normalize_name_filter([]) returns set(), which is falsey, so _should_register() returns True for every tool.

Minimal reproduction

from tools.mcp_tool import _normalize_name_filter

include_set = _normalize_name_filter([], "mcp_servers.demo.tools.include")
exclude_set = _normalize_name_filter(None, "mcp_servers.demo.tools.exclude")

def should_register(name):
    if include_set:
        return name in include_set
    if exclude_set:
        return name not in exclude_set
    return True

print(include_set)               # actual: set()
print(should_register("a"))     # actual: True
print(should_register("b"))     # actual: True

Expected behavior

  • Selecting zero tools should disable all tools for that server, or the UI should reject the state explicitly.

Actual behavior

  • The saved config says zero tools are enabled, but runtime re-enables everything.

Suggested investigation

  • Distinguish between include absent and include: [] present.
  • Add a regression test that runs the configure path with an empty selection and verifies no MCP tools register afterward.

extent analysis

TL;DR

The issue can be fixed by modifying the _should_register() function to correctly handle the case when include_set is an empty set.

Guidance

  • Modify the _should_register() function to check if include_set is an empty set and return False in that case, instead of relying on the truthiness of the set.
  • Add a check in the interactive config UI to prevent the user from saving a configuration with an empty include list.
  • Update the _normalize_name_filter() function to return a specific value (e.g., None) when the input list is empty, to distinguish between the absence of an include filter and an empty include filter.
  • Create a regression test to verify that selecting zero tools disables all tools for the server.

Example

def _should_register(name):
    if include_set is not None and len(include_set) == 0:
        return False
    if include_set:
        return name in include_set
    if exclude_set:
        return name not in exclude_set
    return True

Notes

The current implementation of _should_register() relies on the truthiness of the include_set, which can lead to unexpected behavior when the set is empty. By explicitly checking for an empty set, we can ensure that the function behaves correctly in this case.

Recommendation

Apply workaround: Modify the _should_register() function to correctly handle the case when include_set is an empty set, as shown in the example above. This will ensure that selecting zero tools disables all tools for the server, as expected.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

  • Selecting zero tools should disable all tools for that server, or the UI should reject the state explicitly.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING