CodeGym /Kursy /ChatGPT Apps /Model zdarzeń w MCP: typy powiadomień, format wiadomości,...

Model zdarzeń w MCP: typy powiadomień, format wiadomości, idempotentność

ChatGPT Apps
Poziom 13 , Lekcja 0
Dostępny

1. Po co w ogóle są potrzebne zdarzenia MCP

Do tej pory niemal cała komunikacja między ChatGPT a waszym backendem wyglądała jak RPC: model wywołał narzędzie, to coś zrobiło, zwróciło wynik – gotowe. To wygodne, dopóki operacje są krótkie: 200–500 ms, co najwyżej parę sekund.

Ale gdy tylko pojawia się coś długotrwałego – analiza dużego pliku z preferencjami pracowników dla GiftGenius, agregacja rekomendacji z wielu zewnętrznych API, przeliczenie dużego feedu – wszystko zaczyna być kłopotliwe. Limity czasu HTTP, restarty funkcji, „wieczne” spinnery, a użytkownik siedzi i zgaduje: „to jeszcze żyje czy już umarło?”.

Właśnie tu zaczyna się model zdarzeń. Zamiast trzymać jedno długie wywołanie narzędzia, uruchamiacie zadanie, dostajecie jobId, a potem serwer z własnej inicjatywy wysyła zdarzenia: start, postęp, zakończone, błąd. Te zdarzenia w MCP są realizowane jako powiadomienia JSON-RPC (notifications) – jednokierunkowe wiadomości bez id, na które nie oczekuje się odpowiedzi.

Ważne: zdarzenie to nie „console.log na kablu”. To formalna wiadomość protokołu o określonym schemacie, którą wasz UI (widżet) i/lub agent powinien umieć obsłużyć równie zdyscyplinowanie, jak wynik wywołania narzędzia.

Przypomnienie: typy wiadomości w MCP

Zanim pójdziemy dalej, krótko odświeżmy, jakie w ogóle są wiadomości w MCP.

Jeśli odrzucić warstwę marketingową, MCP opiera się na JSON-RPC 2.0. Są tam trzy podstawowe typy wiadomości: żądania, odpowiedzi i powiadomienia.

Zamiast wypisywać je w punktach, spójrzmy na krótką tabelę porównawczą:

Typ Pole id Kto inicjuje Czy oczekuje się odpowiedzi? Przykład w MCP
Request jest Zazwyczaj klient (ChatGPT) Tak Wywołanie narzędzia tools/call
Response jest Serwer MCP To właśnie odpowiedź Wynik tools/call
Notification nie Klient lub serwer Nie notifications/progress, resources/updated, logging/message

Zdarzenia MCP żyją właśnie w trzeciej kategorii: to notifications. Cechy charakterystyczne:

  • brak id na najwyższym poziomie – żadnego result ani error w odpowiedzi nie przyjdzie;
  • inicjator nie czeka na ACK – „wyślij i zapomnij” na poziomie protokołu;
  • niezawodność opiera się nie na potwierdzeniach, lecz na idempotentności obsługujących i polityce ponownych wysyłek.

Ważne ograniczenie: zdarzenia MCP nie latają „w losowym momencie gdzieś w kosmosie”. Żyją wewnątrz ustanowionego połączenia MCP na konkretnym transporcie. Najczęściej jest to strumień podobny do SSE (szczegóły transportu i jego warianty omówimy w osobnym wykładzie).

2. Czym jest „zdarzenie MCP” w praktyce

Formalnie zdarzenie MCP to powiadomienie JSON-RPC (notification), czyli obiekt w rodzaju:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/progress",
  "params": {
    "jobId": "job_123",
    "percentage": 30,
    "stage": "Szukamy opcji w katalogu",
    "eventId": "evt_abc123",
    "timestamp": "2025-11-21T10:15:00Z"
  }
}

Oto kilka ważnych punktów:

  1. W polu method kodujemy typ zdarzenia i jego „przestrzeń nazw”. MCP określa już szereg standardowych metod notifications/... dla logów, postępu i zmian zasobów, ale możecie i powinniście dodawać własne metody biznesowe, jak notifications/job/progress czy notifications/job/completed.
  2. Wszystkie dane biznesowe znajdują się w params. Tam będziemy przechowywać identyfikatory zadań (jobId), unikalne identyfikatory zdarzeń (eventId), czas (timestamp), komunikaty czytelne dla człowieka i inne.
  3. Brak pola id na najwyższym poziomie – dlatego to notification. Protokół nie przewiduje odpowiedzi. Jeśli serwer chce się upewnić, że „został zrozumiany”, może wysłać kolejne zdarzenie albo poczekać na działania reaktywne klienta (np. nowe żądanie). Ale ACK w kategoriach JSON-RPC nie ma.

Na poziomie modelu mentalnego można myśleć tak: wywołanie narzędzia tools/call to „list, na który czekacie z odpowiedzią”, a zdarzenie – to „powiadomienie od bota Slack: „Zadanie w tle nr 123 zakończone””.

3. Taksonomia zdarzeń: jakie są powiadomienia

Jeśli po prostu pozwolić „wysyłajcie dowolne JSON-y jako notifications”, po dwóch tygodniach system zamienia się w śmietnik: nazwy zdarzeń różne, pola pływają, UI nie wie, co z tym robić. Dlatego warto uzgodnić niewielką taksonomię.

Poniżej – jedna z wygodnych klasyfikacji, która dobrze spina się ze specyfikacją MCP i realnymi przypadkami ChatGPT Apps.

Zdarzenia cyklu życia zadania (Job Lifecycle)

To zdarzenia odzwierciedlające kluczowe przejścia stanów zadania. Zwykle zadanie ma automat stanów (state machine) w rodzaju pendingrunning → (completed | failed | canceled).

Typowe zdarzenia:

  • job.created – zadanie zarejestrowano;
  • job.started – worker rozpoczął pracę;
  • job.completed – zadanie zakończyło się pomyślnie;
  • job.failed – zadanie zakończyło się błędem;
  • job.canceled – zadanie anulowane przez użytkownika.

Przykład job.completed dla GiftGenius:

{
  "jsonrpc": "2.0",
  "method": "notifications/job/completed",
  "params": {
    "eventId": "evt_gg_100",
    "jobId": "giftjob_42",
    "timestamp": "2025-11-21T10:20:00Z",
    "summary": "Dobór prezentów zakończony",
    "resultResourceId": "resource:gifts:giftjob_42"
  }
}

Tutaj resultResourceId może wskazywać na zasób MCP, który później odczyta widżet lub agent.

Zdarzenia postępu (Progress Updates)

To „małe kroki” wewnątrz cyklu życia: nie zmieniają statusu końcowego, ale dają użytkownikowi poczucie, że coś się dzieje.

Typowe zdarzenie 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": "Filtrujemy prezenty według budżetu",
    "etaSeconds": 25
  }
}

Ważne, by percentage rozsądnie rósł w stronę 100, a nie skakał wte i wewte. Wybierzcie jedną nazwę pola postępu (np. percentage) i używajcie jej we wszystkich zdarzeniach. W oficjalnym narzędziu postępu MCP również obowiązuje zasada: postęp tylko rośnie.

Zdarzenia aktualizacji danych (Resource/Data events)

Czasem użytkownikowi nie jest nawet ważny konkretny jobId. Ważniejsze, że jakaś encja się zmieniła: zaktualizowano feed produktów, utworzono nowy snapshot raportu, ponownie wygenerowano profil personalny.

W MCP są już standardowe powiadomienia poziomu resources/updated, resources/list_changed i podobne, które sygnalizują klientowi: „wczytaj ponownie listę zasobów, coś się zmieniło”.

Dla GiftGenius może to wyglądać tak:

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

Widżet, otrzymawszy takie zdarzenie, może na przykład podświetlić przycisk „Odśwież listę prezentów”.

Zdarzenia UX i systemowe

Są też zdarzenia, które nie są stricte biznesowe, ale ważne dla UX lub diagnostyki:

  • komunikaty logów logging/message – standardowe powiadomienie MCP dla logów;
  • heartbeat/ping – okresowe „żyję” od serwera;
  • ostrzeżenia o degradacji: np. „zewnętrzne API teraz spowalnia, wyniki mogą przychodzić wolniej”.

Takie zdarzenia są przydatne do monitoringu i debugowania; czasem można je „uczynić widocznymi” w UI, pokazując użytkownikowi, że system nie umarł, tylko jest zajęty.

4. Struktura zdarzenia: pola obowiązkowe i payload

Zdarzenie to taki sam obiekt API jak żądanie do narzędzia. Trzeba je zaprojektować. Dobrą praktyką jest uzgodnienie bazowego zestawu pól.

Koncepcyjnie warto dzielić zdarzenie na trzy części: metadane, korelację i ładunek (payload).

Przykład ogólnej formy:

{
  "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": "Porównujemy opinie",
      "etaSeconds": 15
    }
  }
}

W tej strukturze można wyróżnić:

  • eventId – unikalny identyfikator zdarzenia. Potrzebny do deduplikacji po stronie klienta;
  • type – logiczna nazwa zdarzenia (można dublować/normalizować method);
  • timestamp – kiedy zdarzenie zostało wygenerowane przez serwer;
  • jobId lub inny identyfikator korelacji – aby wiedzieć, czego dotyczy zdarzenie;
  • payload – właściwe dane. Dla każdego typu zdarzenia ma własny kształt.

W realnym systemie prawie na pewno zechcecie formalnie opisać te struktury przez JSON Schema albo przynajmniej typy TypeScript, by i serwer, i klient walidowały wiadomości. Niektóre zespoły używają do tego formatu inspirowanego CloudEvents: tam również są standardowe pola id, source, type, time itp.

Kluczowa idea jest prosta: zdarzenie powinno być maszynowo czytelne i spójne – bez niespodzianek typu „czasem pole nazywa się jobId, czasem job_id, a czasem go nie ma”.

W przykładach poniżej, aby nie przeładowywać kodu, częściej użyjemy „spłaszczonej” wersji: wszystkie dane zdarzenia leżą bezpośrednio w params bez zagnieżdżonego payload, a pole type czasem pominiemy, jeśli jego rolę pełni method. Zasada pozostaje ta sama: każde zdarzenie ma stabilne metadane (eventId, jobId, timestamp) i przewidywalny ładunek.

5. Idempotentność zdarzeń: po co i jak

Teraz najważniejsze słowo tego wykładu – idempotentność.

Idempotentność obsługującego zdarzenie oznacza, że jeśli to samo zdarzenie zostanie przetworzone raz lub dziesięć razy, stan końcowy systemu pozostanie poprawny. W systemach rozproszonych z siecią i ponownymi próbami to dosłownie kwestia życia i śmierci.

Dlaczego to samo zdarzenie może przyjść kilka razy?

Powodów jest wiele: od banalnych zerwań połączenia i ponownych podłączeń po retraje po stronie serwera, który „na wszelki wypadek” wysłał powiadomienie jeszcze raz. Przy użyciu protokołów strumieniowych (np. gdy serwer sam pcha zdarzenia w otwarte połączenie, jak SSE – więcej o tym w osobnym wykładzie o transporcie) to klasyka: klient połączył się ponownie z Last-Event-ID, serwer dośle pominięte zdarzenia i niektóre z nich klient zobaczy drugi raz.

Jeśli wasz obsługujący nie jest idempotentny, zaczynają się dziwactwa:

  • zdarzenie job.completed powoduje podwójne naliczenie bonusów lub dwukrotną zmianę statusu zamówienia;
  • zdarzenie resource.updated sprawia, że widżet za każdym razem „dodaje” kafelki, dublując je w UI;
  • powtórne job.progress straszą użytkowników, jeśli pasek postępu zaczyna skakać w przód i w tył.

Poprawna strategia działa w dwóch warstwach: generowanie zdarzeń po stronie serwera i ich obsługa po stronie klienta.

Strona serwera: stabilne identyfikatory i automat stanów

Serwer powinien:

  • generować unikalny eventId dla każdego logicznego zdarzenia;
  • gwarantować, że zdarzenia jednego jobId tworzą poprawną sekwencję stanów: nie można wysłać job.failed po job.completed ani dwóch różnych job.completed z innymi wynikami.

Innymi słowy, macie faktycznie automat stanów zadania, a każde zdarzenie to dozwolone przejście.

Strona klienta: deduplikacja i „miękkie” aktualizacje

Klient (widżet, agent lub inny komponent) powinien:

  • przechowywać zbiór już przetworzonych eventId przynajmniej na czas życia bieżącego połączenia/sesji;
  • sprawdzać przed przetworzeniem: jeśli eventId już widziano, po prostu zignorować albo odświeżyć UI bez skutków ubocznych;
  • przy otrzymaniu zdarzeń zmieniających status zadania (job.completed, job.failed) upewnić się, że przejście jest dozwolone: np. jeśli zadanie już jest oznaczone jako completed, ponowne job.completed nie powinno nic zmieniać, a failed najlepiej zignorować jako niepoprawne.

Klasyczny przykład ze świata e‑commerce: obsługa webhooka potwierdzenia płatności. To samo order.paid może przyjść dwa razy; dlatego backend przechowuje paymentId i flagę „już zaksięgowane”. Nawet jeśli webhook przyleci ponownie, stan zamówienia się nie zmieni. Zdarzenia MCP należy projektować z takim właśnie myśleniem.

6. Przykład: projektujemy zdarzenia dla GiftGenius

Przenieśmy to na nasz przykładowy GiftGenius. Wyobraźmy sobie długi scenariusz: użytkownik załadował duży plik CSV z listą pracowników i ich zainteresowaniami, poprosił „dobierz pomysły prezentów dla wszystkich”. Operacja może trwać kilkadziesiąt sekund.

Rozsądny model zdarzeń można opisać tak:

  1. Użytkownik uruchamia narzędzie start_bulk_gift_analysis. Narzędzie zwraca jobId: "bulk_2025_001".
  2. Serwer MCP tworzy zadanie i niemal od razu wysyła job.started z krótkim opisem.
  3. W miarę wykonywania wysyła kilka job.progress z etapami:
    • 10 % – „Parsujemy plik i sprawdzamy format”;
    • 40 % – „Wydobywamy zainteresowania i działy”;
    • 70 % – „Dopasowujemy prezenty do kategorii”;
    • 100 % – tuż przed zakończeniem.
  4. Na końcu przychodzi job.completed z odnośnikiem do zasobu z końcowymi rekomendacjami.
  5. Jeśli coś poszło źle – zamiast completed przyjdzie job.failed z kodem błędu i, być może, wskazówką, co poprawić.

Nieformalnie tak to będzie wyglądać, ale zapiszmy to jako JSON‑schematy dla dwóch kluczowych zdarzeń job.progress i job.completed. Pseudo‑JSON Schema (uproszczona):

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

Nie musicie od razu implementować pełnej walidacji schematów, ale warto mentalnie trzymać taką strukturę: pomaga to nie „rozmazywać” pól po różnych formatach i nie zapominać o ważnych metadanych.

7. Mini‑praktyka: serwer, który wysyła zdarzenia MCP

Teraz połączymy teorię z małym kawałkiem pseudokodu TypeScript. Nie będziemy wchodzić w prawdziwe biblioteki MCP (po pierwsze, jeszcze ewoluują, po drugie, tu skupiamy się na modelu), ale narysujemy szkielet struktury.

Załóżmy, że w naszym serwerze MCP jest abstrakcja sendNotification, która potrafi wysłać powiadomienie JSON-RPC z powrotem do ChatGPT. Pseudointerfejs:

// Narzędzie do wysyłania powiadomień MCP
async function sendNotification(
  method: string,
  params: Record<string, unknown>
) {
  // Tutaj zserializowalibyście JSON i wysłali po aktywnym połączeniu MCP
}

Teraz zaimplementujemy obsługę narzędzia start_bulk_gift_analysis. Rejestruje ono zadanie, zwraca jobId, a gdzieś w tle „tyka” i wysyła postęp. W rzeczywistości byłby to worker i kolejka, ale na razie ograniczmy się do timera.

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

  // Od razu wysyłamy job.started
  await sendNotification("notifications/job/started", {
    eventId: `evt_${jobId}_started`,
    jobId,
    timestamp: new Date().toISOString(),
    summary: "Uruchomiono analizę dużej listy prezentów"
  });

  simulateJob(jobId); // „uruchamiamy” zadanie w tle

  return { jobId };
}

Symulacja zadania:

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

  const stages = [
    { percent: 10, stage: "Parsujemy CSV" },
    { percent: 40, stage: "Analizujemy zainteresowania" },
    { percent: 70, stage: "Dobieramy prezenty" },
    { percent: 100, stage: "Tworzymy wynik" }
  ];

  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: "Analiza prezentów zakończona",
    resultResourceId: `resource:gifts:${jobId}`
  });
}

Kod jest celowo prosty, ale dobrze na nim widać:

  • używamy sekwencji zdarzeń startedprogress* → completed;
  • każde zdarzenie dostaje unikalny eventId;
  • wszystkie zdarzenia są powiązane z jednym jobId.

W przyszłości, gdy dodacie prawdziwe kolejki i workery, struktura zdarzeń pozostanie w przybliżeniu taka sama – zmieni się tylko to, gdzie dokładnie wywołujecie sendNotification.

8. Klient: najprostszy idempotentny obsługujący zdarzenia

Po stronie klienta (np. w waszym widżecie Apps SDK) trzeba nauczyć się przyjmować takie zdarzenia, wiązać je z bieżącymi zadaniami i nie wariować od duplikatów.

Nie wchodząc jeszcze w transport (o tym później), wyobraźmy sobie funkcję onMcpNotification, którą wasza warstwa klienta MCP wywołuje przy każdym przychodzącym notification.

Dodajmy najprostszą deduplikację:

const processedEvents = new Set<string>();

function handleNotification(method: string, params: any) {
  const eventId = params.eventId as string | undefined;
  if (!eventId) return; // to dość dyskusyjne, ale do przykładu wystarczy

  if (processedEvents.has(eventId)) {
    // Duplikat — ignorujemy lub miękko odświeżamy 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);
  }
}

Implementacja updateJobProgress i markJobCompleted to już czysty kod React/UI:

function updateJobProgress(jobId: string, percent: number, stage: string) {
  // na przykład, wkładamy do Zustand/Redux/React state
  console.log(`Job ${jobId}: ${percent}% — ${stage}`);
}

function markJobCompleted(jobId: string, resourceId: string) {
  console.log(`Job ${jobId} zakończony, zasób: ${resourceId}`);
}

Taki obsługujący:

  • nie psuje się, jeśli zdarzenie przyszło dwa razy;
  • nie wywołuje skutków ubocznych (typu „drugi raz pokazaliśmy modal „Gotowe!””);
  • toruje drogę do bardziej złożonej logiki, np. walidacji dozwolonych przejść stanów (nie pozwalać na failed po już ustawionym completed).

W produkcyjnym kodzie najpewniej będziecie chcieli wyzerować processedEvents przy ponownym połączeniu z serwerem MCP, a także przechowywać nie tylko eventId, ale i bieżący status każdego jobId, aby przy dziwnej sekwencji zdarzeń zachowywać się rozsądniej.

Dalej ważne jest zrozumienie, jak wszystkie te zdarzenia MCP przechodzą przez agenta/widżet i zamieniają się w konkretny doświadczenie użytkownika: pasek postępu, etapy wykonywania, pojawienie się wyników końcowych. Przejdziemy do powiązania zdarzeń z run/workflow i UX.

9. Powiązanie zdarzeń, run/workflow i UX

Choć pełny moduł o workflow i agentach już był, teraz zobaczycie cały obraz. Wprowadziliśmy już rodziny zdarzeń (job.*, resource.*, systemowe); zobaczmy, jak przechodzą przez agenta/widżet i ChatGPT oraz zamieniają się w konkretne doświadczenie użytkownika.

Typowy scenariusz z długim zadaniem wygląda tak: ChatGPT wywołuje narzędzie MCP, otrzymując jobId; następnie po tym jobId serwer wysyła zdarzenia o postępie, zakończeniu lub błędzie; wasz widżet lub logika agenta na ich podstawie aktualizują UI i podejmują decyzje.

Na diagramie sekwencji można to narysować tak:

sequenceDiagram
    participant User as Użytkownik
    participant GPT as ChatGPT (model)
    participant App as Serwer MCP GiftGenius
    participant Widget as Widżet GiftGenius

    User->>GPT: "Dobierz prezenty dla 2000 pracowników"
    GPT->>App: tools.call start_bulk_gift_analysis
    App-->>GPT: response { jobId: "bulk_2025_001" }

    GPT->>Widget: ToolOutput { jobId }
    Widget->>Widget: Pokaż pasek postępu

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

    GPT->>Widget: Przekazuje zdarzenia/dane do widżetu
    Widget->>User: Aktualizuje postęp i pokazuje wynik
    

W praktyce rzeczywisty diagram będzie nieco bardziej złożony, ale myśl przewodnia jest prosta: zdarzenia MCP to „układ nerwowy” między waszymi operacjami w tle a doświadczeniem użytkownika.

10. Typowe błędy przy pracy ze zdarzeniami MCP

Błąd nr 1: „Zdarzenie = log w formacie produkcyjnym”.
Czasem deweloperzy zaczynają od tego, że po prostu przesyłają do MCP to, co wcześniej pisali w console.log. W efekcie w zdarzeniach nie ma ani eventId, ani jobId, ani porządnego timestamp, tylko półpoetyckie wiadomości „prawie skończyliśmy”. Takie podejście czyni system kruchym: trudno to parsować, nie da się deduplikować, UI nie wie, do jakiego zadania należy komunikat. Lepiej od początku projektować zdarzenia jako formalny kontrakt: jasna nazwa metody, stabilny zestaw pól, logiczny payload.

Błąd nr 2: Brak idempotentności i unikalnego eventId.
Wielu zaczyna od naiwnej idei: „przecież zdarzenia przychodzą raz”. Po tygodniu zaczyna się: przy ponownym podłączeniu klienta duplikują się powiadomienia, użytkownik dostaje to samo dwa razy, backend komercyjny podwójnie nalicza bonusy. Bez unikalnego eventId i elementarnej deduplikacji po stronie klienta prędzej czy później złapiecie poważny błąd. W systemie rozproszonym trzeba wychodzić z modelu „at-least-once delivery”: duplikaty są nieuniknione.

Błąd nr 3: Mieszanie systemowych i biznesowych zdarzeń w jeden kocioł.
Na przykład do jednego strumienia sypią się logging/message, job.progress, job.completed, resources/updated, i to wszystko bez wyraźnych rozróżnień type/method. W rezultacie warstwa UI zaczyna robić dziwne if (message.includes("gotowe")), aby zrozumieć, że zadanie się zakończyło. Lepiej wyraźnie rozdzielać: są powiadomienia systemowe (logi, heartbeat) i są zdarzenia biznesowe (job.*, resource.*) ze ściśle opisanymi schematami.

Błąd nr 4: Niespójne przejścia stanów zadania.
Zdarza się, że serwer w jednym strumieniu zdarzeń najpierw wysyła job.completed, potem nagle job.progress, potem job.failed. Dzieje się tak, gdy nie ma jawnego automatu stanów i kontroli przy emisji zdarzeń. Klientom staje się niemożliwe zrozumienie, co naprawdę się dzieje. Właściwiej opisać skończony automat stanów i nie wypuszczać zdarzeń, które go naruszają: np. po completed można co najwyżej wysłać dodatkowe informacyjne zdarzenie, ale nie przenosić zadania z powrotem do running.

Błąd nr 5: Twarde przywiązanie do konkretnych nazw metod MCP z bieżącej wersji specyfikacji.
Specyfikacja MCP wciąż się rozwija. Jeśli uzależnicie wszystko od konkretnych bieżących metod o systemowych nazwach, bez własnych przestrzeni nazw, każda zmiana protokołu zmusi was do przepisywania połowy systemu. Lepiej traktować zdarzenia jak własną mini‑specyfikację nad MCP: możecie opierać się na istniejących metodach (notifications/progress, resources/updated), ale zdarzenia biznesowe (notifications/job/*) projektować w swojej przestrzeni nazw i trzymać je względnie niezależnie.

Błąd nr 6: Brak powiązania zdarzeń z UX.
Czasem zespół robi ładny model zdarzeń na backendzie, ale nie doprowadza go do widżetu: job.progress istnieje w logach, ale UI pokazuje samotny spinner przez 40 sekund. Użytkownik w takim scenariuszu nie wierzy ani w MCP, ani w AI. Projektując zdarzenia, zawsze myślcie o konkretnym efekcie w UI: pasek postępu, etapy, wyniki cząstkowe. Zdarzenia MCP są potrzebne nie dla protokołu, lecz dla jasnego zachowania aplikacji.

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