Skip to content

Intégration MCP

源码版本v2026.7.20

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

Flux de données

  1. Au démarrage (ou en chargement à chaud à l'exécution), lecture de la config MCP, mcp_tool se connecte aux serveurs MCP externes.
  2. Récupère la liste des outils exposés par le serveur, et enregistre chacun via registry.register:365 comme ToolEntry (isolation par espace de noms, pour éviter les collisions avec les outils intégrés).
  3. L'admission des espaces de noms est contrôlée par register_plugin_override_policy:316.
  4. La boucle principale collecte les schémas, distribue les tool_calls et réinjecte les résultats comme pour les outils intégrés.
  5. 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 :

python
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 :

python
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 :

python
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, MCPServerTask se 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_reconnect le 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_mcp avec 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.

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).