Intégration MCP
Responsabilité
MCP (Model Context Protocol) est un protocole standard pour connecter des outils (tools) et sources de données externes. Hermes agit à la fois comme client MCP en consommant les outils exposés par des serveurs MCP externes (pontés dans le ToolRegistry via mcp_tool), et comme serveur MCP (mcp_serve.py) en exposant ses propres outils à d'autres clients agents. Cette couche permet à Hermes d'accueillir indéfiniment des capacités tierces.
Mot de conception
Pourquoi choisir MCP plutôt que réinventer un protocole d'outils ? MCP est un standard ouvert poussé par Anthropic ; Claude Code, Cursor et Codex s'y conforment déjà, l'écosystème est en place. En tant que client, Hermes réutilise des dizaines de serveurs MCP communautaires ; en tant que serveur, il peut être piloté par n'importe quel client MCP, décrochant l'interopérabilité bidirectionnelle gratuitement. Réinventer un protocole collerait au ToolRegistry interne, mais au prix de tout l'écosystème.
Fichiers clés
tools/mcp_tool.py— pont client MCP : enregistre les outils des serveurs MCP externes dans le ToolRegistrymcp_serve.py— Hermes comme serveur MCP exposant ses propres outils (racine du dépôt)registry.register:365-436— les outils pontés via MCP passent par le même chemin d'enregistrementregister_plugin_override_policy:316-365— contrôle l'admission des espaces de noms pour les outils MCP / pluginsrépertoire optional-mcps/— implémentations optionnelles de serveurs MCP
Flux de données
- Au démarrage (ou en chargement à chaud à l'exécution), lecture de la config MCP,
mcp_toolse connecte aux serveurs MCP externes. - Récupère la liste des outils exposés par le serveur, et enregistre chacun via
registry.register:365commeToolEntry(isolation par espace de noms, pour éviter les collisions avec les outils intégrés). - L'admission des espaces de noms est contrôlée par
register_plugin_override_policy:316. - La boucle principale collecte les schémas, distribue les tool_calls et réinjecte les résultats comme pour les outils intégrés.
- Sens inverse : Hermes peut exposer ses propres outils à d'autres clients MCP via
mcp_serve.py, pour les éditeurs ou autres agents.
Code clé
Pontage des outils MCP dans le ToolRegistry
Une fois connecté, _register_server_tools convertit chaque outil MCP en schéma et l'enregistre ; le handler attache le nom du serveur et le délai d'attente via une fermeture :
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"])Le préfixage des noms d'outils garantit qu'aucune collision n'aura lieu entre serveurs, et un contrôle de conflit est fait avec les outils intégrés : si un outil entre en collision avec un toolset non préfixé mcp-, on saute l'enregistrement plutôt que de remplacer silencieusement.
Pontage des appels d'outils
Quand la boucle principale invoque un outil MCP, le handler du registre (registry) forward vers cette fermeture. Il vérifie d'abord si le disjoncteur est ouvert, prend la connexion (en déclenchant au besoin une reconnexion stdio), puis pousse l'appel synchrone sur la boucle d'événements en arrière-plan en attendant la réponse MCP :
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)Le message d'erreur précise explicitement « Do NOT retry this tool yet », c'est destiné au modèle : l'empêcher de brûler des itérations (iterations) à réessayer pendant que le disjoncteur est ouvert.
Sens inverse : exposer Hermes lui-même comme serveur MCP
mcp_serve.py utilise FastMCP pour démarrer un serveur stdio, enregistre les outils de pontage de messages, n'importe quel client MCP peut list/read/send des messages :
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."""
...Le décorateur @mcp.tool() fait l'enregistrement, pas besoin d'écrire le schéma à la main — FastMCP l'infère à partir des annotations de type.
Limites et échecs
- Serveur MCP en panne : le sous-processus meurt ou le endpoint HTTP tombe,
MCPServerTaskse reconnecte automatiquement ; une fois le seuil de failures atteint, le disjoncteur s'ouvre, les tool_calls suivants renvoient une erreur, et ce n'est qu'après le cooldown qu'une sonde est à nouveau autorisée. - Récupération des serveurs stdio : un processus inactif trop longtemps est récupéré pour économiser des ressources ; au prochain appel
_request_lazy_reconnectle réveille avant de forwarder, induisant une latence allant de quelques dizaines de millisecondes à quelques secondes. - Conflit de noms d'outils : un outil MCP préfixé qui collisionne avec un toolset intégré est sauté ; si deux serveurs MCP entrent en conflit sur le même nom, la surcharge est autorisée via la branche
both_mcpavec un log de debug.
Résumé
MCP est le « bus d'extension de capacités » d'Hermes. mcp_tool fait entrer les outils externes dans le pool d'outils unifié sans couture ; mcp_serve.py permet à Hermes lui-même d'être appelé de l'extérieur. Tout passe par le ToolRegistry, donc la boucle principale n'a pas à se soucier de savoir si un outil vient de l'intégré ou de MCP.