CodeGym /Cursos /ChatGPT Apps /Modelo de eventos en MCP: tipos de notificaciones, format...

Modelo de eventos en MCP: tipos de notificaciones, formato de mensajes, idempotencia

ChatGPT Apps
Nivel 13 , Lección 0
Disponible

1. Para qué sirven los eventos de MCP

Hasta ahora, casi toda la comunicación entre ChatGPT y tu backend parecía RPC: el modelo invocaba una herramienta, esta hacía algo, devolvía el resultado y listo. Es cómodo mientras las operaciones son cortas: 200–500 ms, como máximo un par de segundos.

Pero en cuanto aparece algo de larga duración —el análisis de un archivo grande con las preferencias de empleados para GiftGenius, la agregación de recomendaciones desde montones de APIs externas, el recálculo de un feed voluminoso— todo se vuelve incómodo. Timeouts de HTTP, reinicios de funciones, spinners «eternos», y el usuario se queda pensando: «¿sigue vivo o ya ha muerto?».

Aquí es donde empieza el modelo de eventos. En lugar de mantener una llamada larga a la herramienta, lanzas una tarea, recibes un jobId, y a partir de ahí el servidor envía eventos por iniciativa propia: iniciado, hay progreso, listo, falló. Estos eventos en MCP se implementan como notificaciones JSON-RPC —mensajes unidireccionales sin id, de los que no se espera respuesta.

Es importante entender: un evento no es «un console.log por la red». Es un mensaje formal del protocolo con un esquema definido, que tu UI (widget) y/o agente debe saber procesar con la misma disciplina que el resultado de llamar a una herramienta.

Recordatorio: tipos de mensajes en MCP

Antes de seguir, repasemos brevemente qué tipos de mensajes existen en MCP.

Dejando a un lado la capa de marketing, MCP se apoya en JSON-RPC 2.0. Allí hay tres tipos básicos de mensajes: solicitudes, respuestas y notificaciones.

Para no enumerarlos en una lista, veamos una pequeña tabla comparativa:

Tipo Campo id Quién inicia ¿Se espera respuesta? Ejemplo en MCP
Request Normalmente el cliente (ChatGPT) Llamada de herramienta tools/call
Response Servidor MCP Es la respuesta Resultado de tools/call
Notification no Cliente o servidor No notifications/progress, resources/updated, logging/message

Los eventos de MCP viven precisamente en la tercera fila: son notificaciones. Rasgos distintivos:

  • no hay id en el nivel superior — no llegará ningún result ni error como respuesta;
  • el iniciador no espera un ACK — «disparar y olvidar» a nivel de protocolo;
  • la fiabilidad no se basa en confirmaciones, sino en la idempotencia de los manejadores y en la política de reintentos.

Una limitación importante: los eventos de MCP no «vuelan por ahí en cualquier momento». Viven dentro de una conexión MCP establecida sobre un transporte concreto. La mayoría de las veces es un flujo tipo SSE (los detalles del transporte y sus variantes los veremos en una lección aparte).

2. Qué es un «evento de MCP» en la práctica

Formalmente, un evento de MCP es una notificación JSON-RPC, es decir, un objeto de la forma:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "jobId": "job_123",
    "percentage": 30,
    "stage": "Buscando opciones en el catálogo",
    "eventId": "evt_abc123",
    "timestamp": "2025-11-21T10:15:00Z"
  }
}

Aquí hay varios puntos importantes:

  1. En el campo method codificamos el tipo de evento y su «espacio de nombres». MCP ya define una serie de métodos estándar del tipo notifications/... para logs, progreso y cambios de recursos, pero puedes y debes añadir tus propios métodos específicos del negocio, como notifications/job/progress o notifications/job/completed.
  2. Todos los datos de negocio están en params. Allí también almacenaremos los identificadores de tareas (jobId), IDs únicos de eventos (eventId), la hora (timestamp), mensajes legibles por humanos, etc.
  3. Falta el campo id en el nivel superior — por eso es una notificación. El protocolo no contempla respuesta a ella. Si el servidor quiere saber si «se le ha entendido», puede enviar otro evento o esperar acciones reactivas del cliente (por ejemplo, una nueva solicitud). Pero no hay ACK en términos de JSON-RPC.

A nivel de modelo mental, podemos pensarlo así: la llamada de herramienta tools/call es «una carta a la que esperas respuesta», y un evento es «una notificación de un bot de Slack: «La tarea en segundo plano n.º 123 se ha completado»».

3. Taxonomía de eventos: qué notificaciones existen

Si simplemente permites «enviar cualquier JSON como notificación», en dos semanas el sistema se convierte en un vertedero: los nombres de los eventos varían, los campos fluctúan, el UI no entiende qué hacer con ello. Por eso conviene acordar una pequeña taxonomía.

A continuación —una de las clasificaciones cómodas que encaja bien con la especificación de MCP y con casos reales de ChatGPT Apps.

Eventos del ciclo de vida de la tarea (Job Lifecycle)

Son eventos que reflejan transiciones clave del estado de la tarea. Normalmente, una tarea tiene una máquina de estados (state machine) como pendingrunning → (completed | failed | canceled).

Eventos típicos:

  • job.created — tarea registrada;
  • job.started — el worker ha comenzado a ejecutar el trabajo;
  • job.completed — la tarea finalizó con éxito;
  • job.failed — la tarea falló con un error;
  • job.canceled — la tarea fue cancelada por el usuario.

Ejemplo de job.completed para GiftGenius:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/completed",
  "params": {
    "eventId": "evt_gg_100",
    "jobId": "giftjob_42",
    "timestamp": "2025-11-21T10:20:00Z",
    "summary": "Selección de regalos completada",
    "resultResourceId": "resource:gifts:giftjob_42"
  }
}

Aquí resultResourceId puede apuntar a un recurso MCP que luego leerá el widget o el agente.

Eventos de progreso (Progress Updates)

Son «pasos pequeños» dentro del ciclo de vida: no cambian el estado final, pero dan al usuario la sensación de que algo está ocurriendo.

Evento típico job.progress:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "eventId": "evt_gg_101",
    "jobId": "giftjob_42",
    "timestamp": "2025-11-21T10:18:30Z",
    "percentage": 40,
    "stage": "Filtramos regalos por presupuesto",
    "etaSeconds": 25
  }
}

Aquí es importante que percentage avance de forma razonable hacia 100, y no salte de un lado a otro. Elige un único nombre para el campo de progreso (por ejemplo, percentage) y úsalo en todos los eventos. En la utilidad oficial de progreso de MCP también hay una regla: el progreso solo crece.

Eventos de actualización de datos (Resource/Data events)

A veces al usuario ni siquiera le importa un jobId concreto. Es más importante que alguna entidad haya cambiado: se actualizó el feed de productos, se generó un nuevo snapshot del informe, se regeneró el perfil personal.

En MCP ya existen notificaciones estándar de nivel resources/updated, resources/list_changed y similares, que señalan al cliente: «vuelve a leer la lista de recursos, algo ha cambiado».

Para GiftGenius podría verse así:

{
  "jsonrpc": "2.0",
  "method": "resources/updated",
  "params": {
    "eventId": "evt_feed_17",
    "timestamp": "2025-11-21T09:00:00Z",
    "resourceId": "resource:product-feed",
    "changeType": "snapshot_ready"
  }
}

El widget, al recibir este evento, puede, por ejemplo, resaltar el botón «Actualizar lista de regalos».

Eventos de UX y del sistema

Hay también eventos que no son estrictamente de negocio, pero son importantes para UX o diagnóstico:

  • mensajes de log logging/message — notificación estándar de MCP para logs;
  • heartbeat/ping — señales periódicas de «sigo vivo» del servidor;
  • avisos de degradación: por ejemplo, «ahora la API externa va lenta; los resultados pueden tardar más en llegar».

Estos eventos son útiles para monitorización y depuración; a veces se pueden mostrar de forma discreta en el UI, indicando a la persona que el sistema no está muerto, sino ocupado.

4. Estructura de un evento: campos obligatorios y payload

Un evento es un objeto de API igual que la llamada de una herramienta. Hay que diseñarlo. Un buen hábito es acordar un conjunto básico de campos.

Conceptualmente, es útil dividir un evento en tres partes: metadatos, correlación y carga útil.

Ejemplo de forma general:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "eventId": "evt_gg_103",
    "type": "job.progress",
    "timestamp": "2025-11-21T10:19:00Z",
    "jobId": "giftjob_42",
    "payload": {
      "percentage": 60,
      "stage": "Comparamos reseñas",
      "etaSeconds": 15
    }
  }
}

En esta estructura podemos distinguir:

  • eventId — identificador único del evento. Necesario para la desduplicación en el cliente;
  • type — nombre lógico del evento (puede duplicar/normalizar method);
  • timestamp — cuándo fue generado el evento por el servidor;
  • jobId u otro correlation-id — para entender a qué se refiere este evento;
  • payload — los datos propiamente dichos. Para cada tipo de evento tiene su propia forma.

En un sistema real, casi seguro querrás describir formalmente estas estructuras mediante JSON Schema o, al menos, tipos de TypeScript, para que tanto el servidor como el cliente validen los mensajes. En algunos equipos se usa un formato inspirado en CloudEvents: allí también hay campos estándar id, source, type, time, etc.

Pero la idea clave es sencilla: el evento debe ser legible por máquina y consistente, sin sorpresas como «a veces el campo se llama jobId, a veces job_id, a veces no está».

En los ejemplos siguientes, para no sobrecargar, usaremos más a menudo una variante «aplanada»: todos los datos del evento residen directamente en params sin un payload anidado, y el campo type a veces se omite si su papel ya lo desempeña method. El principio sigue siendo el mismo: cada evento tiene metadatos estables (eventId, jobId, timestamp) y una carga útil predecible.

5. Idempotencia de los eventos: por qué y cómo

Ahora la palabra más importante de esta lección: idempotencia.

La idempotencia de un manejador de eventos significa que, si se procesa el mismo evento una o diez veces, el estado final del sistema seguirá siendo correcto. En sistemas distribuidos con red y reintentos, es literalmente cuestión de vida o muerte.

¿Por qué puede llegar el mismo evento varias veces?

Muchas razones: desde cortes de conexión y reconexiones hasta reintentos en el servidor, que «por si acaso» envía la notificación otra vez. Usando protocolos de streaming (por ejemplo, cuando el servidor empuja eventos en una conexión abierta como SSE —habrá otra lección sobre el transporte—) esto es clásico: el cliente se reconecta con Last-Event-ID, el servidor reenvía los eventos omitidos y el cliente verá algunos por segunda vez.

Si tu manejador no es idempotente, empiezan las rarezas:

  • el evento job.completed provoca un abono doble de bonos o cambia dos veces el estado de un pedido;
  • el evento resource.updated hace que el widget «añada» tarjetas cada vez, duplicándolas en el UI;
  • repeticiones de job.progress asustan a los usuarios si la barra de progreso empieza a avanzar y retroceder.

La estrategia correcta funciona en dos capas: la generación de eventos en el servidor y su procesamiento en el cliente.

Lado servidor: IDs estables y máquina de estados

El servidor debe:

  • generar un eventId único para cada evento lógico;
  • garantizar que los eventos de un mismo jobId forman una secuencia de estados válida: no puedes enviar job.failed después de job.completed ni dos job.completed distintos con resultados diferentes.

Es decir, tienes de facto una máquina de estados de la tarea, y cada evento es una transición permitida.

Lado cliente: desduplicación y actualizaciones «suaves»

El cliente (widget, agente u otro componente) debe:

  • almacenar el conjunto de eventId ya procesados al menos durante la vida de la conexión/sesión actual;
  • comprobar antes de procesar: si ya has visto ese eventId, simplemente ignóralo o vuelve a pintar el UI sin efectos secundarios;
  • al recibir eventos que cambian el estado de la tarea (job.completed, job.failed), asegurarte de que la transición es válida: por ejemplo, si la tarea ya está marcada como completed, un job.completed repetido no debe cambiar nada, y failed debería ignorarse por incorrecto.

Ejemplo clásico del mundo del comercio: procesamiento del webhook de confirmación de pago. Un mismo order.paid puede llegar dos veces; por eso el backend guarda el paymentId y una marca de «ya abonado». Aunque el webhook llegue otra vez, el estado del pedido no cambia. Los eventos de MCP deben diseñarse con la misma mentalidad.

6. Ejemplo: diseñamos eventos para GiftGenius

Llevemos esto a nuestro GiftGenius didáctico. Imaginemos un escenario largo: el usuario sube un CSV grande con la lista de empleados y sus intereses, y pide «elegir ideas de regalos para todos». La operación puede tardar decenas de segundos.

Un modelo razonable de eventos podría describirse así:

  1. El usuario lanza la herramienta start_bulk_gift_analysis. La herramienta devuelve un jobId: "bulk_2025_001".
  2. El servidor MCP crea la tarea y casi de inmediato envía job.started con una breve descripción.
  3. A medida que avanza, envía varios job.progress con etapas:
    • 10 % — «Parseamos el archivo y comprobamos el formato»;
    • 40 % — «Extraemos intereses y departamentos»;
    • 70 % — «Emparejamos regalos por categorías»;
    • 100 % — justo antes de finalizar.
  4. Al final llega job.completed con un enlace al recurso con las recomendaciones finales.
  5. Si algo salió mal —en lugar de completed llegará job.failed con un código de error y, posiblemente, una sugerencia de qué corregir.

Informalmente así será, pero fijémoslo en forma de esquemas JSON para dos eventos clave: job.progress y job.completed. Pseudo JSON Schema (simplificada):

{
  "job.progress": {
    "type": "object",
    "properties": {
      "eventId": { "type": "string" },
      "jobId": { "type": "string" },
      "timestamp": { "type": "string", "format": "date-time" },
      "percentage": { "type": "number", "minimum": 0, "maximum": 100 },
      "stage": { "type": "string" },
      "etaSeconds": { "type": "number" }
    },
    "required": ["eventId", "jobId", "timestamp", "percentage", "stage"]
  }
}
{
  "job.completed": {
    "type": "object",
    "properties": {
      "eventId": { "type": "string" },
      "jobId": { "type": "string" },
      "timestamp": { "type": "string", "format": "date-time" },
      "summary": { "type": "string" },
      "resultResourceId": { "type": "string" }
    },
    "required": ["eventId", "jobId", "timestamp", "resultResourceId"]
  }
}

No estás obligado a implementar ahora una validación completa de esquemas, pero es útil mantener mentalmente esta estructura: ayuda a no «desparramar» campos en distintos formatos y a no olvidar metadatos importantes.

7. Mini-práctica: servidor que envía eventos MCP

Ahora unimos la teoría con un pequeño trozo de TypeScript pseudocódigo. No entraremos en librerías reales de MCP (primero, aún evolucionan; segundo, aquí el foco está en el modelo), pero dibujaremos el esqueleto estructural.

Supongamos que en nuestro servidor MCP existe la abstracción sendNotification, que sabe enviar una notificación JSON-RPC de vuelta a ChatGPT. Pseudo-interfaz:

// Utilidad para enviar una notificación MCP
async function sendNotification(
  method: string,
  params: Record<string, unknown>
) {
  // Aquí serializarías el JSON y lo enviarías por la conexión MCP activa
}

Ahora implementemos el manejador de la herramienta start_bulk_gift_analysis. Registra una tarea, devuelve un jobId, y en «background» va generando y enviando progreso. En la vida real sería un worker y una cola, pero de momento nos basta con un temporizador.

type Job = {
  id: string;
  status: "pending" | "running" | "completed" | "failed";
};

const jobs = new Map<string, Job>();

export async function startBulkGiftAnalysisTool() {
  const jobId = `bulk_${Date.now()}`;
  jobs.set(jobId, { id: jobId, status: "pending" });

  // Enviamos inmediatamente job.started
  await sendNotification("notifications/job/started", {
    eventId: `evt_${jobId}_started`,
    jobId,
    timestamp: new Date().toISOString(),
    summary: "Se ha iniciado el análisis de una lista grande de regalos"
  });

  simulateJob(jobId); // "lanzamos" la tarea en background

  return { jobId };
}

La simulación de la tarea en sí:

async function simulateJob(jobId: string) {
  jobs.set(jobId, { id: jobId, status: "running" });

  const stages = [
    { percent: 10, stage: "Parseamos CSV" },
    { percent: 40, stage: "Analizamos intereses" },
    { percent: 70, stage: "Seleccionamos regalos" },
    { percent: 100, stage: "Generamos el resultado" }
  ];

  for (const s of stages) {
    await sendNotification("notifications/job/progress", {
      eventId: `evt_${jobId}_${s.percent}`,
      jobId,
      timestamp: new Date().toISOString(),
      percentage: s.percent,
      stage: s.stage
    });
    await new Promise(r => setTimeout(r, 1000));
  }

  jobs.set(jobId, { id: jobId, status: "completed" });

  await sendNotification("notifications/job/completed", {
    eventId: `evt_${jobId}_done`,
    jobId,
    timestamp: new Date().toISOString(),
    summary: "El análisis de regalos se ha completado",
    resultResourceId: `resource:gifts:${jobId}`
  });
}

El código es deliberadamente simple, pero en él se ve bien:

  • usamos la secuencia de eventos startedprogress* → completed;
  • cada evento recibe un eventId único;
  • todos los eventos están vinculados al mismo jobId.

En el futuro, cuando añadas colas y workers reales, la estructura de eventos seguirá siendo prácticamente la misma: solo cambiará el lugar desde donde se invoca sendNotification.

8. Cliente: un manejador de eventos idempotente sencillo

En el lado del cliente (por ejemplo, en tu widget del Apps SDK) hay que aprender a recibir estos eventos, vincularlos con las tareas actuales y no volverse loco con los duplicados.

Sin entrar aún en el transporte (eso más adelante), imaginemos una función onMcpNotification, que tu capa cliente de MCP llama en cada notificación entrante.

Añadamos una desduplicación sencilla:

const processedEvents = new Set<string>();

function handleNotification(method: string, params: any) {
  const eventId = params.eventId as string | undefined;
  if (!eventId) return; // muy discutible, pero para el ejemplo vale

  if (processedEvents.has(eventId)) {
    // Repetido: ignoramos o actualizamos el UI suavemente
    return;
  }
  processedEvents.add(eventId);

  if (method === "notifications/job/progress") {
    updateJobProgress(params.jobId, params.percentage, params.stage);
  } else if (method === "notifications/job/completed") {
    markJobCompleted(params.jobId, params.resultResourceId);
  }
}

La implementación de updateJobProgress y markJobCompleted ya es código puro de React/UI:

function updateJobProgress(jobId: string, percent: number, stage: string) {
  // por ejemplo, lo guardamos en Zustand/Redux/React state
  console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}

function markJobCompleted(jobId: string, resourceId: string) {
  console.log(`Job ${jobId} completado, recurso: ${resourceId}`);
}

Este manejador:

  • no se rompe si el evento llega dos veces;
  • no produce efectos secundarios (del tipo «mostrar por segunda vez el modal “¡Listo!”»);
  • abre camino a lógica más compleja, por ejemplo, validar las transiciones de estado permitidas (no permitir failed sobre algo ya completed).

En código de producción probablemente querrás vaciar processedEvents al reconectar con el servidor MCP, así como almacenar no solo el eventId, sino también el estado actual de cada jobId, para comportarte de forma más razonable ante secuencias extrañas de eventos.

Después es importante entender cómo pasan todos estos eventos de MCP por el agente/widget y se convierten en una experiencia de usuario concreta: barra de progreso, etapas de ejecución, aparición de resultados finales. Pasemos a vincular eventos con run/workflow y UX.

9. Vinculación de eventos, run/workflow y UX

Aunque ya tuvimos un módulo completo sobre workflows y agentes, ahora verás la imagen completa. Ya introdujimos familias de eventos (job.*, resource.*, del sistema); veamos cómo pasan por el agente/widget y ChatGPT y se convierten en una experiencia de usuario concreta.

Un escenario típico con una tarea de larga duración se ve así: ChatGPT llama a una herramienta MCP, obtiene un jobId; después, para ese jobId, el servidor envía eventos de progreso, finalización o error; tu widget o la lógica del agente, en base a ellos, actualiza el UI y toma decisiones.

En un diagrama de secuencia podría dibujarse así:

sequenceDiagram
    participant User as Usuario
    participant GPT as ChatGPT (modelo)
    participant App as Servidor MCP de GiftGenius
    participant Widget as Widget de GiftGenius

    User->>GPT: "Elige regalos para 2000 empleados"
    GPT->>App: tools.call start_bulk_gift_analysis
    App-->>GPT: response { jobId: "bulk_2025_001" }

    GPT->>Widget: ToolOutput { jobId }
    Widget->>Widget: Mostrar barra de progreso

    App-->>GPT: notification job.started
    App-->>GPT: notification job.progress (10%, 40%, 70%, 100%)
    App-->>GPT: notification job.completed { resultResourceId }

    GPT->>Widget: Propaga eventos/datos al widget
    Widget->>User: Actualiza el progreso y muestra el resultado
    

En la práctica el diagrama real será un poco más complejo, pero la idea clave es simple: los eventos de MCP son el «sistema nervioso» entre tus operaciones en background y la experiencia del usuario.

10. Errores típicos al trabajar con eventos MCP

Error n.º 1: «Evento = log en formato de producción».
A veces los desarrolladores empiezan simplemente reenviando a MCP lo que antes escribían en console.log. Como resultado, en los eventos no hay eventId, ni jobId, ni un timestamp decente, solo mensajes semipoéticos de «ya casi terminamos». Este enfoque vuelve el sistema frágil: son difíciles de parsear, imposible desduplicarlos, el UI no sabe a qué tarea pertenece el mensaje. Es mejor diseñar los eventos desde el principio como un contrato formal: nombre de método claro, conjunto de campos estable, payload lógico.

Error n.º 2: Falta de idempotencia y de eventId único.
Muchos empiezan con la idea ingenua: «bah, los eventos llegan una vez». A la semana empieza el caos: al reconectarse el cliente, se duplican notificaciones; el usuario recibe lo mismo dos veces; el backend comercial abona los bonos por duplicado. Sin un eventId único y una desduplicación elemental en el cliente, tarde o temprano tendrás un bug serio. En un sistema distribuido hay que partir del modelo «at-least-once delivery»: los duplicados son inevitables.

Error n.º 3: Mezclar eventos del sistema y de negocio en un mismo batiburrillo.
Por ejemplo, en un mismo flujo caen logging/message, job.progress, job.completed, resources/updated, y todo ello sin una clara separación por type/method. El resultado es que la capa de UI empieza a hacer cosas raras como if (message.includes("listo")) para entender que la tarea terminó. Es mejor separar con claridad: hay notificaciones del sistema (logs, heartbeat) y hay eventos de negocio (job.*, resource.*) que tienen esquemas estrictamente descritos.

Error n.º 4: Transiciones de estado de la tarea inconsistentes.
Ocurre que el servidor, en un mismo flujo de eventos, primero envía job.completed, luego de repente job.progress, luego job.failed. Esto pasa si no hay una máquina de estados explícita y comprobaciones al emitir eventos. Para los clientes se vuelve imposible entender qué está pasando realmente. Lo correcto es describir un autómata finito de estados y no emitir eventos que lo violen: por ejemplo, después de completed como mucho puedes mandar un evento informativo adicional, pero no regresar la tarea a running.

Error n.º 5: Atarse rígidamente a nombres de métodos MCP concretos de la versión actual de la especificación.
La especificación de MCP sigue evolucionando. Si atas todo a métodos concretos actuales con nombres del sistema, sin contemplar tus propios namespaces, cualquier cambio del protocolo te obligará a reescribir medio sistema. Es mejor percibir los eventos como tu mini especificación sobre MCP: puedes basarte en los métodos existentes (notifications/progress, resources/updated), pero diseñar los eventos de negocio (notifications/job/*) en tu propio espacio de nombres y mantenerlos relativamente independientes.

Error n.º 6: Sin vínculo entre eventos y UX.
A veces el equipo crea un modelo de eventos bonito en el backend, pero no lo lleva hasta el widget: job.progress existe en los logs, pero el UI muestra un solitario spinner durante 40 segundos. En ese escenario el usuario no confía ni en MCP ni en la IA. Al diseñar los eventos, piensa siempre qué efecto de UI concreto quieres conseguir: barra de progreso, etapas, resultados parciales. Los eventos de MCP no existen por el protocolo, sino por un comportamiento de la aplicación comprensible.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION