feat(gateway): key-addressed plugins.manage rows + portable MCP toolset fold-in
plugins.manage list rows now carry the canonical registry key and a portable flag (Agent Plugins v1 plugin.json packages), and toggles address the key — bare names collide across category dirs (image_gen/fal vs video_gen/fal), so name-addressed toggles flipped both. Portable packages' in-memory MCP servers also fold into enabled_mcp_server_names(); without that their tools registered with the MCP runtime but never reached the model's schema.
This commit is contained in:
parent
51597c5e07
commit
a60b492e07
|
|
@ -1061,6 +1061,21 @@ def _read_manifest_info(d: Path, prefix: str):
|
|||
return name, version, description, key
|
||||
|
||||
|
||||
def _is_portable_plugin_dir(dir_path) -> bool:
|
||||
"""True when *dir_path* is an Agent Plugins v1 package (``plugin.json``
|
||||
only — a native ``plugin.yaml`` takes precedence, matching the loader)."""
|
||||
try:
|
||||
d = Path(dir_path)
|
||||
if not d.is_dir():
|
||||
return False
|
||||
if (d / "plugin.yaml").exists() or (d / "plugin.yml").exists():
|
||||
return False
|
||||
portable_file = d / "plugin.json"
|
||||
return portable_file.exists() or portable_file.is_symlink()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def _scan_level(
|
||||
base: Path,
|
||||
source: str,
|
||||
|
|
|
|||
|
|
@ -2123,21 +2123,38 @@ def _parse_enabled_flag(value, default: bool = True) -> bool:
|
|||
|
||||
|
||||
def enabled_mcp_server_names(config: dict) -> Set[str]:
|
||||
"""Names of MCP servers globally enabled in config.yaml.
|
||||
"""Names of MCP servers globally enabled in config.yaml or by a plugin.
|
||||
|
||||
Shared by the gateway/CLI platform resolver (``_get_platform_tools``) and
|
||||
the cron per-job toolset resolver (``cron.scheduler``) so every path agrees
|
||||
on MCP membership. A server is enabled unless its config sets an explicitly
|
||||
falsey ``enabled`` (per ``_parse_enabled_flag``: false/0/no/off) — a missing
|
||||
flag or an unrecognized value is treated as enabled.
|
||||
|
||||
Portable Agent Plugins contribute MCP servers in-memory rather than via
|
||||
``config.yaml`` (see ``PluginManager.get_portable_mcp_servers``). Those are
|
||||
included here so their tools fold into platform toolsets like native
|
||||
servers do — the user's opt-in is enabling the plugin itself. Without this,
|
||||
a portable server registers with the MCP runtime but its tools never reach
|
||||
the model's schema.
|
||||
"""
|
||||
mcp_servers = (config or {}).get("mcp_servers") or {}
|
||||
return {
|
||||
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)
|
||||
}
|
||||
try:
|
||||
from hermes_cli.plugins import discover_plugins, get_plugin_manager
|
||||
|
||||
discover_plugins()
|
||||
portable = set(get_plugin_manager().get_portable_mcp_servers())
|
||||
# Native config wins on a name collision (mirrors _load_mcp_config).
|
||||
names |= portable - set(mcp_servers)
|
||||
except Exception:
|
||||
logger.debug("Failed to include portable MCP servers", exc_info=True)
|
||||
return names
|
||||
|
||||
|
||||
def _exempt_explicit_platform_native(
|
||||
|
|
|
|||
|
|
@ -1803,10 +1803,10 @@ def _(rid, params: dict) -> dict:
|
|||
agree on what's installed and what's enabled.
|
||||
|
||||
Actions:
|
||||
- ``list`` → {"plugins": [{name, version, description, source,
|
||||
status}], "user_count": N, "bundled_count": M}
|
||||
- ``toggle`` → flip ``name`` based on ``enable`` (bool). Returns the
|
||||
refreshed row plus {"ok", "unchanged"}.
|
||||
- ``list`` → {"plugins": [{name, key, version, description, source,
|
||||
status, portable}], "user_count": N, "bundled_count": M}
|
||||
- ``toggle`` → flip ``key`` (or ``name``) based on ``enable`` (bool).
|
||||
Returns the refreshed row plus {"ok", "unchanged"}.
|
||||
"""
|
||||
action = params.get("action", "list")
|
||||
try:
|
||||
|
|
@ -1814,6 +1814,7 @@ def _(rid, params: dict) -> dict:
|
|||
_discover_all_plugins,
|
||||
_get_disabled_set,
|
||||
_get_enabled_set,
|
||||
_is_portable_plugin_dir,
|
||||
_plugin_status,
|
||||
)
|
||||
|
||||
|
|
@ -1827,10 +1828,17 @@ def _(rid, params: dict) -> dict:
|
|||
out.append(
|
||||
{
|
||||
"name": name,
|
||||
# Canonical registry key (e.g. ``image_gen/fal``). Names
|
||||
# can collide across category dirs — both fal backends
|
||||
# are named "fal" — so toggles must address the key.
|
||||
"key": key,
|
||||
"version": str(version or ""),
|
||||
"description": desc or "",
|
||||
"source": source,
|
||||
"status": _plugin_status(name, enabled, disabled, key=key),
|
||||
# Agent Plugins v1 package (plugin.json — the portable
|
||||
# skills/MCP format) vs a native Hermes plugin.
|
||||
"portable": _is_portable_plugin_dir(_dir),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
|
@ -1850,20 +1858,24 @@ def _(rid, params: dict) -> dict:
|
|||
if action == "toggle":
|
||||
from hermes_cli.plugins_cmd import dashboard_set_agent_plugin_enabled
|
||||
|
||||
name = (params.get("name") or "").strip()
|
||||
if not name:
|
||||
return _err(rid, 4019, "plugins.toggle requires a 'name'")
|
||||
# Prefer the canonical key — bare names are ambiguous when two
|
||||
# category plugins share one (image_gen/fal vs video_gen/fal).
|
||||
ident = (params.get("key") or params.get("name") or "").strip()
|
||||
if not ident:
|
||||
return _err(rid, 4019, "plugins.toggle requires a 'key' or 'name'")
|
||||
enable = bool(params.get("enable"))
|
||||
result = dashboard_set_agent_plugin_enabled(name, enabled=enable)
|
||||
result = dashboard_set_agent_plugin_enabled(ident, enabled=enable)
|
||||
if not result.get("ok"):
|
||||
return _err(rid, 5026, result.get("error") or "toggle failed")
|
||||
row = next((r for r in _rows() if r["name"] == name), None)
|
||||
row = next(
|
||||
(r for r in _rows() if ident in (r["key"], r["name"])), None
|
||||
)
|
||||
return _ok(
|
||||
rid,
|
||||
{
|
||||
"ok": True,
|
||||
"unchanged": bool(result.get("unchanged")),
|
||||
"name": name,
|
||||
"name": ident,
|
||||
"plugin": row,
|
||||
},
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue