From 3e00a356a4c1c2be3632cf7740111c868aca349b Mon Sep 17 00:00:00 2001 From: Siddharth Balyan <52913345+alt-glitch@users.noreply.github.com> Date: Wed, 23 Sep 2026 03:30:24 +0530 Subject: [PATCH] fix(mcp): one reader for mcp_servers..enabled (#119567) The `enabled` key had four parsers. The MCP client (`_parse_boolish`) read `enabled: 0` as on; the toolset resolver and editor (`_parse_enabled_flag`) read it as off. The server list (`summarize_server`, `/api/mcp/servers`) read any non-`False` value as on, so `enabled: "false"` showed on while the agent skipped it. The catalog and `hermes mcp list` accepted only true/1/yes, so `enabled: on` showed off while the server ran. `tools/mcp_tool_common.py::mcp_server_enabled` is now the only reader, and every surface calls it. `_parse_boolish` treats YAML numbers by truthiness (0 off, other numbers on). Everything else keeps the client's semantics: the off words are off, absent / null / junk stay on, with the existing warning for junk. The desktop MCP page mirrors the rule in `serverEnabled` (`apps/desktop/src/lib/mcp-servers.ts`). One case table (`mcp-enabled-cases.json`) drives the Python invariant test and the vitest test, so the page and the runtime cannot drift apart again. --- agent/coding_context.py | 4 +- .../src/app/capabilities/mcp/mcp-doc.ts | 2 - .../src/app/capabilities/mcp/mcp-status.ts | 3 +- .../app/capabilities/mcp/use-mcp-probes.ts | 3 +- apps/desktop/src/lib/mcp-enabled-cases.json | 22 ++++++++++ apps/desktop/src/lib/mcp-servers.test.ts | 11 ++++- apps/desktop/src/lib/mcp-servers.ts | 11 +++++ hermes_cli/mcp_catalog.py | 9 ++-- hermes_cli/mcp_config.py | 6 +-- hermes_cli/oneshot.py | 4 +- hermes_cli/tools_config.py | 4 +- hermes_cli/web_server_mcp.py | 4 +- tests/tools/test_mcp_enabled_reader.py | 41 +++++++++++++++++++ tools/mcp_tool_common.py | 13 ++++-- tools/mcp_tool_discovery.py | 18 ++++---- tools/mcp_tool_registration.py | 10 ++--- tools/mcp_tool_server_run.py | 4 +- tui_gateway/mcp_rpc_helpers.py | 3 +- tui_gateway/methods_profiles.py | 8 ++-- tui_gateway/server.py | 4 +- 20 files changed, 132 insertions(+), 52 deletions(-) create mode 100644 apps/desktop/src/lib/mcp-enabled-cases.json create mode 100644 tests/tools/test_mcp_enabled_reader.py diff --git a/agent/coding_context.py b/agent/coding_context.py index 178d71eeb9..1cc962c1f6 100644 --- a/agent/coding_context.py +++ b/agent/coding_context.py @@ -281,11 +281,11 @@ def _enabled_mcp_servers(config: Optional[dict[str, Any]]) -> list[str]: """Names of MCP servers the user has enabled — kept in the coding posture.""" try: from hermes_cli.config import read_raw_config - from hermes_cli.tools_config import _parse_enabled_flag + from tools.mcp_tool_common import mcp_server_enabled servers = read_raw_config().get("mcp_servers") or {} return [ str(name) for name, cfg in servers.items() - if isinstance(cfg, dict) and _parse_enabled_flag(cfg.get("enabled", True), default=True) + if isinstance(cfg, dict) and mcp_server_enabled(cfg) ] except Exception: return [] diff --git a/apps/desktop/src/app/capabilities/mcp/mcp-doc.ts b/apps/desktop/src/app/capabilities/mcp/mcp-doc.ts index 625e565511..37c229d8c6 100644 --- a/apps/desktop/src/app/capabilities/mcp/mcp-doc.ts +++ b/apps/desktop/src/app/capabilities/mcp/mcp-doc.ts @@ -27,8 +27,6 @@ export function parseServersDoc(raw: string): McpServers { return Object.fromEntries(Object.entries(map).map(([name, entry]) => [name, normalizeEntry(entry)])) } -export const serverEnabled = (server: McpServerEntry) => server.enabled !== false - export function withEnabled(server: McpServerEntry, enabled: boolean): McpServerEntry { const next = { ...server } diff --git a/apps/desktop/src/app/capabilities/mcp/mcp-status.ts b/apps/desktop/src/app/capabilities/mcp/mcp-status.ts index d109b7e8c1..3aa0cca9e0 100644 --- a/apps/desktop/src/app/capabilities/mcp/mcp-status.ts +++ b/apps/desktop/src/app/capabilities/mcp/mcp-status.ts @@ -4,10 +4,9 @@ import { getUsageAnalytics, type McpTestResult, type ProfileScope } from '@/herm import type { Translations } from '@/i18n' import { estimateServerTokens, serverUsageCount } from '@/lib/mcp-cost' import { NEEDS_AUTH_RE } from '@/lib/mcp-probe-cache' -import type { McpServerEntry } from '@/lib/mcp-servers' +import { type McpServerEntry, serverEnabled } from '@/lib/mcp-servers' import { countEnabledTools } from '@/lib/mcp-tool-filter' -import { serverEnabled } from './mcp-doc' export const MCP_CATALOG_KEY = ['mcp-catalog'] as const diff --git a/apps/desktop/src/app/capabilities/mcp/use-mcp-probes.ts b/apps/desktop/src/app/capabilities/mcp/use-mcp-probes.ts index fc4e4f6536..28cf772efc 100644 --- a/apps/desktop/src/app/capabilities/mcp/use-mcp-probes.ts +++ b/apps/desktop/src/app/capabilities/mcp/use-mcp-probes.ts @@ -2,10 +2,9 @@ import { type RefObject, useEffect, useMemo, useRef, useState } from 'react' import { type ProfileScope, testMcpServer } from '@/hermes' import { PROBE_TTL_MS, probeCache, probeKey, serverFingerprint } from '@/lib/mcp-probe-cache' -import type { McpServerEntry, McpServers } from '@/lib/mcp-servers' +import { type McpServerEntry, type McpServers, serverEnabled } from '@/lib/mcp-servers' import { countEnabledTools } from '@/lib/mcp-tool-filter' -import { serverEnabled } from './mcp-doc' import { loadMcpUsage, okProbe, type Probe, serverCost, type ServerCost } from './mcp-status' export interface McpProbes { diff --git a/apps/desktop/src/lib/mcp-enabled-cases.json b/apps/desktop/src/lib/mcp-enabled-cases.json new file mode 100644 index 0000000000..0bed4cbb7c --- /dev/null +++ b/apps/desktop/src/lib/mcp-enabled-cases.json @@ -0,0 +1,22 @@ +[ + {"enabled": true, "on": true}, + {"enabled": false, "on": false}, + {"enabled": 1, "on": true}, + {"enabled": 0, "on": false}, + {"enabled": 0.0, "on": false}, + {"enabled": 2, "on": true}, + {"enabled": -0.0, "on": false}, + {"enabled": [], "on": true}, + {"enabled": "true", "on": true}, + {"enabled": "Yes", "on": true}, + {"enabled": " on ", "on": true}, + {"enabled": "1", "on": true}, + {"enabled": "false", "on": false}, + {"enabled": "OFF", "on": false}, + {"enabled": "no", "on": false}, + {"enabled": "0", "on": false}, + {"enabled": "", "on": true}, + {"enabled": "maybe", "on": true}, + {"enabled": null, "on": true}, + {"on": true} +] diff --git a/apps/desktop/src/lib/mcp-servers.test.ts b/apps/desktop/src/lib/mcp-servers.test.ts index 8928ebde1c..7f50e0e1d6 100644 --- a/apps/desktop/src/lib/mcp-servers.test.ts +++ b/apps/desktop/src/lib/mcp-servers.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' -import { getServers } from './mcp-servers' +import cases from './mcp-enabled-cases.json' +import { getServers, serverEnabled } from './mcp-servers' describe('getServers', () => { it('returns empty when the map is absent or not a map', () => { @@ -35,3 +36,11 @@ describe('getServers', () => { expect(getServers({ mcp_servers: servers })).toEqual({ working: { command: 'npx' } }) }) }) + +// The backend reads the same table (tests/tools/test_mcp_enabled_reader.py), so +// the MCP page and the runtime cannot disagree about whether a server is on. +describe('serverEnabled', () => { + it.each(cases)('%j', ({ on, ...entry }) => { + expect(serverEnabled({ command: 'x', ...entry })).toBe(on) + }) +}) diff --git a/apps/desktop/src/lib/mcp-servers.ts b/apps/desktop/src/lib/mcp-servers.ts index eed292f5cf..a61b676eb4 100644 --- a/apps/desktop/src/lib/mcp-servers.ts +++ b/apps/desktop/src/lib/mcp-servers.ts @@ -23,6 +23,17 @@ export function normalizeEntry(entry: Record): Record !OFF_WORDS.has(String(entry.enabled).trim().toLowerCase()) + /** A value a reader can reach into: an object, not `null`, an array or a scalar. */ const isEntry = (value: unknown): value is Record => !!value && typeof value === 'object' && !Array.isArray(value) diff --git a/hermes_cli/mcp_catalog.py b/hermes_cli/mcp_catalog.py index 7546ddfe9a..aa8c39cc77 100644 --- a/hermes_cli/mcp_catalog.py +++ b/hermes_cli/mcp_catalog.py @@ -373,11 +373,10 @@ def is_installed(name: str) -> bool: def server_enabled(cfg: dict) -> bool: - """Interpret a server block's ``enabled`` flag (bools, and yes/true/1 strings).""" - enabled = cfg.get("enabled", True) - if isinstance(enabled, str): - return enabled.lower() in {"true", "1", "yes"} - return bool(enabled) + """Whether the server block is on: the same reader the MCP client uses.""" + from tools.mcp_tool_common import mcp_server_enabled + + return mcp_server_enabled(cfg) def is_enabled(name: str) -> bool: diff --git a/hermes_cli/mcp_config.py b/hermes_cli/mcp_config.py index 088a094b5c..6cc2e84788 100644 --- a/hermes_cli/mcp_config.py +++ b/hermes_cli/mcp_config.py @@ -19,7 +19,7 @@ from hermes_cli.colors import Colors, color from hermes_constants import display_hermes_home from hermes_cli.mcp_security import validate_mcp_server_entry from tools.mcp_tool_config import _ENV_VAR_PATTERN -from tools.mcp_tool_common import _env_ref_name +from tools.mcp_tool_common import _env_ref_name, mcp_server_enabled logger = logging.getLogger(__name__) @@ -755,9 +755,7 @@ def cmd_mcp_list(args=None): else: tools_str = "all" - enabled = cfg.get("enabled", True) - if isinstance(enabled, str): - enabled = enabled.lower() in {"true", "1", "yes"} + enabled = mcp_server_enabled(cfg) status = color("✓ enabled", Colors.GREEN) if enabled else color("✗ disabled", Colors.DIM) print(f" {name:<16} {transport:<30} {tools_str:<12} {status}") print() diff --git a/hermes_cli/oneshot.py b/hermes_cli/oneshot.py index bc42dc5b49..96c8227fad 100644 --- a/hermes_cli/oneshot.py +++ b/hermes_cli/oneshot.py @@ -147,7 +147,7 @@ def _configured_mcp_servers() -> tuple[set[str], set[str]]: """``(enabled, disabled)`` MCP server names from config; both empty on any error.""" try: from hermes_cli.config import read_raw_config - from hermes_cli.tools_config import _parse_enabled_flag + from tools.mcp_tool_common import mcp_server_enabled cfg = read_raw_config() mcp_servers = cfg.get("mcp_servers") if isinstance(cfg.get("mcp_servers"), dict) else {} @@ -156,7 +156,7 @@ def _configured_mcp_servers() -> tuple[set[str], set[str]]: for name, server_cfg in mcp_servers.items(): if not isinstance(server_cfg, dict): continue - target = enabled if _parse_enabled_flag(server_cfg.get("enabled", True), default=True) else disabled + target = enabled if mcp_server_enabled(server_cfg) else disabled target.add(str(name)) return enabled, disabled except Exception: diff --git a/hermes_cli/tools_config.py b/hermes_cli/tools_config.py index 1453d4a0da..4722938a8d 100644 --- a/hermes_cli/tools_config.py +++ b/hermes_cli/tools_config.py @@ -415,10 +415,12 @@ def enabled_mcp_server_names(config: dict) -> Set[str]: """MCP servers globally enabled in config.yaml or by a plugin (shared by platform + cron resolvers). Enabled unless ``enabled`` is explicitly falsey; portable-plugin servers (in-memory) count — enabling the plugin is the opt-in.""" + from tools.mcp_tool_common import mcp_server_enabled + mcp_servers = (config or {}).get("mcp_servers") or {} names = { str(name) for name, server_cfg in mcp_servers.items() - if isinstance(server_cfg, dict) and _parse_enabled_flag(server_cfg.get("enabled", True), default=True) + if isinstance(server_cfg, dict) and mcp_server_enabled(server_cfg) } try: from hermes_cli.plugins import get_portable_mcp_server_names_nowait diff --git a/hermes_cli/web_server_mcp.py b/hermes_cli/web_server_mcp.py index a4cbfbab60..24f3cc87b1 100644 --- a/hermes_cli/web_server_mcp.py +++ b/hermes_cli/web_server_mcp.py @@ -82,6 +82,8 @@ def _redact_mcp_env(env: Dict[str, Any]) -> Dict[str, str]: def _mcp_server_summary(name: str, cfg: Dict[str, Any], plugin: str | None = None) -> Dict[str, Any]: + from tools.mcp_tool_common import mcp_server_enabled + transport = "http" if cfg.get("url") else ("stdio" if cfg.get("command") else "unknown") auth = cfg.get("auth") headers = cfg.get("headers") or {} @@ -95,7 +97,7 @@ def _mcp_server_summary(name: str, cfg: Dict[str, Any], plugin: str | None = Non "args": list(cfg.get("args") or []), "env": _redact_mcp_env(cfg.get("env") or {}), "auth": auth, - "enabled": cfg.get("enabled", True) is not False, + "enabled": mcp_server_enabled(cfg), # Tool selection: list of enabled tool names, or None = all. "tools": cfg.get("tools"), "source": "plugin" if plugin is not None else "config", diff --git a/tests/tools/test_mcp_enabled_reader.py b/tests/tools/test_mcp_enabled_reader.py new file mode 100644 index 0000000000..4022862024 --- /dev/null +++ b/tests/tools/test_mcp_enabled_reader.py @@ -0,0 +1,41 @@ +"""Every reader of ``mcp_servers..enabled`` gives one answer per value. + +The MCP client, the toolset resolver, the profile editor, the catalog and the list/status +surfaces used to parse the key four different ways, so ``enabled: 0`` was off for the resolver +and on for the client, and ``enabled: "false"`` was on in the server list. The case table is +shared with the desktop (``apps/desktop/src/lib/mcp-enabled-cases.json``). +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +CASES = json.loads( + (Path(__file__).resolve().parents[2] / "apps/desktop/src/lib/mcp-enabled-cases.json").read_text(encoding="utf-8")) + + +def _readers(): + from hermes_cli.mcp_catalog import server_enabled + from hermes_cli.tools_config import enabled_mcp_server_names + from tools.mcp_tool_common import mcp_server_enabled + from tui_gateway.mcp_rpc_helpers import summarize_server + from tui_gateway.methods_profiles import _mcp_entry_enabled + + return { + "mcp client": mcp_server_enabled, + "toolset resolver": lambda cfg: "s" in enabled_mcp_server_names({"mcp_servers": {"s": cfg}}), + "profile editor": _mcp_entry_enabled, + "catalog": server_enabled, + "server list": lambda cfg: summarize_server("s", cfg)["enabled"], + } + + +@pytest.mark.parametrize("case", CASES, ids=lambda c: repr(c.get("enabled", ""))) +def test_every_reader_agrees_with_the_shared_table(case): + entry = {k: v for k, v in case.items() if k != "on"} + answers = {name: bool(read({"command": "x", **entry})) for name, read in _readers().items()} + + assert answers == dict.fromkeys(answers, case["on"]) diff --git a/tools/mcp_tool_common.py b/tools/mcp_tool_common.py index 6ad04d7b21..8104ee628c 100644 --- a/tools/mcp_tool_common.py +++ b/tools/mcp_tool_common.py @@ -122,11 +122,11 @@ _FALSE_WORDS = frozenset({"false", "0", "no", "off"}) def _parse_boolish(value: Any, default: bool = True) -> bool: - """Parse a bool-like config value with safe fallback.""" + """Parse a bool-like config value with safe fallback (YAML ``0``/``1`` are numbers, not words).""" if value is None: return default - if isinstance(value, bool): - return value + if isinstance(value, (bool, int, float)): + return bool(value) if isinstance(value, str): lowered = value.strip().lower() if lowered in _TRUE_WORDS: @@ -137,6 +137,13 @@ def _parse_boolish(value: Any, default: bool = True) -> bool: return default +def mcp_server_enabled(cfg: dict) -> bool: + """Whether ``mcp_servers.`` is on. The ONE reader of the ``enabled`` key: the MCP client, + the toolset resolver, the profile editor, and every list/status surface call it, so a value + can never be on for one surface and off for another. Absent, ``null`` or unparseable = on.""" + return _parse_boolish(cfg.get("enabled", True), default=True) + + def _get_lifecycle_seconds(config: dict, key: str) -> Optional[float]: """Optional positive lifecycle timeout from top-level/nested ``lifecycle`` config (``0`` disables; negatives and non-numbers are warned about and ignored).""" diff --git a/tools/mcp_tool_discovery.py b/tools/mcp_tool_discovery.py index 472ddaca2c..1dd1eb9b16 100644 --- a/tools/mcp_tool_discovery.py +++ b/tools/mcp_tool_discovery.py @@ -11,7 +11,7 @@ import time from contextlib import contextmanager from pathlib import Path from typing import Dict, List, Optional, Tuple -from tools.mcp_tool_common import _core, _parse_boolish +from tools.mcp_tool_common import _core, _parse_boolish, mcp_server_enabled from tools import mcp_tool_config as _config from tools import mcp_tool_errors as _errors from tools import mcp_tool_lifecycle as _lifecycle @@ -66,10 +66,6 @@ def _connect_cooldown_active(server_name: str) -> bool: return deadline is not None and time.monotonic() < deadline -def _enabled(cfg: dict) -> bool: - return _parse_boolish(cfg.get("enabled", True), default=True) - - def _owner_scope_home() -> Optional[Path]: """The profile home whose secret scope MCP credential reads must resolve under, or None when the caller is already scoped or this is a single-profile process (scope key ``None``). @@ -335,9 +331,9 @@ def _select_new_servers(servers: Dict[str, dict]) -> Dict[str, dict]: k: v for k, v in servers.items() if keys[k] not in _core._servers and keys[k] not in _core._server_connecting and keys[k] not in _core._lazy_server_configs - and _enabled(v) and not _connect_cooldown_active(k)} + and mcp_server_enabled(v) and not _connect_cooldown_active(k)} stale_cached = [_core._servers[keys[k]] for k, v in servers.items() - if keys[k] in _core._servers and _enabled(v) + if keys[k] in _core._servers and mcp_server_enabled(v) and getattr(_core._servers[keys[k]], "session", None) is None] for srv_name in new_servers: _core._server_connecting.add(keys[srv_name]) @@ -587,7 +583,7 @@ def discover_mcp_tools(allowed_mcp_names: Optional[List[str]] = None) -> List[st keys = {name: _resolve_server_key(name) for name in servers} new_server_names = [name for name, cfg in servers.items() if keys[name] not in _core._servers and keys[name] not in _core._server_connecting - and _enabled(cfg)] + and mcp_server_enabled(cfg)] prior_lazy = set(_core._lazy_server_configs) tool_names = register_mcp_servers(servers) if new_server_names: @@ -619,7 +615,7 @@ def reconcile_mcp_servers_with_config() -> Dict[str, List[str]]: ``{"removed": [...], "added": [...], "pending": [...]}``; a no-op when nothing changed.""" with _owner_secret_scope(): servers = _config._load_mcp_config() - wanted = {name for name, cfg in servers.items() if _enabled(cfg)} + wanted = {name for name, cfg in servers.items() if mcp_server_enabled(cfg)} scope = _core._mcp_registry_scope() with _core._lock: owned = [key for key, owner in _core._server_scope_keys.items() if owner == scope] @@ -708,7 +704,7 @@ def get_mcp_status(configured: Optional[Dict[str, dict]] = None, *, include_runt result: List[dict] = [] for name, cfg in configured.items(): - enabled = _enabled(cfg) # evaluated unconditionally: malformed values warn even when connected + enabled = mcp_server_enabled(cfg) # evaluated unconditionally: malformed values warn even when connected server = active_servers.get(name) live = server is not None and server.session is not None # An in-flight or failed first-use connect outranks "lazy": that server is no longer @@ -754,7 +750,7 @@ def probe_mcp_server_tools() -> Dict[str, List[tuple]]: if not _core._ensure_mcp_sdk(): return {} with _owner_secret_scope(): - enabled = {k: v for k, v in (_config._load_mcp_config() or {}).items() if _enabled(v)} + enabled = {k: v for k, v in (_config._load_mcp_config() or {}).items() if mcp_server_enabled(v)} if not enabled: return {} _loop._ensure_mcp_loop() diff --git a/tools/mcp_tool_registration.py b/tools/mcp_tool_registration.py index a01d24c2c5..2170d8120a 100644 --- a/tools/mcp_tool_registration.py +++ b/tools/mcp_tool_registration.py @@ -9,7 +9,7 @@ import threading from dataclasses import dataclass from types import SimpleNamespace from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional -from tools.mcp_tool_common import _parse_boolish, _core, _resolve_tool_timeout, mcp_field +from tools.mcp_tool_common import _parse_boolish, _core, _resolve_tool_timeout, mcp_field, mcp_server_enabled from tools import mcp_tool_handlers as _handlers from tools import mcp_tool_schema as _schema from tools.mcp_tool_handlers import ( @@ -407,10 +407,6 @@ def _register_server_tools(name: str, server: "MCPServerTask", config: dict) -> return registered -def _server_enabled(config: dict) -> bool: - return _parse_boolish(config.get("enabled", True), default=True) - - def _connection_identity(config: dict) -> tuple: """What makes one live connection reusable for another profile: the route fingerprint PLUS everything that authenticates it (``config_fingerprint`` deliberately excludes credentials so @@ -473,7 +469,7 @@ def _register_connected_into_current_scope(servers: dict) -> int: server = _core._servers.get(key) config = servers.get(_key_name(key)) cross_profile = _key_scope(key) != scope - if (config is None or not _server_enabled(config) or server is None + if (config is None or not mcp_server_enabled(config) or server is None or getattr(server, "session", None) is None or not _same_server_route(server, config, cross_profile=cross_profile)): stale.append(key) @@ -482,7 +478,7 @@ def _register_connected_into_current_scope(servers: dict) -> int: registered_servers = 0 for name, config in servers.items(): - if not _server_enabled(config): + if not mcp_server_enabled(config): continue with _core._lock: if _server_key(name, scope, current=False) in _core._servers: diff --git a/tools/mcp_tool_server_run.py b/tools/mcp_tool_server_run.py index 5ef3a8c822..db6d7f83fc 100644 --- a/tools/mcp_tool_server_run.py +++ b/tools/mcp_tool_server_run.py @@ -249,8 +249,8 @@ class MCPServerRunMixin: entry = (_config._load_mcp_config() or {}).get(self.name) if entry is None: return False - from tools.mcp_tool_common import _parse_boolish - return _parse_boolish(entry.get("enabled", True), default=True) + from tools.mcp_tool_common import mcp_server_enabled + return mcp_server_enabled(entry) except Exception: return True diff --git a/tui_gateway/mcp_rpc_helpers.py b/tui_gateway/mcp_rpc_helpers.py index 2102a01ce7..89868b80c0 100644 --- a/tui_gateway/mcp_rpc_helpers.py +++ b/tui_gateway/mcp_rpc_helpers.py @@ -31,6 +31,7 @@ def server_configs_with_sources(config_servers: Mapping[str, dict]) -> tuple[Dic def summarize_server(name: str, cfg: dict, plugin: str | None = None) -> Dict[str, Any]: from hermes_cli.mcp_config import _oauth_tokens_present + from tools.mcp_tool_common import mcp_server_enabled cfg = cfg if isinstance(cfg, dict) else {} transport = "http" if cfg.get("url") else ("stdio" if cfg.get("command") else "unknown") @@ -47,7 +48,7 @@ def summarize_server(name: str, cfg: dict, plugin: str | None = None) -> Dict[st "env": sorted(str(k) for k in (cfg.get("env") or {})), "auth": auth, "oauth_tokens_present": _oauth_tokens_present(name) if auth == "oauth" else None, - "enabled": cfg.get("enabled", True) is not False, + "enabled": mcp_server_enabled(cfg), "tools": cfg.get("tools"), "source": "plugin" if plugin is not None else "config", "plugin": plugin} diff --git a/tui_gateway/methods_profiles.py b/tui_gateway/methods_profiles.py index b1fab60576..f0c0374f32 100644 --- a/tui_gateway/methods_profiles.py +++ b/tui_gateway/methods_profiles.py @@ -570,11 +570,11 @@ def _save_toolset_pin(cfg, enabled, save_config) -> None: def _mcp_entry_enabled(entry: dict) -> bool: - """Editor-side twin of ``enabled_mcp_server_names``: the runtime keys off ``enabled`` (default - True); a legacy ``disabled: true`` (what older editors wrote) is still honoured.""" + """The runtime's ``enabled`` reader; a legacy ``disabled: true`` (what older editors wrote, + migrated by config v46) still reads as off.""" from hermes_cli.tools_config import _parse_enabled_flag - return (_parse_enabled_flag(entry.get("enabled", True), default=True) - and not _parse_enabled_flag(entry.get("disabled", False), default=False)) + from tools.mcp_tool_common import mcp_server_enabled + return mcp_server_enabled(entry) and not _parse_enabled_flag(entry.get("disabled", False), default=False) def _save_mcp_toggles(cfg, enabled, launch_mcp, save_config) -> None: diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 56a188d5fe..cb7db335ae 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1865,13 +1865,13 @@ def _resolve_explicit_toolsets(explicit: list[str], validate_toolset) -> list[st return built_in try: # (enabled, disabled) MCP server names from raw config; both empty on any failure from hermes_cli.config import read_raw_config - from hermes_cli.tools_config import _parse_enabled_flag + from tools.mcp_tool_common import mcp_server_enabled raw_cfg = read_raw_config() mcp_servers = raw_cfg.get("mcp_servers") if isinstance(raw_cfg.get("mcp_servers"), dict) else {} mcp_names, mcp_disabled = set(), set() for name, server_cfg in mcp_servers.items(): if isinstance(server_cfg, dict): - on = _parse_enabled_flag(server_cfg.get("enabled", True), default=True) + on = mcp_server_enabled(server_cfg) (mcp_names if on else mcp_disabled).add(str(name)) except Exception: mcp_names, mcp_disabled = set(), set()