Construire un client MCP: découverte, routage et double époque de retour
Type: Build
Languages: Python
Prerequisites: Phase 13, Lesson 07
Time: ~85 minutes
Objectifs d'apprentissage
- Construire chaque MCP
2026-07-28une demande avec des métadonnées actuelles. - Probe des serveurs stdio avec
server/discoveret sélectionnez une version mutuellement prise en charge. - Autoriser une sonde d'héritage limitée uniquement pour les pairs explicitement autorisés.
- Acceptez une ère d' héritage seulement après avoir validé un positif
initializerésultat pour une révision appuyée. - Fusion des listes d'outils déterministes sans écraser silencieusement les collisions.
- Route appels à la paire qui possède chaque outil sans inventer des sessions de protocole.
Le problème
Un agent hôte parle généralement à plus d'un serveur MCP. Il doit découvrir chaque serveur, fusionner les catalogues des outils, résoudre les noms dupliqués, les appels de route et récupérer des défaillances de transport.
Le 2026-07-28La révision rend l'état stable plus simple parce que chaque demande est autonome.
- un serveur moderne qui prend en charge la version préférée;
- un serveur moderne qui renvoie une version reconnue ou une erreur d'en-tête;
- Un serveur hérité dont vous n' avez jamais entendu parler .
server/discoverLe dépôt de la commission - Un serveur ancien qui reste silencieux jusqu' à ce qu' il reçoive
initialize- Je suis désolé .
Traiter chaque erreur de sonde comme héritage est dangereux. Une demande moderne malformée, un serveur surchargé, un processus mort et un serveur ancien peuvent tous produire le même temps ou la fermeture de la connexion. Ces signaux sont ambiguës. Le client doit combiner l'intention explicite de l'opérateur avec des preuves de protocole positifs avant de choisir l'ère héritage.
Le concept
Une session de pairs, pas une session de protocole
Garder un enregistrement par rapport au transport pour chaque processus ou point final du serveur:
- fonction de poignée de transport ou d'envoi;
- l'ère et la version du protocole sélectionnés;
- les capacités du serveur découvertes pour la dernière fois;
- dernière liste d'outils déterministes;
- les identifiants de demande en attente de corrélation;
- santé des transports.
C'est la comptabilité du client. Ce n'est pas l'état de session du protocole. Sur le MCP moderne, le serveur reçoit toujours la version et les capacités actuelles à chaque demande.
Construire chaque demande moderne à partir de zéro
pythondef modern_request(request_id, method, params, version, capabilities):
return {
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": {
**params,
"_meta": {
"io.modelcontextprotocol/protocolVersion": version,
"io.modelcontextprotocol/clientCapabilities": capabilities,
"io.modelcontextprotocol/clientInfo": CLIENT_INFO,
},
},
}Ne joignez pas les métadonnées à un objet de connexion une fois et supposez qu'il a atteint le fil.
Découverte moderne
server/discoverrenvoie les versions prises en charge, les capacités du serveur, les instructions, les indices de cache et l'identité du serveur recommandée. Un client choisit la version moderne la plus haut en charge mutuellement.
La découverte est facultative pour un client moderne uniquement, mais elle est recommandée sur stdio. Certains serveurs anciens acceptent une opération avant l'initialisation, donc l'envoi tools/listLa première peut produire un succès ambigu. server/discovercrée une frontière d'ère propre.
La sonde de compatibilité avec le studio
Un client de studio à deux époques envoie server/discoverIl existe trois classes de résultats:
- DiscoverResult.Le serveur est moderne. Sélectionnez une version mutuellement prise en charge et continuez avec les métadonnées par demande.
- Recognized modern error.Le serveur est moderne.
-32022, choisissez parmidata.supportedPour les erreurs d'en-tête ou de capacité, corrigez la demande.initialize- Je suis désolé . - Ambiguous signal.Une erreur JSON-RPC non reconnue, une échéance de temps, une connexion fermée ou une réponse vide n'identifient pas une époque.
Les erreurs de protocole modernes reconnues comprennent:
-32020HeaderIncohérent-32021Faute de capacité requise-32022Protocol non pris en chargeVersion
Les erreurs modernes reconnues restent modernes même lorsque le pair est sur la liste d'allowness.initializeCe serait une dégradation.
Ne traitez pas-32601Il n'est possible de faire une enquête par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un seul analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste par un analyste
L'autorisation est l'intention de l'opérateur, pas une preuve
La compatibilité héréditaire doit être une propriété explicite d'une configuration de pair fichée:
pythonclient.add_server("archive", archive_transport, allow_legacy=True)Passer le temps à la sélectionner.allow_legacy=TrueIl échoue après une découverte ambiguë et ne reçoit jamais .initialize- Je suis désolé .
L'autorisation de l'internaute est accordée par le client, mais pas par l'ère.initializedans le cadre d'une date limite imposée par le transport, il exige alors tout ce qui suit:
- un JSON-RPC
2.0la réponse avec l'identifiant de la demande correspondante; - Exactement une .
resultet nonerrorLe dépôt de la commission - - Il y a
protocolVersiondans le jeu de révision de l'héritage configuré du client; - une valeur d' objet
capabilitieschamp; - - Il y a
serverInfoobjet avec une chaîne non videnameetversionles champs.
Un délai, la fermeture de la connexion, la réponse à l'erreur, le résultat malformé, l'identifiant inégalé ou la révision non prise en charge échouent à fermer. Seul un résultat positif structurellement valide sélectionne l'ère héréditaire. Le code passe legacy_probe_timeout_msà l'adaptateur de transport; un véritable stdio ou adaptateur HTTP doit appliquer cette date limite plutôt que de simplement l'enregistrer.
Conservez l'ère sélectionnée en cache pour le transport.
L'héritage est une branche de compatibilité
Une fois que la sonde délimité renvoie des preuves de l'héritage positif valides, le client utilise la version d'héritage sélectionnée exactement comme définie par cette révision:
- Vérifiez l'enveloppe de réponse et l'identifiant de corrélation.
- Vérifiez que la révision négociée est dans l'ensemble de l'héritage configuré.
- Enregistrer les capacités validées et l'identité du serveur.
- Envoyez-moi .
notifications/initializedseulement après que tous les chèques aient passé. - Utilisez des formes de demande héritées pour cette durée de vie du transport.
Cette branche existe pour l'interopérabilité avec des pairs connus. Ce n'est pas la conception par défaut pour de nouveaux serveurs ou de nouvelles demandes. Si le transport redémarre ou que son point d'extrémité change, jetez le cache de l'ère des pairs et négociez à nouveau.
Outils de découverte et de mise en cache
Pour chaque paire actif, appelez tools/listUn résultat moderne comprend resultType- Je suis là .ttlMs, et cacheScope- Respecter le signe de fraîcheur dans le contexte de l'autorisation correcte.
Les clients doivent traiter un disparu .resultTypeà partir d' un serveur ancien comme "complete"Ne nécessite pas de champs de cache modernes sur une réponse d'une ère précédente négociée.
Le serveur doit retourner l'ordre déterministe. Le client doit également trier avant la fusion afin que l'ordre du registre local ne dépend pas du moment de démarrage du processus.
fusion de l'espace de noms à risque de collision
Deux serveurs peuvent exposer les deux .search. Choisir une politique déclarée:
- Prefix on collision.Gardez le premier nom canonique et exposez les collisions ultérieures comme
<server>/<tool>- Je suis désolé . - Reject on collision.Ne chargez pas le double et ne faites pas apparaître une erreur de configuration claire.
- Silent overwrite.Ne l'utilisez jamais, il cache quel serveur reçoit une action sélectionnée par le modèle.
Le modèle voit le nom canonique.tools/callutilise le nom local déclaré par le serveur propriétaire.
Routage d'une appel
Le routage est une simple recherche:
textcanonical tool name
-> peer name + local tool name
-> new JSON-RPC request id
-> modern request metadata or explicit legacy shape
-> matching response idNe pas envoyer d'appel lorsque le transport de son propriétaire n'est pas disponible.tools/list. Les demandes modernes perdues en vol lors d'un transport défectueux peuvent être retenues avec un nouvel identifiant JSON-RPC lorsque la politique de sécurité de l'opération le permet.
Notifications et abonnements
Les modifications modernes des listes et des ressources ne sont effectuées que sur une liste ouverte par le client subscriptions/listenLe client envoie le filtre de notification, attend notifications/subscriptions/acknowledged, et corréle les événements avec l'identifiant de demande d'écoute dans les métadonnées de notification.
Lorsqu'il est déconnecté, ouvrez une nouvelle demande d'écoute et réaffectez les listes ou ressources pertinentes.Last-Event-ID- Je suis désolé .
Aucune demande initiée par le serveur
Les serveurs modernes n'appellent pas le client avec des demandes indépendantes JSON-RPC pour le prélèvement d'échantillons, l'élicitation ou la racine.input_required, et le client tente à nouveau la demande originale après avoir rempli les demandes d'entrée intégrées.
Ne bloquez pas le lecteur de réponse de l'utilisateur pendant la commande.
Utilisez-le
code/main.pyIl utilise des fonctions de pair en cours de processus afin que les décisions du protocole restent visibles. Il se connecte à deux pairs modernes et à un paire hérité délibérément autorisé, puis fusionne et route leurs outils.
bashcd code
python3 main.py
python3 -m unittest discover tests -vLes tests prouvent les limites que les démos normaux manquent:
- les demandes modernes répéteront les métadonnées;
-32022réessaye la découverte moderne sans initialisation;- les erreurs modernes reconnues ne sont jamais rebaptisées, même pour un paire autorisé;
- Les temps de sortie, la fermeture de la connexion, les réponses vides et les erreurs non reconnues ne déclenchent pas
initializesans permis; - Un coéquipier autorisé devient héritier seulement après un coéquipier valide et soutenu
initializerésultat; - les résultats antérieurs mal formés et non soutenus rendent le produit non disponible;
- une époque sélectionnée avec succès est mise en cache pour la durée de vie du transport.
La faire partir
Cette leçon va à l' air .outputs/skill-mcp-client-harness.mdIl comporte le timbre de requête moderne, la négociation de l'ère studio, la fusion déterministe de l'espace de noms, le routage et une branche de compatibilité héréditaire fermées en échec.
Exercices
- Faites une fausse restitution de serveur .
-32022Confirmer que le client échoue au lieu d'envoyerinitialize- Je suis désolé . - Permettez un faux serveur hérité, faites-le limité
initialize- Je vais vous montrer que le groupe reste.unknownet non disponible. - Ajouter
cacheScope: "private"Confirmer que le client ne partage jamais le résultat caché d'un contexte avec l'autre. - Modifiez la politique de collision en rejet et faites échouer le démarrage avec les deux noms de pairs dans l'erreur.
- Ajouter une finite
subscriptions/listenEn cas de perte de flux, écoutez à nouveau avec un nouvel identifiant de requête et des outils de rééducation.
Les termes clés
| Term | Meaning |
|---|---|
| Peer | Client-side record for one server transport and its discovered data |
| Protocol era | Modern per-request metadata or legacy initialization semantics |
| Discovery probe | Initial server/discover used to identify the stdio era |
| Recognized modern error | Error that proves modern behavior and forbids legacy fallback |
| Legacy allowlist | Operator configuration permitting one bounded compatibility probe for a pinned peer |
| Positive legacy evidence | Valid, correlated initialize result for an explicitly supported legacy revision |
| Merged namespace | Canonical tool names across all active peers |
| Collision policy | Prefix or reject rule for duplicate tool names |
| Era cache | Selected modern or legacy behavior stored for one transport peer |
| Transport recovery | Restart or reconnect, rediscover, relist, and retry safely with a new id |
Pour en savoir plus
This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.
Browse the complete course catalog or open this lesson on GitHub.