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:
- 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.
- 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.
- 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 pending → running → (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:
- Użytkownik uruchamia narzędzie start_bulk_gift_analysis. Narzędzie zwraca jobId: "bulk_2025_001".
- Serwer MCP tworzy zadanie i niemal od razu wysyła job.started z krótkim opisem.
- 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.
- Na końcu przychodzi job.completed z odnośnikiem do zasobu z końcowymi rekomendacjami.
- 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ń started → progress* → 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.
GO TO FULL VERSION