CodeGym /Corsi /ChatGPT Apps /Modello degli eventi in MCP: tipi di notifiche, formato d...

Modello degli eventi in MCP: tipi di notifiche, formato dei messaggi, idempotenza

ChatGPT Apps
Livello 13 , Lezione 0
Disponibile

1. Perché servono gli eventi MCP

Finora quasi tutta la comunicazione tra ChatGPT e il vostro backend è sembrata un RPC: il modello chiama uno strumento, questo fa qualcosa, restituisce il risultato — fatto. È comodo finché le operazioni sono brevi: 200–500 ms, al massimo un paio di secondi.

Ma non appena appare qualcosa di lunga durata — analisi di un file di grandi dimensioni con le preferenze dei dipendenti per GiftGenius, aggregazione di raccomandazioni da un mucchio di API esterne, ricalcolo di un grande feed — tutto diventa spiacevole. Timeout HTTP, riavvii di funzioni, spinner «eterni», e l’utente si chiede: «è ancora vivo o è già morto?».

Qui entra in gioco il modello a eventi. Invece di mantenere una singola chiamata lunga allo strumento, avviate un job, ottenete un jobId e poi il server, di sua iniziativa, invia eventi: avviato, avanzamento, completato, fallito. Questi eventi in MCP sono implementati come JSON-RPC notifications — messaggi unidirezionali senza id, per i quali non è prevista alcuna risposta.

È importante capire: un evento non è un «console.log sul filo». È un messaggio formale del protocollo con uno schema definito, che la vostra UI (widget) e/o l’agente devono saper gestire con la stessa disciplina del risultato di una chiamata allo strumento.

Promemoria: tipi di messaggi in MCP

Prima di proseguire, un breve ripasso su quali messaggi esistono in MCP.

MCP si basa su JSON-RPC 2.0. Ci sono tre tipi di messaggi di base: richieste, risposte e notifiche.

Invece di elencarli, guardiamo una piccola tabella comparativa:

Tipo Campo id Chi lo inizia Si aspetta una risposta? Esempio in MCP
Request presente Di solito il client (ChatGPT) Chiamata dello strumento tools/call
Response presente Server MCP Questa è la risposta Risultato di tools/call
Notification assente Client o server No notifications/progress, resources/updated, logging/message

Gli eventi MCP vivono proprio nella terza riga: sono notifications. Segni distintivi:

  • nessun id al livello superiore — non arriverà alcun result o error in risposta;
  • l’iniziatore non aspetta un ACK — «spara e dimentica» a livello di protocollo;
  • l’affidabilità si costruisce non tramite conferme, ma tramite l’idempotenza dei gestori e una politica di retry.

Una limitazione importante: gli eventi MCP non «volano nello spazio in qualunque momento». Vivono all’interno di una connessione MCP stabilita sopra un trasporto specifico. Molto spesso è un flusso tipo SSE (i dettagli del trasporto e delle varianti li vedremo in una lezione separata).

2. Che cos’è un «evento MCP» in pratica

Formalmente un evento MCP è una JSON-RPC notification, cioè un oggetto del tipo:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "jobId": "job_123",
    "percentage": 30,
    "stage": "Cerchiamo opzioni nel catalogo",
    "eventId": "evt_abc123",
    "timestamp": "2025-11-21T10:15:00Z"
  }
}

Qui alcuni punti importanti:

  1. Nel campo method codifichiamo il tipo di evento e il suo «namespace». MCP definisce già una serie di metodi standard del tipo notifications/... per log, avanzamento e modifica delle risorse, ma potete e dovete aggiungere i vostri metodi specifici di business, come notifications/job/progress o notifications/job/completed.
  2. Tutti i dati di business risiedono in params. Lì conserveremo gli identificatori dei job (jobId), gli id univoci degli eventi (eventId), il tempo (timestamp), messaggi leggibili e altro.
  3. Manca il campo id al livello superiore — per questo è una notification. Il protocollo non prevede una risposta ad essa. Se il server vuole sapere «se è stato capito», può inviare un altro evento o aspettare azioni reattive del client (ad esempio, una nuova richiesta). Ma non esiste un ACK nei termini di JSON-RPC.

A livello di modello mentale si può pensare così: la chiamata allo strumento tools/call è «una lettera a cui ti aspetti una risposta», mentre un evento è «una notifica da uno Slack bot: «Attività in background #123 completata»».

3. Tassonomia degli eventi: quali notifiche esistono

Se si permette semplicemente «inviate qualsiasi JSON come notifications», in due settimane il sistema diventa una discarica: i nomi degli eventi divergono, i campi variano, la UI non capisce cosa farne. Perciò è utile concordare una piccola tassonomia.

Di seguito — una comoda variante di classificazione, ben allineata con la specifica MCP e i casi reali delle ChatGPT Apps.

Eventi del ciclo di vita del job (Job lifecycle)

Sono eventi che riflettono transizioni chiave di stato del job. Di solito un job ha una state machine tipo pendingrunning → (completed | failed | canceled).

Eventi tipici:

  • job.created — job registrato;
  • job.started — il worker ha iniziato l’esecuzione;
  • job.completed — job completato con successo;
  • job.failed — il job è fallito con errore;
  • job.canceled — il job è stato annullato dall’utente.

Esempio di job.completed per GiftGenius:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/completed",
  "params": {
    "eventId": "evt_gg_100",
    "jobId": "giftjob_42",
    "timestamp": "2025-11-21T10:20:00Z",
    "summary": "Selezione dei regali completata",
    "resultResourceId": "resource:gifts:giftjob_42"
  }
}

Qui resultResourceId può puntare a una risorsa MCP che poi leggerà il widget o l’agente.

Eventi di avanzamento (Progress updates)

Sono «passaggi minori» all’interno del ciclo di vita: non cambiano lo stato finale, ma danno all’utente la sensazione che qualcosa stia accadendo.

Un tipico evento 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": "Filtriamo i regali per budget",
    "etaSeconds": 25
  }
}

È importante che percentage aumenti ragionevolmente verso 100, e non salti avanti e indietro. Scegliete un unico nome per il campo di avanzamento (ad esempio, percentage) e usatelo in tutti gli eventi. Anche nell’utility ufficiale di avanzamento di MCP c’è una regola: l’avanzamento cresce soltanto.

Eventi di aggiornamento dei dati (Resource/Data events)

A volte all’utente non interessa nemmeno lo specifico jobId. È più importante che una certa entità sia cambiata: il feed dei prodotti si è aggiornato, è stato formato un nuovo snapshot del report, è stato rigenerato il profilo personale.

In MCP esistono già notifiche standard a livello di resources/updated, resources/list_changed e simili, che segnalano al client: «ricalcola l’elenco delle risorse, qualcosa è cambiato».

Per GiftGenius può essere così:

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

Il widget, ricevuto tale evento, può per esempio evidenziare il pulsante «Aggiorna l’elenco dei regali».

Eventi UX e di sistema

Ci sono anche eventi non strettamente di business, ma importanti per UX o diagnostica:

  • messaggi di log logging/message — notifica MCP standard per i log;
  • heartbeat/ping — «sono vivo» periodici dal server;
  • avvisi di degrado: ad esempio, «l’API esterna è lenta, i risultati potrebbero arrivare più lentamente».

Tali eventi sono utili per il monitoraggio e il debugging; a volte si possono rendere più discreti in UI, mostrando all’utente che il sistema non è morto ma semplicemente occupato.

4. Struttura dell’evento: campi obbligatori e payload

Un evento è un oggetto API come una richiesta di uno strumento. Va progettato. Una buona abitudine è accordarsi su un set di campi di base.

Concettualmente è utile dividere un evento in tre parti: metadati, correlazione e payload.

Esempio di forma generale:

{
  "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": "Confrontiamo le recensioni",
      "etaSeconds": 15
    }
  }
}

In questa struttura possiamo distinguere:

  • eventId — identificatore univoco dell’evento. Serve per la deduplicazione lato client;
  • type — nome logico dell’evento (può duplicare/normalizzare method);
  • timestamp — quando l’evento è stato generato dal server;
  • jobId o altro correlation-id — per capire a cosa si riferisce l’evento;
  • payload — i dati veri e propri. Per ogni tipo di evento ha una forma propria.

In un sistema reale probabilmente vorrete descrivere formalmente queste strutture tramite JSON Schema o almeno tipi TypeScript, in modo che sia il server sia il client validino i messaggi. Alcuni team usano un formato ispirato a CloudEvents: lì ci sono campi standard come id, source, type, time, ecc.

Ma l’idea chiave è semplice: l’evento deve essere leggibile dalla macchina e consistente — senza sorprese tipo «a volte il campo si chiama jobId, a volte job_id, a volte non c’è».

Negli esempi sotto, per non appesantire il codice, useremo più spesso una variante «appiattita»: tutti i dati dell’evento stanno direttamente in params senza il payload annidato, e il campo type a volte viene omesso, se il suo ruolo è già svolto da method. Il principio rimane lo stesso: ogni evento ha metadati stabili (eventId, jobId, timestamp) e un payload prevedibile.

5. Idempotenza degli eventi: perché e come

Ora la parola più importante di questa lezione — idempotenza.

L’idempotenza del gestore di un evento significa che, se lo stesso evento viene elaborato una o dieci volte, lo stato finale del sistema rimane corretto. Nelle architetture distribuite con rete e retry è letteralmente una questione di vita o di morte.

Perché lo stesso evento può arrivare più volte?

Le ragioni sono molte: dalle banali interruzioni di connessione e riconnessioni fino ai retry lato server, che «per sicurezza» invia di nuovo la notifica. Con protocolli in streaming (ad esempio quando il server invia eventi su una connessione aperta, tipo SSE — ne parleremo meglio in una lezione sul trasporto) è un classico: il client si ricollega con Last-Event-ID, il server recapiterà gli eventi mancanti e alcuni il client li vedrà una seconda volta.

Se il vostro gestore non è idempotente, iniziano stranezze:

  • l’evento job.completed causa un doppio accredito di bonus o cambia due volte lo stato dell’ordine;
  • l’evento resource.updated costringe il widget a «aggiungere» ogni volta le card, duplicandole nella UI;
  • i job.progress ripetuti spaventano gli utenti se la barra di avanzamento inizia a saltare avanti e indietro.

La strategia giusta funziona su due strati: generazione degli eventi sul server e loro gestione sul client.

Lato server: id stabili e state machine

Il server deve:

  • generare un eventId univoco per ogni evento logico;
  • garantire che gli eventi di uno stesso jobId formino una sequenza di stati valida: non potete inviare job.failed dopo job.completed o due job.completed diversi con risultati differenti.

Cioè avete di fatto una state machine del job, e ogni evento è una transizione consentita.

Lato client: deduplicazione e aggiornamenti «soft»

Il client (widget, agente o altro componente) deve:

  • conservare l’insieme degli eventId già elaborati almeno per la durata della connessione/sessione in corso;
  • verificare prima dell’elaborazione: se l’eventId è già stato visto, ignorare o ridisegnare la UI senza effetti collaterali;
  • alla ricezione di eventi che cambiano lo stato del job (job.completed, job.failed), assicurarsi che la transizione sia lecita: per esempio, se il job è già marcato come completed, un job.completed ripetuto non deve cambiare nulla, mentre un failed andrebbe ignorato come non corretto.

Esempio classico dal mondo commerce: gestione del webhook di conferma pagamento. Lo stesso order.paid può arrivare due volte; perciò il backend conserva il paymentId e un flag «già accreditato». Anche se il webhook arriva una seconda volta, lo stato dell’ordine non cambia. Gli eventi MCP vanno progettati con lo stesso mindset.

6. Esempio: progettiamo gli eventi per GiftGenius

Portiamo tutto questo nel nostro GiftGenius didattico. Immaginiamo uno scenario lungo: l’utente ha caricato un grande CSV con l’elenco dei dipendenti e i loro interessi, chiedendo «trova idee regalo per tutti». L’operazione può richiedere decine di secondi.

Un modello ragionevole di eventi può essere descritto così:

  1. L’utente avvia lo strumento start_bulk_gift_analysis. Lo strumento restituisce un jobId: "bulk_2025_001".
  2. Il server MCP crea il job e quasi subito invia job.started con una breve descrizione.
  3. Durante l’esecuzione invia diversi job.progress con le fasi:
    • 10% — «Eseguiamo il parsing del file e verifichiamo il formato»;
    • 40% — «Estraiamo interessi e dipartimenti»;
    • 70% — «Abbiniamo i regali per categoria»;
    • 100% — poco prima della conclusione.
  4. Alla fine arriva job.completed con il riferimento alla risorsa con le raccomandazioni finali.
  5. Se qualcosa va storto — invece di completed arriva job.failed con un codice di errore e, possibilmente, un suggerimento su cosa correggere.

Informalmente è così che avverrà, ma fissiamolo come JSON Schema per due eventi chiave, job.progress e job.completed. Pseudo-JSON Schema (semplificata):

{
  "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"]
  }
}

Non siete obbligati a implementare adesso una validazione completa degli schemi, ma tenerli a mente è utile: aiuta a non «spargere» i campi in formati diversi e a non dimenticare metadati importanti.

7. Mini-pratica: un server che invia eventi MCP

Ora uniamo la teoria a un piccolo pezzo di pseudo‑codice TypeScript. Non useremo le librerie MCP reali (primo, stanno ancora evolvendo; secondo, qui ci concentriamo sul modello), ma disegneremo uno scheletro strutturale.

Supponiamo che nel nostro server MCP ci sia un’astrazione sendNotification, che sappia inviare una JSON-RPC notification indietro a ChatGPT. Pseudo‑interfaccia:

// Utility per inviare una notification MCP
async function sendNotification(
  method: string,
  params: Record<string, unknown>
) {
  // Qui serializzeresti il JSON e lo invieresti sulla connessione MCP attiva
}

Ora implementiamo il gestore dello strumento start_bulk_gift_analysis. Registra un job, restituisce il jobId, e in background «ticchetta» inviando l’avanzamento. Nella vita reale sarebbe un worker e una coda, ma per ora ci basta un timer.

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" });

  // Inviamo subito job.started
  await sendNotification("notifications/job/started", {
    eventId: `evt_${jobId}_started`,
    jobId,
    timestamp: new Date().toISOString(),
    summary: "Avviata l’analisi del grande elenco di regali"
  });

  simulateJob(jobId); // avviamo il job in background

  return { jobId };
}

La stessa simulazione del job:

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

  const stages = [
    { percent: 10, stage: "Facciamo il parsing del CSV" },
    { percent: 40, stage: "Analizziamo gli interessi" },
    { percent: 70, stage: "Selezioniamo i regali" },
    { percent: 100, stage: "Prepariamo il risultato" }
  ];

  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: "Analisi dei regali completata",
    resultResourceId: `resource:gifts:${jobId}`
  });
}

Il codice è volutamente semplice, ma mostra bene:

  • usiamo la sequenza di eventi startedprogress* → completed;
  • ogni evento riceve un eventId univoco;
  • tutti gli eventi sono collegati allo stesso jobId.

In futuro, quando aggiungerete code e worker reali, la struttura degli eventi rimarrà più o meno la stessa — cambierà soltanto il punto in cui viene chiamato sendNotification.

8. Client: un gestore di eventi idempotente elementare

Lato client (ad esempio, nel vostro widget dell’Apps SDK) bisogna imparare a ricevere tali eventi, associarli ai job correnti e non impazzire per i duplicati.

Senza entrare ancora nel trasporto (ne parleremo dopo), immaginiamo una funzione onMcpNotification, che lo strato client MCP chiama a ogni notification in ingresso.

Aggiungiamo una deduplicazione elementare:

const processedEvents = new Set<string>();

function handleNotification(method: string, params: any) {
  const eventId = params.eventId as string | undefined;
  if (!eventId) return; // molto discutibile, ma per l'esempio va bene

  if (processedEvents.has(eventId)) {
    // Duplicato — ignorare o aggiornare delicatamente la UI
    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);
  }
}

L’implementazione di updateJobProgress e markJobCompleted è puro codice React/UI:

function updateJobProgress(jobId: string, percent: number, stage: string) {
  // ad esempio, lo mettiamo nello stato di Zustand/Redux/React
  console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}

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

Un gestore del genere:

  • non si rompe se l’evento arriva due volte;
  • non produce effetti collaterali (tipo «mostrare due volte la modale “Fatto!”»);
  • spiana la strada a logiche più complesse, ad esempio validazione delle transizioni di stato consentite (non permettere failed sopra un completed già avvenuto).

Nel codice di produzione probabilmente vorrete azzerare processedEvents al riconnettersi al server MCP, nonché conservare non solo l’eventId, ma anche lo stato corrente di ciascun jobId, in modo da comportarvi più saggiamente in caso di sequenze di eventi strane.

È poi importante capire come tutti questi eventi MCP attraversano agente/widget e si trasformano in un’esperienza utente concreta: barra di avanzamento, fasi di esecuzione, comparsa dei risultati finali. Passiamo al collegamento tra eventi, run/workflow e UX.

9. Collegamento fra eventi, run/workflow e UX

Anche se abbiamo già un modulo completo su workflow e agenti, qui vedrete l’insieme completo. Abbiamo introdotto famiglie di eventi (job.*, resource.*, di sistema); vediamo come passano attraverso agente/widget e ChatGPT e diventano un’esperienza utente concreta.

Uno scenario tipico con un job di lunga durata è questo: ChatGPT chiama un MCP tool ottenendo un jobId; poi, per quel jobId, il server invia eventi di avanzamento, completamento o errore; il vostro widget o la logica dell’agente, sulla loro base, aggiorna la UI e prende decisioni.

In un diagramma di sequenza lo si può disegnare così:

sequenceDiagram
    participant User as Utente
    participant GPT as ChatGPT (modello)
    participant App as Server MCP GiftGenius
    participant Widget as Widget GiftGenius

    User->>GPT: "Scegli i regali per 2000 dipendenti"
    GPT->>App: tools.call start_bulk_gift_analysis
    App-->>GPT: response { jobId: "bulk_2025_001" }

    GPT->>Widget: ToolOutput { jobId }
    Widget->>Widget: Mostrare la barra di avanzamento

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

    GPT->>Widget: Propaga eventi/dati al widget
    Widget->>User: Aggiorna l’avanzamento e mostra il risultato
    

In pratica il diagramma reale sarà un po’ più complesso, ma l’idea chiave è semplice: gli eventi MCP sono il «sistema nervoso» tra le vostre operazioni in background e l’esperienza utente.

10. Errori tipici nel lavoro con gli eventi MCP

Errore n. 1: «Evento = log in formato produzione».
A volte gli sviluppatori iniziano semplicemente a inoltrare in MCP ciò che prima scrivevano in console.log. Di conseguenza negli eventi non ci sono né eventId, né jobId, né un timestamp decente, solo messaggi semi‑poetici «siamo quasi alla fine». Un tale approccio rende il sistema fragile: è difficile fare parsing, impossibile deduplicare, la UI non sa a quale job si riferisca il messaggio. Meglio progettare gli eventi fin dall’inizio come un contratto formale: nome del metodo chiaro, set di campi stabile, payload logico.

Errore n. 2: Mancanza di idempotenza e di un eventId univoco.
Molti partono dall’idea ingenua: «gli eventi arrivano una sola volta». Dopo una settimana inizia: al riconnettersi del client le notifiche si duplicano, l’utente riceve due volte la stessa cosa, il backend commerciale accredita i bonus due volte. Senza un eventId univoco e una deduplicazione elementare lato client prima o poi catturerete un bug serio. In un sistema distribuito bisogna partire dal modello «at‑least‑once delivery»: i duplicati sono inevitabili.

Errore n. 3: Mescolare eventi di sistema e di business in un unico calderone.
Per esempio, nello stesso flusso cadono logging/message, job.progress, job.completed, resources/updated, e tutto senza chiare separazioni tramite type/method. Di conseguenza lo strato UI inizia a fare cose bizzarre tipo if (message.includes("completato")) per capire che il job è finito. Meglio separare nettamente: ci sono notifiche di sistema (log, heartbeat) ed eventi di business (job.*, resource.*) con schemi descritti in modo rigoroso.

Errore n. 4: Transizioni di stato del job incoerenti.
Capita che il server nello stesso flusso di eventi invii prima job.completed, poi all’improvviso job.progress, poi job.failed. Succede se non c’è una state machine esplicita e controlli in emissione. Per i client diventa impossibile capire cosa stia succedendo. È meglio descrivere un automa a stati finiti e non emettere eventi che lo violano: ad esempio, dopo completed al massimo si può inviare un evento informativo aggiuntivo, ma non riportare il job in running.

Errore n. 5: Legarsi rigidamente a specifici nomi di metodi MCP della versione attuale della spec.
La specifica MCP è ancora in evoluzione. Se legate tutto a metodi specifici attuali con nomi di sistema, senza prevedere i vostri namespace, qualunque cambiamento del protocollo vi costringerà a riscrivere mezza soluzione. Meglio considerare gli eventi come una mini‑specifica vostra sopra MCP: potete basarvi sui metodi esistenti (notifications/progress, resources/updated), ma progettate gli eventi di business (notifications/job/*) nel vostro namespace e manteneteli relativamente indipendenti.

Errore n. 6: Nessun collegamento tra eventi e UX.
A volte il team crea un bel modello a eventi sul backend, ma non lo porta nel widget: job.progress esiste nei log, ma la UI mostra uno spinner solitario per 40 secondi. In uno scenario del genere l’utente non crede né a MCP, né all’IA. Progettando gli eventi, pensate sempre a quale effetto UI concreto volete ottenere: barra di avanzamento, fasi, risultati parziali. Gli eventi MCP servono non per il protocollo in sé, ma per un comportamento dell’applicazione comprensibile.

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