Tout le monde disséque le MCP comme s’il s’agissait de la pierre de Rosetta de l’IA. Diagrammes! Whitepapers! Mais où, mes chers druides de données, sont les paquets réels? voici ce qui me pousse à me nourrir: nobody's showing the actual packets. C'est comme essayer d'apprendre la chirurgie à partir d'une affiche de motivation. Bien sûr, le cadre théorique est *inspirant*, mais je veux voir le sang et les intestins! Où est le JSON brut? Où sont les déchets stdin / stdout? Comment dois-je animer ma création quand je ne peux même pas voir la foudre? C'est comme essayer d'apprendre la chirurgie à partir d'une affiche de motivation. Bien sûr, le cadre théorique est *inspirant*, mais je veux voir le sang et les intestins! Où est le JSON brut? Où sont les déchets stdin / stdout? Comment dois-je animer ma création quand je ne peux même pas voir la foudre? Essayer d’apprendre un protocole sans déchets de fil, c’est comme essayer d’apprendre la clôture à partir d’un PowerPoint. Give me the electrons or give me death. Maintenant que nous avons notre mantra, allons creuser ! Est-ce là votre modèle d’affaires ? Votre plan d’affaires MCP : Apprendre le MCP Un miracle se produit... ? Les bénéfices C'est l'équivalent de la mise en place du plomb en or. Alchimie, mais avec beaucoup plus de JSON. Heureusement... # Tu n’es pas seule ! Il semblerait que des filets d'encre aient été versés sur ce que l'on appelle le Protocole de Contexte Modèle. Tout le monde parle de l'évolution des LLM, de l'appel à outils, de l'appel à fonctions, du bidirectionnel et de la capacité. "What's this look like ON THE WIRE???" Tout le protocole est documenté à Je ne veux pas de philosophie, je veux Je veux me sentir comme le Dr Frankenstein tendant à mon golem, l'éclair et tout. Modèle de protocole.io see Malheur à moi, un simple habitant parmi les illuminés, jusqu'à ce que je m'ouvre ainsi ! Les premières observations sur le terrain Le MCP est très, très nouveau. Marge de saignement. Possiblement encore saignant. Peut-être qu'un jour, des livres entiers seront écrits sur sa politique interne, comme une sorte de convention constitutionnelle numérique. Mais pour l’instant, une chose se distingue avant tout : L'appel à outils est tout ce dont vous avez besoin Si nous zoomons le chemin, le chemin dans ce cas d'utilisation unique et critique - On peut ignorer a Nous pouvons simplifier jusqu'à ce que nous regardions le beau squelette du système: seulement quatre types de messages pour la sortie, et trois pour l'entrée. (Nous expliquerons pourquoi les chiffres ne correspondent pas exactement un peu plus tard.) tool invocation lot Initialisé Initialisé Outils / Liste Outils / Appel (et les 3 résultats qui reviennent) Vous pouvez comprendre l’ensemble du protocole en connaissant ces sept messages. La météo (de ) Modèle de protocole.io Modèle de protocole.io Nous utiliserons — un exemple de serveur MCP simple qui vous permet de consulter les données de prévision et d'alerte. Vous pouvez le trouver dans leur GitHub ici: [ weather.py https://github.com/modelcontextprotocol/quickstart-resources/blob/main/weather-server-python/weather.py Parlons de ce qui se passe réellement « sur le fil » lorsque vous parlez de cette chose. Première étape : le MCP Cela signifie que le spec ne se soucie pas de la façon dont vous vous connectez au serveur. HTTPS? Sockets Web? Tuyaux nommés? Tappage dans un ancien câble de vampire Ethernet? Le transport agnostique. Dans la pratique, la méthode la plus courante consiste à lancer le serveur en tant que sous-processus et à communiquer sur et Cela vous donne un bon canal de communication privé. (Techniquement plein duplex, mais si vous ajoutez , nous l'appellerons 1.5-duplex pour être mignon). stdin stdout stderr Comment les messages circulent MCP utilise JSON-RPC 2.0 sur le fil. Cela vous donne un protocole de demande/réponse, ainsi que des notifications. Chaque message est envoyé comme un par ligne de JSON.C'est comme un service de télégramme numérique où chaque ligne est un paquet complet de sens. Est-ce dans le spec? Une sorte de; pas vraiment. Le spec laisse ouvert. Mais ce JSON délimité par la nouvelle ligne est l'idiome par défaut. Le mythe de « stdin / stdout considéré comme nocif » Oui, vous pouvez trouver des articles de blog dramatiques affirmant que ce modèle est "dangereux" ou "ineffable". Nous ne voulons pas déclencher un incendie dans le laboratoire, n’est-ce pas ? pourquoi les gens diraient-ils cela si ce n’était pas vrai ? Vous voyez, dans Ye Olden Times, les lignes extrêmement longues pourraient causer des surflots de tampon (bonjour, Morris Worm). Heureusement, la plupart des logiciels modernes ne souffrent pas de ce problème (trop! les doigts ont été croisés!). Les analyseurs JSON modernes sont à la fois rapides et résilients. Voulez-vous envoyer une alerte météorologique de 256 Mo? Allez-y. Ces avertissements proviennent généralement de personnes qui câblent manuellement des descripteurs de fichiers en C. Vous ne faites pas cela. module qui a été construit précisément pour ce type de tâche. subprocess # fork off an MCP subprocess import subprocess as sp # Redirect stderr to /dev/null stderr = sp.DEVNULL # Start the weather.py process process = sp.Popen( ["uv", "run", "weather.py"], stdin=sp.PIPE, stdout=sp.PIPE, stderr=stderr ) Tout natif Python. Aucun package supplémentaire. Batteries incluses. Le Le module remplace les flux I/O de l’enfant par des tuyaux, vous donnant une maîtrise complète.Nous pouvons également envoyer nos nouveaux signaux de processus si nous le souhaitons. subprocess Maintenant, nous sommes enfin prêts à faire la science du protocole comme un proper digital necromancer! Plot Twist : C’est embarrassantement simple MCP, dans toutes ses regalies d’entreprises, c’est justement cela : YOU: "Hello, I speak robot." SERVER: "Delightful. I also speak robot." YOU: "Excellent, we both can speak robot!" YOU: "What tricks can you do?" SERVER: "I can juggle and summon storms." YOU: "Storm, please!" SERVER: "⛈️ As you wish." Tout ce protocole n’est qu’une façon très formelle de demander à quelqu’un de faire quelque chose et d’obtenir la confirmation qu’il l’a fait. Le reste est juste une cérémonie de classe entreprise et une colle brillante autour de ce bracelet magnifiquement simple. 7 échanges de courtoisie. C’est ça. Stitching the Monster Together : un prototype de travail Nous allons engendrer un serveur météorologique et l'interroger comme un scientifique fou. Conseil #1: La meilleure façon de comprendre un protocole est de l'abuser jusqu'à ce qu'il crie, puis de le patcher jusqu'à ce qu'il coopère. Conseil #1: La meilleure façon de comprendre un protocole est de l'abuser jusqu'à ce qu'il crie, puis de le patcher jusqu'à ce qu'il coopère. Conseil #2: Si vous torturez vos LLM, je ne serai pas tenu responsable de ce qui vous arrive dans The After Times When AI Taketh Over! Conseil #2: Si vous torturez vos LLM, je ne serai pas tenu responsable de ce qui vous arrive dans The After Times When AI Taketh Over! Étape 1 : Convoquer notre mineur numérique # Behold! We create life! # (fork off an MCP subprocess) import subprocess # Start the weather.py process # Use `uv run` to execute the script in the uv environment # Redirect stdin, stdout to PIPE for communication # Redirect stderr to DEVNULL to suppress error messages process = subprocess.Popen( ["uv", "run", "weather.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=stderr) # Behold! We create life! process = subprocess.Popen( ["uv", "run", "weather.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL # Silence the screams ) Le Le module remplace les flux I/O de l'enfant par des tuyaux, vous donnant un contrôle complet sur la conversation.C'est magnifiquement simple: vous avez maintenant un processus d'enfant dédié à votre beck-and-call, prêt à exécuter tout ce que vous y jetez. subprocess Vous pouvez même envoyer des signaux pour vraiment montrer qui est le patron. Dans des conditions normales, lorsque le processus parental quitte, l'enfant descend avec le navire - vous n'avez donc pas à vous soucier des processus zombies qui entravent votre système (bien, la plupart du temps). Vous remarquerez que nous utilisons l'exécution de la procédure. ce n'est pas nécessaire, mais est rapidement devenu la norme d'or pour les outils Python modernes.Si vous ne l'avez pas encore vérifié, vous devriez absolument - c'est un changement de jeu. Quick aside uv uv Si vous utilisez encore pip, nous devons parler. Si vous utilisez toujours Il faut que nous parlions. pip Étape 2 : Le premier rendez-vous Chaque bonne relation commence par l’identification mutuelle.Nous allons commencer par nous annoncer (comme un gentleman): # Define the initialize request init_request = { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": { "name": "MyMCPClient", "version": "1.0.0" } } } C’est notre Il dit au serveur trois choses cruciales : nous parlons de MCP, quelle version nous utilisons, et quelles capacités nous apportons à la table. Ouverture sauvage Vous remarquerez que cela suit le format JSON-RPC 2.0, et que cela sera cohérent tout au long de notre conversation avec le serveur. Let's fire it off. import json def send_mcp_request(process, request): """Sends a JSON-RPC request to the subprocess's stdin.""" json_request = json.dumps(request) + '\n' # Add newline delimiter process.stdin.write(json_request.encode('utf-8')) process.stdin.flush() # Ensure the data is sent immediately # 1. Send the initialize request print("Sending initialize request...") send_mcp_request(process, init_request) Nous devrions voir ce résultat : Sending initialize request... Maintenant, écoutons ce que le serveur a à dire sur tout jusqu'à présent: def read_mcp_response(process): """Reads a JSON-RPC response from the subprocess's stdout.""" # Assuming the server sends one JSON object per line line = process.stdout.readline().decode('utf-8') if line: print(" . . . len is", len(line)) return json.loads(line) return None print("Sending initialized request...") send_mcp_request(process, notified_request) Le serveur, étant polie, se présente à nouveau : . . . len is 266 Received response after initialization:{'id': 1, 'jsonrpc': '2.0', 'result': {'capabilities': {'experimental': {}, 'prompts': {'listChanged': False}, 'resources': {'listChanged': False, 'subscribe': False}, 'tools': {'listChanged': False}}, 'protocolVersion': '2025-03-26', 'serverInfo': {'name': 'weather', 'version': '1.9.4'}}} "Je suis un serveur météorologique, je ne changerai pas de façon aléatoire mes capacités au milieu de la conversation, et je ne vous fantasmerai certainement pas." Translation Il partage les informations de version du protocole et les détails de base, mais La section est le vrai prix. So, what's the server actually telling us here? capabilities Nous ignorons les capacités de cette démo, mais vérifiez ceci: nous Abonnez-vous aux événements "listChanged" si notre serveur était le type d'ajouter ou de supprimer des outils de manière dynamique.Il y a en fait un petit système de pub / sous caché dans le protocole MCP - vous pouvez écouter toutes sortes d'événements.Notre serveur weather.py est trop simple pour n'importe quelle chose fantastique, mais il est là si vous en avez besoin. pouvait Il en va de même avec les « précipitations » et les « ressources » – nous les sautons complètement. Toute l'idée est que les différents systèmes gèrent différentes préoccupations, de sorte que vous n'avez pas à réinventer chaque roue. Vous pouvez choisir et choisir les parties du protocole à mettre en œuvre, mais si vous voulez jouer avec d'autres outils MCP, vous devriez mieux vous tenir à la spécification. API separation Alright, we're connected and ready to rock, right? Wrong. Le serveur est toujours assis là, frappant son pied numérique, en attendant que nous puissions terminer le coup de main.Nous devons envoyer le signal "toutes les bonnes choses à notre fin": notified_request = { "jsonrpc": "2.0", "method": "notifications/initialized" } C’est la façon dont JSON-RPC dit « feu et oublie » – aucune réponse attendue ou requise. Contrairement à la danse de demande/réponse habituelle que nous faisons, c’est une Pensez-y comme envoyant un paquet ACK : « Hey serveur, je suis prêt à rouler ! » Notice the missing id notification Aucun identifiant signifie: «Ne répondez pas, je crois que vous saurez quoi faire avec ces informations.» Aucun identifiant signifie: «Ne répondez pas, je crois que vous saurez quoi faire avec ces informations.» En attendant... de retour au ranch... (retour à l’histoire!) En attendant... de retour au ranch... (retour à l’histoire!) Rappelez-vous que le serveur, étant raisonnablement courtois, a déjà répondu avec un manifeste de ses capacités. Alors confirmons : Oui, nous sommes vraiment prêts pour la fête. # yes we are indeed ready to party # Acknowledge the server so it knows we approve print("// Sending initialized request...") send_mcp_request(process, notified_request) Maintenant, le serveur sait commencer à attendre les demandes. Étape 3 : « Montrez-moi ce que vous avez » Le temps de voir quels jouets ce serveur a apporté au terrain de jeu: tools_list_request = { "jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": { } } # 2. Send the tools/list request print("// Sending tools/list request...") send_mcp_request(process, tools_list_request) Nous obtenons exactement le résultat que nous attendions... Parfait! // Sending tools/list request... Maintenant, revenons à la sortie... Il est temps de voir quels trésors nous avons découverts: # Read the server's response to the tools/list request tools_list_response = read_mcp_response(process) print("// Received tools list response:", end='') pprint(tools_list_response) Et notre serveur affiche fièrement ses marchandises : prévisions, alertes et autres dégâts météorologiques. . . . len is 732 // Received tools list response:{'id': 2, 'jsonrpc': '2.0', 'result': {'tools': [{'description': 'Get weather alerts for a US state.\n' '\n' ' Args:\n' ' state: Two-letter US state code ' '(e.g. CA, NY)\n' ' ', 'inputSchema': {'properties': {'state': {'title': 'State', 'type': 'string'}}, 'required': ['state'], 'title': 'get_alertsArguments', 'type': 'object'}, 'name': 'get_alerts'}, {'description': 'Get weather forecast for a location.\n' '\n' ' Args:\n' ' latitude: Latitude of the ' 'location\n' ' longitude: Longitude of the ' 'location\n' ' ', 'inputSchema': {'properties': {'latitude': {'title': 'Latitude', 'type': 'number'}, 'longitude': {'title': 'Longitude', 'type': 'number'}}, 'required': ['latitude', 'longitude'], 'title': 'get_forecastArguments', 'type': 'object'}, 'name': 'get_forecast'}]}} Whoa ! c’est un JSON ! bon, allons creuser dans les bonnes choses. lot C'est notre mine d'or - chaque objet représente un outil que le LLM peut invoquer. (Plot twist: nous pouvons aussi les appeler, ce qui est exactement ce que nous faisons en ce moment!) See that tools Fait amusant: Les champs de «description» sont comment votre LLM décide quelle fonction appeler. pensez-y comme Tinder pour les outils, avec votre AI regardant son téléphone en essayant de décider si vous devez glisser à gauche ou à droite Fait amusant: Les champs de «description» sont comment votre LLM décide quelle fonction appeler. pensez-y comme Tinder pour les outils, avec votre AI regardant son téléphone en essayant de décider si vous devez glisser à gauche ou à droite : OpenAI a initialement essayé de marquer cela comme "appel de fonction" - ce qui est... Mais quelque part sur le chemin, l'industrie a décidé collectivement que les "outils" sonnaient plus cool (ou peut-être plus accessibles?), et maintenant nous appelons toute la danse "appel à outils". One interesting side note Techniquement Anatomy of a tool (pay attention, this is where the magic lives): nom : le nom réel de la fonction – pas de typographie autorisée ici !C’est le nom définitif, canonique de l’outil ; nous l’utiliserons pour faire référence à cet outil lorsque nous l’appellerons. Description: Anglais simple pour le LLM à lire (c'est littéralement ce que l'IA regarde lors de la décision d'utiliser votre outil) inputSchema: Schema JSON définissant les paramètres dont vous avez besoin outputSchema: Conspicuously missing! Tout retourne juste une "grande chaîne" et espère le meilleur Le nom Description Introduction outil Parce que nous retournons tous essentiellement JSON enveloppé en chaînes et faisant semblant d'être une décision de conception. Les fonctions traditionnelles peuvent retourner n'importe quel type, tandis que les outils de ligne de commande Unix (ce que le nom implique quelque peu) ne font que jeter du texte. Certains modèles ont même des commutateurs pour forcer la sortie JSON, donc en théorie, votre LLM pourrait s'attendre à des données structurées à chaque fois. Ensuite, les outils pourraient retourner du texte simple, CSV, HTML, ou vraiment quoi que ce soit. OK, nous avons notre boîte à outils chargée. Faisons notre premier appel à outils MCP! Étape 4 : Le moment de vérité Quoi qu'il en soit, nous avons une liste d'outils, maintenant quoi? Nous choisissons un outil. pour garder les choses simples puisque nous n'avons pas besoin de latitude et de longitude. Ce serait un meilleur choix. get_alerts get_forecast tools_call_request = { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_alerts", "arguments": { "state": "TX" } } } J’ai choisi le Texas parce que tout y est plus grand, y compris les catastrophes météorologiques. Attendez, attendez – pourquoi c’est « outils / appel » et non « outil / appel » ? une Ok, certainement, "outil / appel" sonnerait plus naturel en anglais, mais apparemment, la cohérence avec les autres endpoints gagne. Avec tous nos canards de données dans une rangée maintenant, nous pouvons appuyer sur le Big Red Button. # 3. Send the tools/call request print("// Sending tools/call request...") send_mcp_request(process, tools_call_request) # Read the server's response to the tools/call request tools_call_response = read_mcp_response(process) print("// Received tools call response:", end='') pprint(tools_call_response) [BEEP BEEP BOOP BOOP] (C’est le son que fait le grand bouton rouge) ] [Drumroll please La pensée du serveur... le traitement... et... ] [And the crowd goes wild! Voilà! Les données météorologiques réelles se matérialisent. Alertes, inondations, tornades, les travaux. Tout enveloppé dans un JSON structuré, comme votre thérapeute l'a commandé: (tremblé, personne ne veut voir 11 pages de déchets JSON) // Sending tools/call request... . . . len is 51305 // Received tools call response:{'id': 3, 'jsonrpc': '2.0', 'result': {'content': [{'text': '\n' 'Event: Flood Advisory\n' 'Area: Hidalgo, TX; Willacy, TX\n' 'Severity: Minor\n' 'Description: * WHAT...Flooding caused by ' 'excessive rainfall continues.\n' '\n' '* WHERE...A portion of Deep South Texas, ' 'including the following\n' 'counties, Hidalgo and Willacy.\n' '\n' '* WHEN...Until 245 PM CDT.\n' '\n' '* IMPACTS...Minor flooding in low-lying and ' 'poor drainage areas.\n' '\n' '* ADDITIONAL DETAILS...\n' '- At 205 PM CDT, Doppler radar indicated ' 'heavy rain due to\n' 'thunderstorms. Minor flooding is ongoing or ' 'expected to begin\n' 'shortly in the advisory area. Between 2 and ' '5 inches of rain\n' 'have fallen.\n' '- Additional rainfall amounts up to 1 inch ' 'are expected over\n' 'the area. This additional rain will result ' 'in minor flooding.\n' '- Some locations that will experience ' 'flooding include...\n' 'Harlingen, Elsa, Edcouch, La Villa, Lasara, ' 'La Villa High\n' 'School, Monte Alto, Jose Borrego Middle ' 'School, Satiago\n' 'Garcia Elementary School, Edcouch Police ' 'Department, Edcouch\n' 'City Hall, Edcouch Volunteer Fire ' 'Department, Edcouch-Elsa\n' 'High School, Laguna Seca, Carlos Truan ' 'Junior High School,\n' 'Elsa Police Department, Lyndon B Johnson ' 'Elementary School,\n' 'Elsa Public Library, Olivarez and Lasara ' 'Elementary School.\n' '- http://www.weather.gov/safety/flood\n' "Instructions: Turn around, don't drown when " 'encountering flooded roads. Most flood\n' '\n' 'The next statement will be issued Tuesday ' 'morning at 830 AM CDT.\n', 'type': 'text'}], 'isError': False}} Notez que Vérifiez toujours ce champ, sauf si vous aimez déboguer des échecs mystérieux à 3 heures du matin. Victory! isError: false Nous recevons des données de chaîne propres (pas de streaming pour compliquer les choses), ce qui nous donne des options. Nous pourrions analyser ces données météorologiques et les masser pour le LLM, ou simplement passer la réponse brute et laisser le modèle le comprendre. Mais si vous construisez quelque chose de sophistiqué, le pré-traitement de la sortie de l'outil peut être incroyablement puissant. Vous pouvez le formater, le filtrer, le combiner avec d'autres sources de données ou le transformer en exactement ce dont votre application a besoin. Nous avons enregistré avec succès un serveur MCP, initialisé la connexion, appelé un outil et traité les résultats. And that's a wrap! Le Monty complet : votre client MCP complet Voici le glorieux rituel en un seul cercle : import subprocess import json from pprint import pprint def send_mcp_request(process, request): json_request = json.dumps(request) + '\n' process.stdin.write(json_request.encode('utf-8')) process.stdin.flush() def read_mcp_response(process): line = process.stdout.readline().decode('utf-8') return json.loads(line) if line else None requests = { 'init': { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-03-26", "capabilities": {}, "clientInfo": {"name": "MyMCPClient", "version": "1.0.0"} } }, 'initialized': { "jsonrpc": "2.0", "method": "notifications/initialized" }, 'list_tools': { "jsonrpc": "2.0", "id": 2, "method": "tools/list" }, 'call_tool': { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "get_alerts", "arguments": {"state": "TX"}} } } process = subprocess.Popen( ["uv", "run", "weather.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL ) try: send_mcp_request(process, requests['init']) pprint(read_mcp_response(process)) send_mcp_request(process, requests['initialized']) send_mcp_request(process, requests['list_tools']) tools = read_mcp_response(process) print("Available tools:", [t['name'] for t in tools['result']['tools']]) send_mcp_request(process, requests['call_tool']) result = read_mcp_response(process) print("Weather alert received:", len(result['result']['content'][0]['text']), "characters") finally: process.terminate() La grande révélation You just built an MCP client using nothing but Python's standard library. Aucun cadre. Aucune dépendance extérieure. Aucune magie. Juste des pipes de sous-processeurs et du JSON – les mêmes outils que vous avez eu depuis Python 2.7. Est-ce que c’est « production-ready » ? Honnêtement? Pas tout à fait – mais c’est étonnamment proche. Qu’il s’agisse de Claude qui parle à votre base de données, de GPT-4 qui appelle vos API, ou de la « plate-forme de workflow d’IA révolutionnaire » de certaines start-ups – en dessous de tout cela, c’est juste cela : Spawn process. Send JSON. Read JSON. Repeat. C'est comme découvrir le sorcier d'Oz, c'est juste un gars avec un très bon système sonore. C'est comme découvrir le sorcier d'Oz, c'est juste un gars avec un très bon système sonore. Les prochains pas vers la folie Maintenant que vous avez vu le ventre de la bête (en code), vous pouvez: Créez des serveurs MCP personnalisés qui effectuent votre offre (pas plus d’attente pour que quelqu’un d’autre écrive l’intégration) Déboguer les connexions MCP comme un nécromancier de réseau lorsqu'elles se brisent inévitablement au pire moment possible Concevez de meilleurs outils en sachant exactement comment les LLM consomment vos métadonnées Écrivez de meilleurs outils si irrésistiblement décrits que vos LLM tombent amoureux Optimisez l'enfer de tout parce que vous comprenez le protocole Ou simplement automatiser votre alimentateur de chat. je ne juge pas. La belle révélation The miracle in step 2 of your business plan? It was you, all along. Le miracle dans l'étape 2 de votre plan d'affaires? c'était vous, tout le long. Construisez quelque chose d’étrange. Voulez-vous voir ce code en action? L'exemple complet vit ici: [https://gitlab.com/-/snippets/4864350] https://gitlab.com/-/snippets/4864350 https://gitlab.com/-/snippets/4864350?embedable=true