Todo el mundo está diseccionando MCP como si fuera la piedra de Rosetta de la IA. Diagramas! Whitepapers! Pero dónde, mis queridos druidas de datos, están los paquetes reales? nobody's showing the actual packets. Es como intentar aprender la cirugía de un cartel motivacional.Sí, el marco teórico es *inspirador*, pero quiero ver la sangre y los intestinos! ¿Dónde está el JSON crudo? ¿Dónde están los desechos de stdin/stdout? ¿Cómo debería animar mi creación cuando ni siquiera puedo ver el rayo? Es como intentar aprender la cirugía de un cartel motivacional.Sí, el marco teórico es *inspirador*, pero quiero ver la sangre y los intestinos! ¿Dónde está el JSON crudo? ¿Dónde están los desechos de stdin/stdout? ¿Cómo debería animar mi creación cuando ni siquiera puedo ver el rayo? Tratar de aprender un protocolo sin descargar cables es como tratar de aprender a cerrar desde un PowerPoint. Give me the electrons or give me death. Ahora que tenemos nuestro mantra, vamos a cavar! ¿Es este su modelo de negocio? Su Plan de Negocios MCP: Aprende el MCP ¿Cuáles son los milagros que ocurren?... El beneficio Suena familiar? Por supuesto que sí! Es el equivalente de la startup de convertir el plomo en oro. Alquimia, pero con mucho más JSON. Afortunadamente... # ¡No estás solo! Parecería que se han derramado filas de tinta sobre este llamado Protocolo de Contexto Modelo. Todo el mundo está hablando de LLMs evolucionando, llamando herramientas, llamando funciones, bidireccional esto y la capacidad que. yo, por supuesto, no soy la excepción. pero una cosa siguió clavando en mi tronco cerebral mientras caminaba por este alegre camino de la madera de la teoría y la abstracción: "What's this look like ON THE WIRE???" Todo el protocolo está documentado en Pero no quiero filosofía, quiero Quiero sentirme como el Dr. Frankenstein tendido a mi golem, rayo y todo. no vagando a través de otro grano semántico de obscura gramática de sintaxis con sabor a BNF. modelocontextoprotocolo.io see ¡Ay de mí, un mero residente entre los que habrían de ser iluminados, hasta que lo rompí abriéndome así! Primeras observaciones desde el campo El MCP es muy, muy nuevo. Borda de sangrado. Posiblemente todavía sangrando. Tal vez un día, se escriban libros enteros sobre su política interna, como una especie de convención constitucional digital. Pero por ahora, una cosa se destaca sobre todo: Calling Tool es todo lo que necesitas Si hacemos el zoom, hacemos el zoom en este caso de uso único y crítico. Podemos ignorar a Podemos simplificar hasta que miramos el hermoso esqueleto del sistema: sólo cuatro tipos de mensajes para la salida, y tres para la entrada. (Explicaremos por qué los números no coinciden exactamente un poco más tarde.) tool invocation Loto Iniciación Iniciación Herramientas / Lista Herramientas / llamadas (y los 3 resultados que vuelven) Puedes comprender todo el protocolo conociendo estos siete mensajes. El tiempo (de ) modelocontextoprotocolo.io modelocontextoprotocolo.io Nosotros utilizaremos — un ejemplo de servidor MCP simple que le permite consultar los datos de predicción y alerta. Puede encontrarlo en su GitHub aquí: [ weather.py https://github.com/modelcontextprotocol/quickstart-resources/blob/main/weather-server-python/weather.py Hablemos de lo que realmente sucede "en el cable" cuando hablas con esta cosa. En primer lugar, el MCP es Eso significa que la especificación no le importa cómo se conecte al servidor. HTTPS? Sockets Web? Pipes nombrados? Tapping en un antiguo cable de vampiros Ethernet? No importa. El transporte agnóstico. En la práctica, el método más común es lanzar el servidor como un subproceso y comunicarse y Esto le da un buen pequeño canal de comunicación privado. (Técnicamente full-duplex, pero si añade , lo llamaremos 1.5-duplex para ser dulce). stdin stdout stderr Cómo fluyen los mensajes MCP utiliza JSON-RPC 2.0 sobre el cable. Esto le da un protocolo de solicitud/respuesta, más notificaciones. Cada mensaje se envía como uno por línea de JSON. Es como un servicio de telegrama digital donde cada línea es un paquete completo de significado. ¿Es esto en la especificación? Algo de; no realmente. La especificación la deja abierta. Pero este JSON delimitado por novedades es el idioma predeterminado. El mito de "stdin / stdout considerado dañino" Sí, es posible que encuentres algunas publicaciones dramáticas en los blogs que afirman que este patrón es "peligroso" o "inefiable". ¡No queremos comenzar un incendio en el laboratorio, ¿no? ¿por qué la gente diría esto si no fuera verdad? Usted ve, en Ye Olden Times, líneas extra largas podrían causar sobrecargas de buffer (hijo, Morris Worm). Afortunadamente, la mayoría de los software modernos no sufren de este problema (tanto! los dedos cruzados!). Los analizadores JSON modernos son rápidos y resilientes. ¿Quieres enviar una alerta meteorológica de 256 MB? Vaya por ello. Estas advertencias generalmente vienen de personas que manejan manualmente los descriptores de archivos en C. Usted no está haciendo eso. Usted está usando Python. Un módulo diseñado específicamente para este tipo de tareas. 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 ) Fácil, ¿verdad? Todo Python nativo. No hay paquetes adicionales. El El módulo reemplaza los flujos I/O del niño con tubos, dándole dominio completo. También podemos enviar nuestras nuevas señales de proceso si lo deseamos. subprocess Ahora, estamos finalmente listos para hacer la ciencia del protocolo como una proper digital necromancer! Título original: It's Embarrassingly Simple MCP, en todas sus regalias de empresa-robe-and-scepter, es sólo esto: 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." Todo este protocolo es sólo una forma muy formal de pedir a alguien que haga una cosa y obtener la confirmación de que lo hicieron. El resto es solo una ceremonia de clase empresarial y una cola brillante en torno a este abrazo hermosamente simple. Siete buenos intercambios. Eso es eso. Pintando el monstruo juntos: un prototipo de trabajo Vamos a engendrar un servidor meteorológico y interrogarlo como un científico loco. Consejo #1: La mejor manera de entender un protocolo es abusar de él hasta que grite, luego patcharlo hasta que coopere. Consejo #1: La mejor manera de entender un protocolo es abusar de él hasta que grite, luego patcharlo hasta que coopere. Consejo #2: Si torturas tus LLMs, no seré responsable de lo que te suceda en The After Times When AI Taketh Over! Consejo #2: Si torturas tus LLMs, no seré responsable de lo que te suceda en The After Times When AI Taketh Over! Paso 1: Convocar a nuestro Minion Digital # 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 ) El El módulo reemplaza los flujos de I/O del niño con tubos, lo que le da un control total sobre la conversación.Es hermosamente simple: ahora tiene un proceso dedicado para el niño en su beck and call, listo para ejecutar lo que usted arroje en él. subprocess Usted puede incluso enviar señales para realmente mostrar quién es el jefe. En condiciones normales, cuando el proceso parental sale, el niño desciende con el barco, por lo que no tiene que preocuparse por los procesos zombies que agitan su sistema (bien, en su mayoría). Usted notará que estamos utilizando para el proceso. no es necesario, pero está convirtiéndose rápidamente en el estándar de oro para las herramientas de Python modernas.Si aún no lo has comprobado, absolutamente deberías - es un cambiador de juego. Quick aside uv uv Si todavía estás usando pip, necesitamos hablar. Si todavía estás usando pip, necesitamos hablar. Paso 2: La primera cita desagradable Cada buena relación comienza con la identificación mutua. Comenzaremos anunciándonos (como un caballero): # 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" } } } Este es nuestro El primer paquete que enviamos para establecer la comunicación le dice al servidor tres cosas cruciales: hablamos de MCP, qué versión estamos usando y qué capacidades traemos a la mesa. Inauguración Salvo Usted notará que esto sigue el formato JSON-RPC 2.0, y que va a ser consistente a lo largo de toda nuestra conversación con el servidor. 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) Debemos ver este resultado: Sending initialize request... Ahora, escuchemos lo que el servidor tiene que decir sobre todo hasta ahora: 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) El servidor, siendo educado, se introduce de nuevo: . . . 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'}}} "¡Hola! soy un servidor meteorológico, no voy a cambiar aleatoriamente mis habilidades en medio de la conversación, y definitivamente no te voy a fantasmar." Translation Es compartir la versión del protocolo información y detalles básicos, pero el La sección es el premio real. So, what's the server actually telling us here? capabilities Estamos ignorando las capacidades para esta demostración, pero comprueba esto: nosotros suscribirse a los eventos "listChanged" si nuestro servidor era el tipo para agregar o eliminar herramientas de forma dinámica.Hay realmente un pequeño sistema de pub / sub que se esconde en el protocolo MCP -puedes escuchar todo tipo de eventos.Nuestro servidor weather.py es demasiado simple para cualquier cosa fantástica, pero está ahí si lo necesitas. Podría El mismo trato con "prompts" y "recursos" - los estamos salpicando por completo.Sí, podríamos rodar nuestra propia implementación, pero eso pierde el punto de vista de La idea entera es que diferentes sistemas manejan diferentes preocupaciones, por lo que no tiene que reinventar cada rueda.Puede elegir y elegir qué partes del protocolo para implementar, pero si desea jugar bien con otras herramientas de MCP, es mejor adherirse a la especificación. API separation Alright, we're connected and ready to rock, right? Wrong. El servidor todavía está sentado allí, tocando su pie digital, esperando a que completemos el abrazo. notified_request = { "jsonrpc": "2.0", "method": "notifications/initialized" } campo? Esa es la manera de JSON-RPC de decir "fuego y olvídate" —no hay respuesta esperada o requerida. A diferencia de la danza de solicitud/respuesta habitual que hemos estado haciendo, esta es una Piense en esto como enviando un paquete ACK: "¡Hey servidor, estoy listo para rodar!" Notice the missing id notification No id significa: "No responda, confiaré en que sepa qué hacer con esta información." significa: "No responda, confiaré en que sepa qué hacer con esta información." No id Mientras tanto... de vuelta en el rancho... (de vuelta a la historia!) Mientras tanto... de vuelta en el rancho... (de vuelta a la historia!) Recuerde que el servidor, siendo razonablemente cortés, ya respondió con un manifiesto de sus capacidades. Así que vamos a confirmar: De hecho, estamos listos para la fiesta. # 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) Ahora, el servidor sabe comenzar a esperar solicitudes. Paso 3: “Show Me What You Got” Tiempo para ver qué juguetes este servidor trajo al campo de juego: 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) El resultado es exactamente el que esperábamos... ¡perfecto! // Sending tools/list request... Ahora, volvamos a leer el resultado... Tiempo para ver qué tesoros hemos descubierto: # 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) Y nuestro servidor muestra orgullosamente sus mercancías: pronósticos, alertas y otros males meteorológicos. . . . 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'}]}} ¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡¡ ¡Sí, JSON! vamos a cavar en las cosas buenas. Loto Eso es nuestra mina de oro - cada objeto representa una herramienta que el LLM puede invocar. (Torsión de la trama: también podemos llamarlos, que es exactamente lo que estamos haciendo ahora!) See that tools Hecho divertido: Los campos de ‘descripción’ son cómo su LLM decide qué función llamar.Piensa en ello como Tinder para herramientas, con tu IA mirando a su teléfono tratando de decidir si desplazarse a la izquierda o a la derecha. Hecho divertido: Los campos de ‘descripción’ son cómo su LLM decide qué función llamar.Piensa en ello como Tinder para herramientas, con tu IA mirando a su teléfono tratando de decidir si desplazarse a la izquierda o a la derecha. OpenAI originalmente trató de marcar esto como "calling de funciones" - que es... Pero en algún lugar a lo largo del camino, la industria decidió colectivamente que "herramientas" sonaban más frescas (o quizás más accesibles?), y ahora llamamos a toda la danza "llamada de herramientas". One interesting side note Técnicamente Anatomy of a tool (pay attention, this is where the magic lives): nombre: el nombre real de la función —no se permite escribir aquí! Este es el nombre definitivo, canónico de la herramienta; lo usaremos para referirse a esta herramienta cuando la llamemos. Descripción: Inglés simple para el LLM para leer (esto es literalmente lo que la IA mira al decidir si usar su herramienta) InputSchema: Esquema de JSON que define qué parámetros necesita Schema: Conspicuamente desaparecido! Todo sólo devuelve una "grande cadena" y espera lo mejor Nombre Descripción INTRODUCCIÓN Desarrollo ¿Por qué no hay esquema de salida? Porque todos basicamente devuelven JSON envuelto en cuerdas y fingimos que es una decisión de diseño. Las funciones tradicionales pueden devolver cualquier tipo, mientras que las herramientas de línea de comandos de Unix (lo que el nombre implica de alguna manera) simplemente salpan texto. Algunos modelos incluso tienen interruptores para forzar la salida de JSON, por lo que en teoría, su LLM podría esperar datos estructurados cada vez.Entonces, las herramientas podrían devolver texto simple, CSV, HTML, o realmente cualquier cosa.Los modelos multi-modales podrían incluso devolver audio, imágenes, vídeo o feeds de detección de objetos en vivo - las posibilidades son maravillosamente caóticas. Bueno, tenemos nuestra caja de herramientas cargada. ¡Vamos a hacer nuestra primera llamada de herramientas de MCP! Paso 4: El momento de la verdad De todos modos, tenemos una lista de herramientas, ahora qué? ¡chamemos una! Seleccionamos una herramienta. para mantener las cosas sencillas ya que no necesitamos latitud y longitud. Sería una mejor opción. get_alerts get_forecast tools_call_request = { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "get_alerts", "arguments": { "state": "TX" } } } He elegido Texas porque todo es más grande allí, incluidos los desastres meteorológicos. Espere, deténgase - ¿por qué es "herramienta / llamada" y no "herramienta / llamada"? Una Bueno, seguro, "herramienta / llamada" suena más natural en inglés, pero aparentemente, la coherencia con los otros endpoints gana. Con todos nuestros piojos de datos en una fila ahora, podemos presionar el botón Big Red. # 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] (Este es el sonido que hace el Big Red Button) ] [Drumroll please El pensamiento del servidor... procesamiento... y... y... ] [And the crowd goes wild! ¡Voiilà! Datos meteorológicos reales se materializan. Alertas, inundaciones, tornados, los trabajos. Todo envuelto en JSON estructurado, al igual que su terapeuta ordenó: (cortado, nadie quiere ver 11 páginas de desechos de 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}} Observa que -Siempre compruebe este campo a menos que le guste debugar fallos misteriosos a las 3 de la mañana. Victory! isError: false ¡Lo veo bien! Estamos recibiendo datos de cadena limpia de vuelta (no streaming para complicar las cosas), lo que nos da opciones. podríamos analizar estos datos meteorológicos y masarlo para el LLM, o simplemente pasar la respuesta cruda y dejar que el modelo lo descubra.La mayoría de las implementaciones van con este último enfoque - ¿por qué hacer trabajo extra cuando los LLM son bastante buenos en analizar texto estructurado? Pero si está construyendo algo sofisticado, el procesamiento previo de la salida de la herramienta puede ser increíblemente poderoso.Puede formatearlo, filtrarlo, combinarlo con otras fuentes de datos o transformarlo en exactamente lo que su aplicación necesita. Hemos registrado con éxito un servidor MCP, inicializado la conexión, llamado una herramienta y procesado los resultados. And that's a wrap! The Full Monty: Tu cliente MCP completo Aquí está todo el glorioso ritual en un círculo llamativo: 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 gran revelación You just built an MCP client using nothing but Python's standard library. No hay frameworks. No hay dependencias externas. No hay magia. Solo los tubos de subproceso y JSON —las mismas herramientas que has tenido desde Python 2.7. ¿está esto “listo para la producción”? ¿Honestamente? ¿No muy— pero está sorprendentemente cerca. A veces, las soluciones más simples son las más prohibidas. Ya sea que sea Claude hablando con su base de datos, GPT-4 llamando sus APIs, o la "plataforma revolucionaria de flujo de trabajo de IA" de algunas startups, debajo de todo, es sólo esto: Spawn process. Send JSON. Read JSON. Repeat. Es como descubrir que el mago de Oz es sólo un tipo con un sistema de sonido realmente bueno. Es como descubrir que el mago de Oz es sólo un tipo con un sistema de sonido realmente bueno. Los próximos pasos hacia la locura Ahora que has visto el vientre de la bestia (en código), puedes: Construye servidores MCP personalizados que realicen su licitación (no más esperar a que otra persona escriba la integración) Debugue las conexiones MCP como un necromancer de red cuando inevitablemente se rompen en el peor momento posible Diseñar mejores herramientas sabiendo exactamente cómo los LLM consumen sus metadatos Escribe mejores herramientas tan irresistiblemente descritas que tus LLM se enamoren Optimice el infierno de todo porque entiende el protocolo sobre la cabeza O simplemente automatizar su alimentador de gato. no lo juzgo. La hermosa revelación The miracle in step 2 of your business plan? It was you, all along. El milagro en el paso 2 de su plan de negocio? Construye algo extraño. ¿Quieres ver este código en acción? El ejemplo completo vive aquí: [https://gitlab.com/-/snippets/4864350] https://gitlab.com/-/snippets/4864350 https://gitlab.com/-/snippets/4864350?embedable=true