1. MCP y JSON‑RPC: el «aburrido» fundamento que hay que entender una vez
En la lección anterior hablamos de para qué sirve MCP y cómo encaja en el stack de Apps SDK. En esta lección reducimos el foco a la capa más «aburrida»: el formato de los mensajes MCP, para que puedas leer con confianza los logs JSON en bruto y entender qué envía exactamente ChatGPT a tu servidor y qué responde este.
MCP utiliza JSON‑RPC 2.0 como transporte de datos: todas las solicitudes, respuestas y notificaciones son objetos JSON normales con un esquema predecible.
Es decir, en lugar de «cada servicio inventa su propio formato», hay un contrato básico:
- una solicitud tiene el campo obligatorio jsonrpc (normalmente "2.0"), un id único, un nombre de método en forma de cadena method y un objeto params con los parámetros;
- la respuesta se vincula con la solicitud por id y contiene result o error;
- las notificaciones (notifications) se parecen a las solicitudes, pero sin id, y no habrá respuesta para ellas.
Tiene un aspecto parecido a:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/list",
"params": {
"cursor": null
}
}
Esto es un request. Y la respuesta en caso de éxito:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"tools": [],
"nextCursor": null
}
}
Si te ha venido a la cabeza «esto es un RPC normal», así es. MCP simplemente fija qué métodos existen (tools/list, tools/call, resources/list, prompts/list, …) y en qué formato esperan parámetros y devuelven datos.
Es importante captar: JSON‑RPC es el armazón «petición–respuesta–notificación». MCP es «qué peticiones exactas existen y qué llevan dentro».
2. Request: cómo MCP pide hacer algo
Empecemos por las solicitudes. Siempre van en la dirección de «alguien quiere hacer algo». Normalmente es cliente → servidor (ChatGPT → tu servidor MCP), pero MCP también permite solicitudes en sentido contrario, cuando el servidor pide al cliente hacer sampling o elicitation. En esta lección nos interesa sobre todo la variante clásica: el cliente le pide al servidor.
Todo MCP‑request tiene tres campos clave:
- jsonrpc: la versión del protocolo JSON‑RPC, normalmente "2.0".
- id: el identificador de la solicitud; cualquier tipo JSON, pero en la práctica suele ser un número o una cadena. Lo principal es que los id sean únicos para las solicitudes activas.
- method: una cadena del tipo "tools/list" o "tools/call". MCP especifica el conjunto de métodos permitidos.
Y hay un objeto params, donde viven los parámetros del método concreto.
Ejemplo: solicitud de la lista de herramientas
Imagina que ChatGPT acaba de conectarse a tu servidor MCP y quiere saber qué tools puede invocar. Enviará una solicitud más o menos así:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": null
}
}
El campo cursor es para la paginación: si hay muchas herramientas, el servidor puede devolverlas por lotes.
Para nuestra aplicación de práctica (selección de regalos) aquí todo será de momento aburrido: una o dos herramientas, pero el protocolo sigue siendo el mismo. Por ahora tómalo como un ejemplo intuitivo; veremos las estructuras formales más adelante en la sección sobre tools.
Ejemplo: invocación de una herramienta (tools/call)
Ahora algo más interesante. Supongamos que ya tenemos un MCP‑tool suggest_gifts, que planeas implementar en la lección sobre el servidor MCP. Espera parámetros:
- occasion: la ocasión (Birthday, Wedding, …),
- budget: un número en dólares,
- recipient: una cadena con la descripción de a quién va dirigido.
ChatGPT, al decidir usar esta herramienta, formará un MCP‑request:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "birthday",
"budget": 100,
"recipient": "friend who loves board games"
}
}
}
Fíjate en varios detalles.
Primero, el nombre de la herramienta se toma de lo que declaraste en el lado del servidor (server.registerTool("suggest_gifts", …)). Segundo, el objeto arguments debe ajustarse al JSON Schema que adjuntes en la descripción de la herramienta.
Si GPT intenta enviar argumentos que no cumplen el esquema (por ejemplo, budget: "cien dólares"), el servidor puede devolver un error a nivel de protocolo o de lógica de negocio según la implementación. Por ahora lo importante es captar la forma general de esta solicitud; en la sección sobre tools más abajo veremos estos mismos mensajes de manera más sistemática.
Requests para recursos y prompts
De forma análoga se ven las solicitudes a recursos y prompts. La especificación de MCP define los métodos:
- resources/list: enumerar los recursos disponibles;
- resources/read (o resources/get): leer un recurso concreto por URI;
- prompts/list: obtener la lista de prompts disponibles;
- prompts/get: obtener el texto de un prompt concreto.
Ejemplo de solicitud de lectura de un recurso con el catálogo de regalos:
{
"jsonrpc": "2.0",
"id": 15,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
Por ahora basta con recordar dos cosas. Primero, para cada primitivo hay métodos */list y */get/*/read. Segundo, el nombre del método siempre está en el campo de cadena method, y todo el contenido en el objeto params.
3. Reply: cómo responde MCP — result y error
La respuesta (reply) siempre está vinculada con la solicitud por el campo id. Es como el correlationId en muchos sistemas distribuidos: miras los logs y ves que la solicitud con id=7 recibió una respuesta con id=7; entonces es la misma pareja.
JSON‑RPC establece una regla simple: en la respuesta hay result o error, pero no ambos a la vez. Encima de esto, MCP precisa la estructura de result para distintos métodos (tools/list, tools/call, etc.) y recomienda códigos de error.
Respuesta correcta (result)
Veamos un ejemplo de respuesta correcta a tools/call de nuestro suggest_gifts. El servidor lo procesó todo, encontró regalos adecuados y devuelve la lista en el campo result:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "Here are some gift ideas for your friend..."
}
],
"structuredContent": {
"gifts": [
{ "name": "Board game: Catan", "price": 45 },
{ "name": "Dice set", "price": 20 }
]
},
"isError": false
}
}
Aquí hay varios puntos importantes.
- Primero, content y structuredContent son esas partes de la respuesta de MCP‑tools que ya has visto en Apps SDK. El modelo utiliza el texto de content, y tu widget renderiza cómodamente los datos de structuredContent.
- Segundo, la marca isError se refiere al resultado de negocio. Desde el punto de vista del protocolo todo fue bien: el JSON es válido, el método existe, los argumentos se han interpretado. Pero la lógica de negocio puede decir: «no encontré ninguna idea de regalo; desde el punto de vista del UX lo consideramos un error». Entonces pones isError: true y describes el problema en content.
- Tercero, la especificación de MCP para distintos métodos (tools/list, tools/call, */list, */get) describe en detalle qué campos deben estar en result. Por ejemplo, para tools/list el servidor devuelve una matriz de descripciones de herramientas con nombres, títulos, descripciones y el JSON Schema de los argumentos de entrada.
Respuesta con error (error)
Si algo sale mal a nivel del protocolo o del servidor, en lugar de result se devuelve un objeto error. Normalmente tiene:
- code: un código numérico de error;
- message: una descripción legible;
- data: datos adicionales opcionales (stack trace, detalles, …).
Ejemplo: el modelo invocó un método inexistente:
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32601,
"message": "Method not found: tools/col"
}
}
El código -32601 es el clásico de JSON‑RPC «method not found».
Hay una diferencia sutil pero importante entre dos tipos de errores.
Error de protocolo: cuando se violan las reglas de MCP/JSON‑RPC: método desconocido, tipo incorrecto en el campo params, JSON no válido. Entonces procede devolver error en el nivel superior.
Error de negocio: cuando el protocolo se respeta, pero la operación no se pudo realizar por un motivo de dominio: catálogo vacío, no hay permisos para un recurso concreto, identificador de negocio no válido. Entonces MCP suele recomendar devolver un result válido, pero marcarlo como isError: true y describir el problema en el contenido.
Esta separación ayuda mucho a ChatGPT y a las herramientas de depuración: mirando los logs, ves al instante si fue una avería técnica o un rechazo consciente de la lógica de negocio.
4. Notifications: mensajes unidireccionales
Una notificación (notification) es «una carta sin esperar respuesta». En JSON‑RPC las notificaciones se ven como solicitudes normales sin el campo id. El cliente no debe enviar una reply para ellas.
En MCP las notificaciones se usan para eventos: cambios en las listas de tools/resources/prompts, progreso de operaciones largas, mensajes de log, etc.
El ejemplo más simple que seguro encontrarás es la notificación de que la lista de herramientas ha cambiado. La especificación de MCP para tools describe la capability listChanged y la notificación tools/list_changed, que el servidor envía si el conjunto de tools disponibles ha cambiado.
La notificación puede verse así:
{
"jsonrpc": "2.0",
"method": "tools/list_changed",
"params": {
"reason": "New tool 'suggest_gift_cards' was added"
}
}
No se requiere respuesta. El cliente, al recibirla, puede decidir: «ajá, hay que volver a llamar a tools/list y actualizar la caché de herramientas».
Otras notificaciones típicas de MCP (hablaremos de ellas en detalle en el módulo sobre flujos y eventos):
- eventos de progreso (notifications/progress) para operaciones largas;
- logs del servidor (notifications/logging/message);
- cambios de recursos (resources/list_changed) y de prompts (prompts/list_changed).
Por ahora importa una cosa: notificación = solicitud sin id y sin respuesta esperada. Si ves en los logs un JSON sin id, lo más probable es que sea una notification.
Insight
Se ha comprobado de forma experimental que la ChatGPT App ignora los mensajes enviados a ella (MCP‑notification). Sin embargo, dado que ChatGPT Apps está aún en los inicios de su desarrollo, la probabilidad de que haya compatibilidad completa con todos los aspectos del protocolo MCP en un futuro próximo es muy alta. Así que recomiendo estudiar de todos modos este lado del protocolo MCP.
5. Cómo se ven tools/resources/prompts en los mensajes
Ahora lo más interesante: cómo exactamente se describen dentro de los mensajes MCP esos tools, resources y prompts de los que tanto hablamos.
Tools: descripción e invocación
A nivel de protocolo, los tools tienen dos procesos principales:
- discovery: el cliente averigua qué herramientas hay;
- invocation: el cliente invoca una herramienta concreta.
Ya hemos visto de pasada tools/list y tools/call arriba. Ahora los veremos de forma más sistemática: qué procesos cubren y qué se devuelve exactamente en result.
5.1.1. Lista de herramientas — tools/list
Ya vimos la solicitud para tools/list. Veamos la estructura de la respuesta. La especificación MCP dice: en result.tools debe devolverse una matriz de objetos, cada uno de los cuales describe una herramienta. Una herramienta debe tener:
- name: un nombre único por el que luego se invocará tools/call;
- title: un título corto (lo ven tanto la persona como el modelo);
- description: una descripción más extensa de lo que hace la tool, como si se lo explicaras a un colega;
- inputSchema: el JSON Schema para los argumentos de la herramienta.
Para nuestro suggest_gifts la respuesta de tools/list puede verse así (muy simplificado):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas for a given occasion and budget.",
"inputSchema": {
"type": "object",
"properties": {
"occasion": { "type": "string" },
"budget": { "type": "number" },
"recipient": { "type": "string" }
},
"required": ["occasion", "budget"]
}
}
],
"nextCursor": null
}
}
Si ya has escrito inputSchema en Apps SDK al registrar la herramienta, prácticamente has visto este objeto, solo que «por arriba», en forma de objeto de TypeScript. MCP simplemente lo transmite por el protocolo al cliente.
5.1.2. Invocación de la herramienta — tools/call
El formato de la invocación ya lo tocamos. La especificación de MCP describe que params debe contener:
- name: el nombre de la herramienta;
- arguments: un objeto que cumpla con inputSchema.
Por ejemplo:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "wedding",
"budget": 150,
"recipient": "coworker from marketing"
}
}
}
Y en la respuesta el servidor devuelve result con content, structuredContent y, opcionalmente, _meta (por ejemplo, indicando openai/outputTemplate si quieres vincular esta herramienta a un widget concreto).
Este encadenamiento tools/list → tools/call es el ciclo básico de trabajo de los MCP‑tools: primero discovery y luego uso.
Resources: datos direccionables
Resources en MCP son cualesquiera trozos de datos a los que el cliente puede acceder por URI: archivos, registros de BD, configuraciones, catálogos, etc.
Tienen un conjunto estándar de operaciones:
- resources/list: para saber qué recursos hay;
- resources/read: para leer un recurso concreto (o una parte del mismo).
Imaginemos el recurso gift_catalog, que describe el catálogo base de regalos: categorías, marcas, precios mínimos y máximos. El servidor puede declararlo con el URI "mcp://gift-server/resources/gift_catalog".
La respuesta a resources/list puede ser así (simplificado):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resources": [
{
"uri": "mcp://gift-server/resources/gift_catalog", // solo una cadena única. MCP no es un protocolo.
"name": "gift_catalog",
"description": "Base catalog of gifts with categories and prices",
"mimeType": "application/json"
}
],
"nextCursor": null
}
}
Y la lectura del recurso — resources/read:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
La respuesta puede contener el propio contenido y metadatos:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"contents": [
{
"uri": "mcp://gift-server/resources/gift_catalog",
"mimeType": "application/json",
"text": "{\"categories\":[\"boardgames\",\"books\"]}"
}
]
}
}
La idea principal: un recurso son datos direccionables, y los tools son operaciones. MCP hace explícitas ambas cosas en el protocolo.
Prompts: plantillas reutilizables
Prompts son «pistas preparadas» o plantillas que el servidor puede proporcionar al cliente. MCP los trata como un primitivo que tiene:
- un nombre;
- un título/descripción legible por humanos;
- contenido (a menudo una plantilla de system‑prompt o un conjunto de ejemplos few‑shot).
Y, como era de esperar, hay dos métodos:
- prompts/list: saber qué prompts hay;
- prompts/get: obtener el contenido de un prompt.
Por ejemplo, quieres definir un estilo especial para generar mensajes de felicitación junto con el regalo. Entonces en el servidor MCP se puede declarar el prompt gift_congrats_style.
La respuesta a prompts/list puede verse así:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"prompts": [
{
"name": "gift_congrats_style",
"description": "Style guide for birthday congratulations in a friendly tone"
}
]
}
}
Y prompts/get devolverá el propio texto (o contenido estructurado), que el cliente puede pasar después a la LLM como parte del system‑prompt. Ejemplo de tal solicitud y respuesta:
{
"jsonrpc": "2.0",
"id": 11,
"method": "prompts/get",
"params": {
"name": "gift_congrats_style"
}
}
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"prompt": {
"name": "gift_congrats_style",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a friendly assistant that writes short, warm birthday congratulations..."
}
]
}
]
}
}
}
6. Cómo se relaciona con Apps SDK y nuestro widget
Ahora el MCP‑JSON quizá siga pareciéndote algo aparatoso. Vamos a conectarlo con lo que ya has hecho mediante Apps SDK.
Recordemos, en el frontend del widget puedes tener este código:
// dentro de un componente de React en la sandbox de ChatGPT
async function fetchGifts() {
const result = await window.openai.callTool("suggest_gifts", {
occasion: "birthday",
budget: 50,
recipient: "friend who loves sci-fi"
});
console.log(result);
}
A nivel de Apps SDK es una función cómoda que:
- conoce la URL del servidor MCP (desde la configuración de la aplicación);
- sabe encontrar por nombre suggest_gifts la descripción de la herramienta;
- empaqueta tu llamada en un MCP‑request tools/call;
- la envía por el transporte elegido (HTTP/SSE);
- espera el MCP‑reply, desempaqueta el result y te lo devuelve como result en JavaScript.
Si lo dibujamos como un esquema, queda más o menos así:
sequenceDiagram
participant Widget
participant AppsSDK as Apps SDK
participant MCP as Servidor MCP
Widget->>AppsSDK: window.openai.callTool("suggest_gifts", {...})
AppsSDK->>MCP: JSON { id:7, method:"tools/call", params:{...} }
MCP-->>AppsSDK: JSON { id:7, result:{ content, structuredContent } }
AppsSDK-->>Widget: result (ToolOutput)
Widget->>Widget: setState(toolOutput)
Comprender el formato MCP te da dos habilidades estupendas.
Primero, puedes mirar con criterio los logs MCP en bruto (por ejemplo, en MCP Inspector, del que habrá una lección aparte) y ver: qué tools/call exacto salió, qué argumentos llevaba, qué volvió en result o error.
Segundo, al diseñar herramientas y recursos puedes pensar no solo en términos de tipos de TypeScript, sino también en términos de esquemas MCP: cómo se verá en JSON, qué tan cómodo será para otros clientes (por ejemplo, agentes que también puedan conectarse a tu servidor MCP).
7. Mini práctica: leemos y «arreglamos» MCP‑JSON
Para hacer tuyo el formato MCP, mejor desmenuzar con las manos un par de mensajes. Tomemos un ejemplo de diálogo completo tools/list → tools/call → resultado.
El cliente quiere la lista de herramientas
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
Qué vemos:
- es un request (hay id);
- el método es tools/list, así que se trata del discovery de herramientas;
- los parámetros están vacíos, sin paginación.
El servidor responde:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas",
"inputSchema": { "type": "object", "properties": { "occasion": { "type": "string" } } }
}
]
}
}
Se ve enseguida que es la respuesta a esa misma solicitud (mismo id: 1), el protocolo fue bien (result está, error no), y ahora el cliente sabe que existe la tool suggest_gifts.
El cliente invoca la herramienta
Después el cliente hace tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "anniversary"
}
}
}
Si el servidor espera también budget, pero el modelo no lo indicó, el servidor puede:
- o bien devolver un error de protocolo (por ejemplo, error con el código «invalid params»);
- o bien tomar una decisión por defecto (por ejemplo, usar un presupuesto medio) y devolver un result normal.
En los términos que introdujimos arriba, la primera opción es un error de protocolo (error en el nivel superior), la segunda ya es el ámbito de la lógica de negocio: igualmente devuelves un result válido y decides si considerar esa situación un error de negocio (isError: true) o un comportamiento normal.
La respuesta en caso de error en los argumentos podría ser así:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Missing required property 'budget' in arguments"
}
}
De nuevo, lo distinguimos de un error de negocio: el protocolo está violado (los argumentos no se ajustan al esquema), por lo tanto aquí procede error.
Ejemplo roto: buscamos el bug
Este es un JSON que a veces aparece entre principiantes:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"tool": "suggest_gifts",
"args": {
"occasion": "birthday",
"budget": 100
}
}
}
A primera vista parece verosímil, pero si lo comparas con la especificación de MCP notarás que los campos tool y args no coinciden con los esperados name y arguments.
Un cliente/servidor de MCP‑SDK probablemente nunca generará tal JSON, pero si, sin conocer la especificación, te integras a mano, un bug así es bastante real. Por eso en el curso desglosamos el protocolo «en limpio», y no solo los wrappers del SDK.
8. Errores típicos al trabajar con mensajes MCP
Error n.º 1: mezclar errores de protocolo y de negocio.
A menudo los desarrolladores, por costumbre, envuelven todo lo que «salió mal» en un error de nivel superior —tanto la ausencia de un recurso como argumentos incorrectos y caídas de la base—. En el contexto de MCP conviene separar: si la estructura JSON y el esquema de la llamada están violados (método incorrecto, campos equivocados, tipos erróneos), es motivo para devolver error. Si la herramienta simplemente no pudo ejecutar la operación de dominio (no hay regalos para ese presupuesto, el usuario no se encuentra), es mejor devolver un result válido con isError: true y un mensaje claro en content. Así tanto el modelo de ChatGPT como los depuradores podrán distinguir correctamente «se rompió el canal» de «el servidor rechazó conscientemente».
Error n.º 2: ignorar el campo id y la correlación de solicitudes.
A veces en los logs de un servidor MCP puede verse salida manual sin id o con valores de id repetidos para distintas solicitudes activas. En un hello‑world de un solo hilo aún puede «sobrevivir», pero en cuanto aparecen llamadas en paralelo o reintentos, se vuelve difícil entender qué respuesta corresponde a qué solicitud. JSON‑RPC exige expresamente un id único durante la vida de la solicitud, y MCP se basa en esta regla. Si usas los SDK oficiales, puedes olvidarte del id, pero en cuanto escribes el transporte o el logging tú mismo, no olvides conservar e imprimir el id: es lo primero por lo que depurarás bugs extraños.
Error n.º 3: estructuras inestables de result para un mismo método.
A veces resulta tentador cambiar «un poquito» el formato de la respuesta según la situación: a veces devolver una matriz de regalos, a veces un objeto con una sola cadena, a veces solo text sin structuredContent. El modelo quizá tolere tales juegos, pero tus widgets y cualquier otro cliente MCP —difícilmente—. La especificación de MCP para cada método describe una estructura predecible de result; procura ceñirte a ella. Si necesitas otro formato, mejor declara otra tool o versión, no cambies el esquema al vuelo.
Error n.º 4: campos de más o ausentes en params.
Un problema típico de implementaciones personalizadas es añadir a params lo que MCP no espera, u olvidar un campo obligatorio. Por ejemplo, enviar toolName en lugar de name en tools/call, o resourceId en lugar de uri en resources/read. Los SDK de MCP suelen validar estas cosas y lanzar una excepción clara, pero si trabajas más cerca del protocolo, puedes pasarte tiempo buscando por qué «el servidor no me entiende». Una buena práctica es mantener junto al manejador un ejemplo de solicitud JSON correcta de la especificación o de los logs de un cliente en funcionamiento y comparar con lo que envías.
Error n.º 5: intentar usar las notifications como «segundo canal de respuestas».
A veces, al ver las notifications, los desarrolladores empiezan a enviar resultados de operaciones por notificaciones en lugar de replies normales: «ya que estamos en MCP y tenemos SSE, vamos a empujarlo todo con notificaciones». El problema es que, por definición, las notificaciones de JSON‑RPC no están vinculadas a un id concreto y el cliente no las percibe como respuesta a una solicitud. Como resultado, es más difícil depurar e imposible entender a qué llamada de herramientas pertenece un mensaje concreto. Las notificaciones son ideales para eventos (cambió la lista de tools/resources/prompts, apareció nuevo progreso, llegó un log), pero no para respuestas normales a tools/call y similares.
Error n.º 6: no mirar los logs e inspectores de MCP.
El error más humano: intentar depurar la integración solo a través del UI de ChatGPT: «pulsé un botón, algo no llegó, ya lo veré algún día». Mientras no veas los mensajes MCP en bruto (requests, replies, notifications), es difícil entender en qué nivel está el problema: el modelo no invocó la herramienta, Apps SDK no llegó al servidor MCP, el servidor devolvió un JSON incorrecto, o todo se rompió ya al renderizar el widget. MCP Inspector / Jam y el logging estructurado de mensajes MCP son tus mejores amigos. Después de ver una vez un tools/call y tools/list reales en los logs, el formato de mensajes MCP dejará de ser «magia» y se convertirá en rutina de ingeniería.
GO TO FULL VERSION