0388d03f22
_profile_scoped_rpc (mcp.servers.*, insights.get, cron.manage, skills.manage,
mcp.catalog, plugins.manage) bound only HERMES_HOME for params.profile.
config.yaml's ${VAR} expansion (config._env_ref_lookup) and the MCP probe's
header/env interpolation resolve through get_secret, which with no scope
installed reads plain os.environ, i.e. the launch profile's values. The
Desktop MCP setup Test-connection (mcp.servers.test) for a secondary thus
sent the default profile's token (or the literal placeholder) and reported
green against the wrong credential - the JSON-RPC twin of the dashboard
REST gap fixed in #110271 (#109901).
Bind the same home + secret + terminal composition a turn binds
(_session_profile_runtime_scope), hydrating the profile's external secret
sources first. os.environ is never mutated. Drops the now-unused
mcp_rpc_helpers.reset_profile.
68 lines
2.8 KiB
Python
68 lines
2.8 KiB
Python
"""Shared helpers for the per-profile MCP lifecycle RPCs (mcp.servers.*).
|
|
|
|
Published onto ``tui_gateway.server`` as ``_mcp_summarize_server`` so the rebound handler
|
|
bodies in methods_tools resolve it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict
|
|
|
|
|
|
def summarize_server(name: str, cfg: dict) -> Dict[str, Any]:
|
|
"""Serialize one server's config for a UI (no secret values).
|
|
|
|
Mirrors web_server._mcp_server_summary plus ``oauth_tokens_present`` so a UI can
|
|
tell an OAuth server that still needs authentication from one already authenticated.
|
|
"""
|
|
from hermes_cli.mcp_config import _oauth_tokens_present
|
|
|
|
cfg = cfg if isinstance(cfg, dict) else {}
|
|
transport = "http" if cfg.get("url") else ("stdio" if cfg.get("command") else "unknown")
|
|
auth = cfg.get("auth")
|
|
headers = cfg.get("headers") or {}
|
|
if not auth and isinstance(headers, dict) and any(str(key).lower() == "authorization" for key in headers):
|
|
auth = "header"
|
|
return {
|
|
"name": name,
|
|
"transport": transport,
|
|
"url": cfg.get("url"),
|
|
"command": cfg.get("command"),
|
|
"args": list(cfg.get("args") or []),
|
|
"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,
|
|
"tools": cfg.get("tools")}
|
|
|
|
|
|
# ---- BEGIN PLUGIN-COMPAT (revert-scheduled; see COMPAT_MANIFEST.md) ----
|
|
# Names external plugins imported from this module before the Sep 2026 decomposition.
|
|
# Internal code MUST NOT use these (scripts/check_compat_pointers.py fails CI if it does).
|
|
# The whole block is removed by reverting the commit that added it.
|
|
from typing import Optional # noqa: F401,E402
|
|
from typing import Tuple # noqa: F401,E402
|
|
|
|
def resolve_profile(rid, params, err_fn) -> Tuple[Optional[Any], Optional[dict]]:
|
|
"""Resolve the optional ``profile`` param to a HERMES_HOME override token.
|
|
|
|
Returns ``(token, error)``: ``token`` is None for the launch profile (no
|
|
override) or an opaque reset token; ``error`` is a JSON-RPC error dict
|
|
(built via ``err_fn``) when the named profile doesn't exist. Callers reset
|
|
``token`` in a finally via :func:`reset_profile`.
|
|
"""
|
|
profile = str(params.get("profile") or "").strip()
|
|
if not profile:
|
|
return None, None
|
|
from hermes_cli.profiles import get_profile_dir
|
|
from hermes_constants import set_hermes_home_override
|
|
|
|
try:
|
|
profile_dir = get_profile_dir(profile)
|
|
except ValueError:
|
|
return None, err_fn(rid, 4064, f"profile '{profile}' not found")
|
|
if not profile_dir or not profile_dir.is_dir():
|
|
return None, err_fn(rid, 4064, f"profile '{profile}' not found")
|
|
return set_hermes_home_override(str(profile_dir)), None
|
|
# ---- END PLUGIN-COMPAT ----
|