MCP-Anbindung
Verantwortung
MCP (Model Context Protocol) ist ein universelles Protokoll, um externe Werkzeuge (tools)/Datenquellen anzubinden. Hermes tritt sowohl als MCP-Client auf und konsumiert Werkzeuge externer MCP-Server (über mcp_tool in die ToolRegistry überbrückt), als auch als MCP-Server (mcp_serve.py), der die eigenen Werkzeuge anderen Agent-Clients zur Verfügung stellt. Diese Schicht ermöglicht es Hermes, Drittfähigkeiten aufzunehmen, ohne sie per Hand anzupassen.
Designmotiv
Warum MCP wählen statt ein eigenes Werkzeugprotokoll neu zu erfinden? MCP ist ein offener Standard, den Anthropic vorantreibt; Claude Code, Cursor und Codex docken bereits danach an — das Ökosystem existiert. Als Client kann Hermes Dutzende Community-MCP-Server wiederverwenden; als Server kann es von beliebigen MCP-Clients gesteuert werden — duale Interoperabilität geschenkt. Ein selbst gebautes Protokoll würde zwar zur internen ToolRegistry passen, kostet aber das ganze Ökosystem.
Schlüsseldateien
tools/mcp_tool.py— MCP-Client-Brücke: registriert Werkzeuge externer MCP-Server in der ToolRegistrymcp_serve.py— Hermes als MCP-Server, der eigene Werkzeuge freigibt (Repo-Wurzel)registry.register:365-436— über MCP überbrückte Werkzeuge laufen über denselben Registrierungspfadregister_plugin_override_policy:316-365— steuert die Namespace-Zulassung für MCP-/Plugin (plugin)-Werkzeugeoptional-mcps/-Verzeichnis— optionale MCP-Server-Implementierungen
Datenfluss
- Beim Start (oder zur Laufzeit per Hot-Reload) wird die MCP-Konfiguration gelesen;
mcp_toolverbindet sich mit dem externen MCP-Server. - Die vom Server freigegebene Werkzeugliste wird abgerufen und jedes Werkzeug einzeln über
registry.register:365alsToolEntryregistriert (Namespace-Isolation, um Konflikte mit eingebauten Werkzeugen zu vermeiden). - Die Namespace-Zulassung wird von
register_plugin_override_policy:316gesteuert. - Die Hauptschleife sammelt Schemata, dispatched (dispatch)
tool_callund füllt Ergebnisse zurück wie bei eingebauten Werkzeugen. - Reverse: Hermes kann über
mcp_serve.pydie eigenen Werkzeuge für andere MCP-Clients freigeben, damit Editoren/andere Agenten sie aufrufen.
Schlüsselcode
MCP-Werkzeuge in die ToolRegistry überbrücken
Nach erfolgreichem Verbinden wandelt _register_server_tools jedes MCP-Tool in ein Schema um und registriert es; der Handler bindet Servernamen und Timeout über einen Closure fest:
for mcp_tool in server._tools:
if not _should_register(mcp_tool.name):
continue
schema = _convert_mcp_schema(name, mcp_tool)
tool_name_prefixed = schema["name"]
existing_toolset = registry.get_toolset_for_tool(tool_name_prefixed)
if existing_toolset and not existing_toolset.startswith("mcp-"):
logger.warning("MCP '%s': tool '%s' collides with built-in — skipping",
name, mcp_tool.name)
continue
registry.register(name=tool_name_prefixed, toolset=toolset_name, schema=schema,
handler=_make_tool_handler(name, mcp_tool.name, server.tool_timeout),
check_fn=_make_check_fn(name), is_async=False,
description=schema["description"])Das Präfixieren der Werkzeugnamen stellt sicher, dass mehrere Server sich nicht in die Quere kommen; gegen eingebaute Werkzeugsets erfolgt eine Kollisionserkennung: Kollidiert ein Name mit einem nicht mit mcp- beginnenden Werkzeugset, wird er übersprungen statt still ersetzt.
Werkzeugaufruf-Brücke
Wenn die Hauptschleife ein MCP-Werkzeug aufruft, leitet der Handler der Registry an diesen Closure weiter. Er prüft zuerst, ob der Breaker offen ist, holt dann die Verbindung (bei Bedarf mit stdio-Reconnect) und legt den synchronen Aufruf auf einem Hintergrund-Event-Loop ab, um auf die MCP-Antwort zu warten:
def _handler(args: dict, **kwargs) -> str:
if _server_error_counts.get(server_name, 0) >= _CIRCUIT_BREAKER_THRESHOLD:
age = time.monotonic() - _server_breaker_opened_at.get(server_name, 0.0)
if age < _CIRCUIT_BREAKER_COOLDOWN_SEC:
remaining = max(1, int(_CIRCUIT_BREAKER_COOLDOWN_SEC - age))
return json.dumps({"error": (
f"MCP server '{server_name}' unreachable after "
f"{_CIRCUIT_BREAKER_THRESHOLD} failures. Retry in ~{remaining}s. "
f"Do NOT retry this tool yet — use alternative approaches.")},
ensure_ascii=False)
server = _get_connected_server_for_call(server_name)
if not server:
_bump_server_error(server_name)
return json.dumps({"error": f"MCP server '{server_name}' is not connected"},
ensure_ascii=False)Die Fehlermeldung enthält ausdrücklich «Do NOT retry this tool yet» — das ist für das Modell zum Lesen: es soll nicht ständig Iterationen (iterations) verbrennen, um es erneut zu versuchen, während der Breaker offen ist.
Reverse: Hermes selbst als MCP-Server freigeben
mcp_serve.py startet über FastMCP einen stdio-Server, registriert die Nachrichten-Brücken-Werkzeuge und macht sie für jeden MCP-Client list-/read-/sendbar:
mcp = FastMCP("hermes", instructions=(
"Hermes Agent messaging bridge. Use these tools to interact with "
"conversations across Telegram, Discord, Slack, WhatsApp, Signal, Matrix."))
@mcp.tool()
def conversations_list(platform=None, limit=50, search=None) -> str:
"""List active messaging conversations across connected platforms."""
...Der @mcp.tool()-Dekorator registriert sofort — kein manuelles Schema; FastMCP leitet es aus den Typannotationen ab.
Grenzen und Fehler
- MCP-Server abgestürzt: Der Kindprozess stirbt oder der HTTP-Endpunkt fällt aus;
MCPServerTaskverbindet sich automatisch neu; erreichten die Fehler einen Schwellwert, öffnet der Breaker, nachfolgendetool_calls liefern direkt einen Fehler; erst nach Ablauf der Abkühlzeit wird eine einzelne Sonde halb-offen durchgelassen. - stdio-Server-Wiederverwertung: Ein zu lange idlender Prozess wird wiederverwertet, um Ressourcen zu sparen; beim nächsten Aufruf weckt
_request_lazy_reconnectzuerst auf und leitet dann weiter — das kostet wenige Dutzend Millisekunden bis Sekunden Latenz. - Werkzeugnamen-Konflikt: Präfixierte MCP-Werkzeuge, die mit eingebauten Werkzeugsets kollidieren, werden übersprungen; zwei MCP-Server mit gleichem Namen dürfen sich gegenseitig überschreiben, laufen über den
both_mcp-Zweig und schreiben einen Debug-Log.
Zusammenfassung
MCP ist der «Fähigkeits-Bus» von Hermes. mcp_tool lässt externe Werkzeuge in den einheitlichen Werkzeugpool einfließen; mcp_serve.py macht Hermes selbst von außen aufrufbar. Weil alles über die ToolRegistry läuft, muss die Hauptschleife nicht wissen, ob ein Werkzeug eingebaut ist oder von MCP stammt — sie sieht nur Schema und Handler.