132 lines
6.2 KiB
Python
132 lines
6.2 KiB
Python
"""Make an installed plugin's MCP servers and skills usable in THIS process now (#87770 follow-up).
|
|
|
|
:func:`hermes_cli.plugins_activation.activate_plugin_now` loads the plugin; skills are live from that
|
|
point (``skill_view`` resolves ``<plugin>:<skill>``). MCP servers are not: nothing connects them until
|
|
``reload.mcp``. :func:`connect_plugin_mcp` connects the plugin's servers one by one (the connector
|
|
flow's ``register_mcp_servers({name: config})``), and :func:`live_notice` renders what became usable
|
|
as the note an open chat receives on its next turn.
|
|
|
|
MCP tools are deferred behind ``tool_search`` / ``tool_describe`` / ``tool_call``, so adding them to a
|
|
live session leaves the model-facing tool array (and the cached prompt prefix) byte-identical.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
def connect_plugin_mcp(activation: dict[str, Any], portable: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
|
|
"""Connect every portable MCP server ``activation`` defers, under the caller's profile scope.
|
|
``portable`` is the manager's portable server configs, read together with ``activation``.
|
|
|
|
Returns ``[{name, connected, tools, error?}]``, one row per server; a server that fails to
|
|
connect is reported, never retried. Never raises."""
|
|
names = list((activation.get("deferred") or {}).get("mcp_servers") or ())
|
|
if not names:
|
|
return []
|
|
try:
|
|
from tools.mcp_tool_config import _filter_suspicious_mcp_servers, _load_mcp_config
|
|
from tools.mcp_tool_discovery import register_mcp_servers
|
|
configured = _load_mcp_config()
|
|
except Exception as exc:
|
|
logger.warning("plugin MCP activation could not read server config: %s", exc)
|
|
return [{"name": n, "connected": False, "tools": [], "error": str(exc)} for n in names]
|
|
rows = []
|
|
for name in names:
|
|
config = configured.get(name) or _filter_suspicious_mcp_servers(
|
|
{name: portable[name]} if name in portable else {}).get(name)
|
|
if not isinstance(config, dict):
|
|
rows.append({"name": name, "connected": False, "tools": [], "error": "no MCP configuration"})
|
|
continue
|
|
try:
|
|
register_mcp_servers({name: config})
|
|
from tools.connectors.mcp import _registered_tool_names
|
|
tools = [t for t in _registered_tool_names(name) if not t.endswith(_utility_suffixes())]
|
|
except Exception as exc:
|
|
rows.append({"name": name, "connected": False, "tools": [], "error": str(exc)})
|
|
continue
|
|
row: dict[str, Any] = {"name": name, "connected": bool(tools), "tools": tools}
|
|
if not tools:
|
|
row["error"] = _server_error(name) or "the server did not connect"
|
|
rows.append(row)
|
|
return rows
|
|
|
|
|
|
def _utility_suffixes() -> tuple:
|
|
"""The resource/prompt wrappers every MCP server gets (``mcp__x__list_resources``, ...): callable,
|
|
but they say nothing about what the plugin does, so they are left out of counts and listings."""
|
|
from tools.mcp_tool_registration import _UTILITY_HANDLER_FACTORIES
|
|
return tuple(f"_{kind}" for kind in _UTILITY_HANDLER_FACTORIES)
|
|
|
|
|
|
def _server_error(name: str) -> Optional[str]:
|
|
try:
|
|
from tools.mcp_tool_discovery import get_mcp_status
|
|
for entry in get_mcp_status():
|
|
if entry.get("name") == name:
|
|
return entry.get("error")
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
def plugin_skills(plugin_key: str) -> list[dict[str, str]]:
|
|
"""``[{name, description}]`` for the skills ``plugin_key`` registered (qualified ``<ns>:<skill>``)."""
|
|
try:
|
|
from hermes_cli.plugins import get_plugin_manager
|
|
skills = get_plugin_manager()._plugin_skills
|
|
except Exception:
|
|
return []
|
|
return [{"name": qualified, "description": str(entry.get("description") or "")}
|
|
for qualified, entry in sorted(skills.items()) if entry.get("plugin_key") == plugin_key]
|
|
|
|
|
|
def live_notice(activation: dict[str, Any]) -> str:
|
|
"""The note an open chat gets on its next turn: connected MCP servers with their tools, skills,
|
|
and what waits for the next session. Empty when nothing became usable."""
|
|
live = activation.get("live_now") or {}
|
|
servers, skills = live.get("mcp_servers") or [], live.get("skills") or []
|
|
deferred = activation.get("deferred") or {}
|
|
if not servers and not skills:
|
|
return ""
|
|
lines = [f"[plugin installed: {activation.get('name') or activation.get('key')}]"]
|
|
connected = [s for s in servers if s.get("connected")]
|
|
if connected:
|
|
lines.append("MCP servers now connected; call their tools with tool_describe / tool_call:")
|
|
for server in connected:
|
|
lines.append(f"- {server['name']}")
|
|
listing = _tool_listing(server["tools"])
|
|
if listing:
|
|
lines.extend(f" {line}" for line in listing.splitlines())
|
|
for server in (s for s in servers if not s.get("connected")):
|
|
lines.append(f"- {server['name']}: not connected ({server.get('error') or 'unknown error'})")
|
|
if skills:
|
|
lines.append("Skills now available; load one with skill_view:")
|
|
lines.extend(f"- {s['name']}: {s['description']}" if s.get("description") else f"- {s['name']}"
|
|
for s in skills)
|
|
later = [f"{len(deferred['tools'])} Python tools" if deferred.get("tools") else "",
|
|
f"{len(deferred['prompt'])} prompt sections" if deferred.get("prompt") else ""]
|
|
if any(later):
|
|
lines.append("Next session: " + ", ".join(p for p in later if p) + ".")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def _tool_listing(names: list[str]) -> str:
|
|
"""``name: first sentence`` per tool."""
|
|
try:
|
|
from tools.registry import registry
|
|
from tools.tool_search_catalog import _short_desc
|
|
definitions = registry.get_definitions(set(names), quiet=True)
|
|
except Exception:
|
|
return ""
|
|
rows = []
|
|
for definition in definitions:
|
|
fn = definition.get("function") or definition
|
|
name = str(fn.get("name") or "")
|
|
if name:
|
|
desc = _short_desc(fn.get("description") or "")
|
|
rows.append(f"{name}: {desc}" if desc else name)
|
|
return "\n".join(sorted(rows))
|