1. MCP i JSON‑RPC: „nudny” fundament, który trzeba zrozumieć raz
W poprzednim wykładzie mówiliśmy o tym, po co w ogóle jest MCP i jak wpisuje się w stos Apps SDK. W tym wykładzie zawężymy fokus do najbardziej „nudnej” warstwy — formatu wiadomości MCP, abyście mogli pewnie czytać surowe logi JSON i rozumieć, co dokładnie ChatGPT wysyła waszemu serwerowi i co ten odpowiada.
MCP używa JSON‑RPC 2.0 jako transportu danych: wszystkie żądania, odpowiedzi i powiadomienia to zwykłe obiekty JSON z przewidywalnym schematem.
Czyli zamiast „każda usługa wymyśla własny format” jest bazowy kontrakt:
- żądanie ma obowiązkowe pole jsonrpc (zwykle "2.0"), unikalne id, tekstową nazwę metody method oraz obiekt params z parametrami;
- odpowiedź wiąże się z żądaniem po id i zawiera albo result, albo error;
- powiadomienia (notifications) są podobne do żądań, ale bez id, i nie będzie na nie odpowiedzi.
Wygląda to mniej więcej tak:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/list",
"params": {
"cursor": null
}
}
To jest request. A odpowiedź w przypadku sukcesu:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"tools": [],
"nextCursor": null
}
}
Jeśli przyszło wam do głowy „przecież to zwykły RPC” — tak właśnie jest. MCP po prostu precyzuje, jakie dokładnie metody istnieją (tools/list, tools/call, resources/list, prompts/list, …) i w jakim formacie oczekują parametrów oraz zwracają dane.
Ważne, by to poczuć: JSON‑RPC to szkielet „żądanie–odpowiedź–powiadomienie”. MCP to „jakie dokładnie żądania istnieją i co mają w środku”.
2. Request: jak MCP prosi o wykonanie akcji
Zacznijmy od żądań. One zawsze idą w stronę „ktoś chce coś zrobić”. Zwykle jest to klient → serwer (ChatGPT → wasz serwer MCP), ale MCP dopuszcza również żądania odwrotne, gdy serwer prosi klienta o sampling lub elicitation. W tym wykładzie interesuje nas przede wszystkim klasyczny wariant: klient prosi serwer.
Każdy MCP‑request ma trzy kluczowe pola:
- jsonrpc — wersja protokołu JSON‑RPC, zwykle "2.0".
- id — identyfikator żądania; dowolny typ JSON, ale w praktyce najczęściej liczba albo tekst. Najważniejsze, by dla aktywnych żądań id były unikalne.
- method — łańcuch znaków w postaci "tools/list" albo "tools/call". MCP specyfikuje zestaw dozwolonych metod.
Jest też obiekt params, w którym znajdują się parametry konkretnej metody.
Przykład: żądanie listy narzędzi
Załóżmy, że ChatGPT właśnie podłączył się do waszego serwera MCP i chce się dowiedzieć, jakie tools może wywołać. Wyśle żądanie mniej więcej takiej postaci:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"cursor": null
}
}
Pole cursor służy do stronicowania — jeśli narzędzi jest dużo, serwer może zwracać je porcjami.
W naszej aplikacji ćwiczebnej (dobór prezentów) będzie tu na razie nudno: jedno–dwa narzędzia, ale protokół pozostaje ten sam. Traktujcie to na razie jako intuicyjny przykład; formalne struktury omówimy później w rozdziale o tools.
Przykład: wywołanie narzędzia (tools/call)
Teraz coś ciekawszego. Załóżmy, że mamy już MCP‑tool suggest_gifts, który planujecie zaimplementować w wykładzie o serwerze MCP. Oczekuje on parametrów:
- occasion — okazja (Birthday, Wedding, …),
- budget — liczba w dolarach,
- recipient — tekst z opisem, komu dajemy prezent.
ChatGPT, decydując się skorzystać z tego narzędzia, uformuje MCP‑żądanie:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "birthday",
"budget": 100,
"recipient": "friend who loves board games"
}
}
}
Zwróćcie uwagę na kilka detali.
Po pierwsze, nazwa narzędzia bierze się z tego, co zadeklarowaliście po stronie serwera (server.registerTool("suggest_gifts", …)). Po drugie, obiekt arguments musi odpowiadać JSON Schema, którą dołączycie w opisie narzędzia.
Jeśli GPT spróbuje wysłać argumenty niezgodnie ze schematem (na przykład, budget: "sto dolarów"), serwer ma prawo zwrócić błąd na poziomie protokołu lub logiki biznesowej — zależnie od implementacji. Na razie najważniejsze jest uchwycić ogólną formę takiego żądania; w rozdziale o tools poniżej spojrzymy na te same wiadomości bardziej systemowo.
Requests dla zasobów i promptów
Analogicznie wyglądają żądania do zasobów i promptów. Specyfikacja MCP określa metody:
- resources/list — wypisać dostępne zasoby;
- resources/read (albo resources/get) — odczytać konkretny zasób po URI;
- prompts/list — pobrać listę dostępnych promptów;
- prompts/get — pobrać treść konkretnego promptu.
Przykład żądania odczytu zasobu z katalogiem prezentów:
{
"jsonrpc": "2.0",
"id": 15,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
Na razie można zapamiętać dwie rzeczy. Po pierwsze, dla każdego prymitywu istnieją metody */list oraz */get/*/read. Po drugie, nazwa metody zawsze znajduje się w polu tekstowym method, a cała zawartość — w obiekcie params.
3. Reply: jak MCP odpowiada — result i error
Odpowiedź (reply) jest zawsze powiązana z żądaniem przez pole id. To jak correlationId w wielu systemach rozproszonych: patrzycie w logi i widzicie, że żądanie z id=7 otrzymało odpowiedź z id=7, czyli to ta sama para.
JSON‑RPC ustanawia prostą zasadę: w odpowiedzi jest albo result, albo error, ale nie oba naraz. MCP ponad to doprecyzowuje strukturę result dla różnych metod (tools/list, tools/call itd.) i rekomenduje kody błędów.
Udana odpowiedź (result)
Spójrzmy na przykład udanej odpowiedzi na tools/call naszego suggest_gifts. Serwer wszystko wykonał, znalazł odpowiednie prezenty i zwraca listę w polu result:
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "Here are some gift ideas for your friend..."
}
],
"structuredContent": {
"gifts": [
{ "name": "Board game: Catan", "price": 45 },
{ "name": "Dice set", "price": 20 }
]
},
"isError": false
}
}
Tu ważnych jest kilka momentów.
- Po pierwsze, content i structuredContent to te części odpowiedzi MCP‑tools, które już widzieliście w Apps SDK. Model używa tekstu z content, a wasz widżet ładnie renderuje dane ze structuredContent.
- Po drugie, flaga isError dotyczy wyniku biznesowego. Z punktu widzenia protokołu wszystko przebiegło pomyślnie: JSON jest poprawny, metoda istnieje, argumenty zostały przetworzone. Ale logika biznesowa może powiedzieć: „nie znalazłem żadnego pomysłu na prezent — z punktu widzenia UX uznajemy to za błąd”. Wtedy ustawiasz isError: true i opisujesz problem w content.
- Po trzecie, specyfikacja MCP dla różnych metod (tools/list, tools/call, */list, */get) szczegółowo opisuje, jakie pola powinny się znaleźć w result. Na przykład dla tools/list serwer zwraca tablicę opisów narzędzi z nazwami, tytułami, opisami i JSON Schema wejściowych argumentów.
Odpowiedź błędna (error)
Jeśli coś poszło nie tak na poziomie protokołu lub serwera, zamiast result zwracany jest obiekt error. Zwykle ma on:
- code — numeryczny kod błędu;
- message — opis zrozumiały dla człowieka;
- data — opcjonalne dodatkowe dane (stack trace, szczegóły, …).
Przykład: model wywołał nieistniejącą metodę:
{
"jsonrpc": "2.0",
"id": 99,
"error": {
"code": -32601,
"message": "Method not found: tools/col"
}
}
Kod -32601 — klasyczny w JSON‑RPC „method not found”.
Istnieje subtelna, ale ważna granica między dwoma typami błędów.
Błąd protokołu — gdy naruszone są reguły MCP/JSON‑RPC: nieznana metoda, nieprawidłowy typ w polu params, niepoprawny JSON. Wtedy zasadnie jest zwrócić error na najwyższym poziomie.
Błąd biznesowy — gdy protokół jest zachowany, ale sama operacja nie powiodła się z przyczyny domenowej: pusty katalog, brak praw do konkretnego zasobu, niepoprawny identyfikator biznesowy. Wtedy MCP zazwyczaj rekomenduje zwrócić poprawny result, ale oznaczyć go jako isError: true i opisać problem w treści.
To rozróżnienie bardzo pomaga ChatGPT i narzędziom debugowania: patrząc w logi, od razu widzicie, czy była to techniczna awaria, czy świadoma odmowa logiki biznesowej.
4. Notifications: jednokierunkowe wiadomości
Powiadomienie (notification) — to „list bez oczekiwania odpowiedzi”. W JSON‑RPC powiadomienia wyglądają jak zwykłe żądania bez pola id. Klient nie powinien na nie odsyłać reply.
W MCP powiadomienia są używane dla zdarzeń: zmiany list tools/resources/prompts, postęp długich operacji, logi i tym podobne.
Najprostszy przykład, na który na pewno traficie — powiadomienie o tym, że lista narzędzi uległa zmianie. Specyfikacja MCP dla tools opisuje capability listChanged i powiadomienie tools/list_changed, które serwer wysyła, jeśli zestaw dostępnych tools się zmienił.
Powiadomienie może wyglądać tak:
{
"jsonrpc": "2.0",
"method": "tools/list_changed",
"params": {
"reason": "New tool 'suggest_gift_cards' was added"
}
}
Odpowiedź nie jest potrzebna. Klient, otrzymawszy takie powiadomienie, może zdecydować: „aha, trzeba ponownie wywołać tools/list i odświeżyć cache narzędzi”.
Inne typowe powiadomienia MCP (omówimy je szczegółowo w module o strumieniach i zdarzeniach):
- zdarzenia postępu (notifications/progress) dla długich operacji;
- logi serwera (notifications/logging/message);
- zmiany zasobów (resources/list_changed) i promptów (prompts/list_changed).
Na razie ważne jest jedno: powiadomienie = żądanie bez id i bez oczekiwanej odpowiedzi. Jeśli widzicie w logach JSON bez id, to najprawdopodobniej jest to notification.
Insight
Doświadczalnie ustalono, że ChatGPT App ignoruje wysyłane do niego wiadomości (MCP‑notification). Jednak biorąc pod uwagę, że ChatGPT Apps jest dopiero na początku rozwoju, prawdopodobieństwo pełnej obsługi wszystkich stron protokołu MCP w niedalekiej przyszłości jest bardzo duże. Dlatego polecam mimo wszystko poznać tę stronę protokołu MCP.
5. Jak wyglądają tools/resources/prompts w wiadomościach
Teraz najciekawsze: jak dokładnie wewnątrz wiadomości MCP opisane są te tools, resources i prompts, o których tyle mówimy.
Tools: opis i wywołanie
Na poziomie protokołu dla tools istnieją dwa główne procesy:
- discovery — klient dowiaduje się, jakie narzędzia istnieją;
- invocation — klient wywołuje konkretne narzędzie.
Widzieliśmy już tools/list i tools/call powyżej. Teraz spójrzmy na nie bardziej systemowo: jakie procesy pokrywają i co dokładnie zwraca się w result.
5.1.1. Lista narzędzi — tools/list
Widzieliśmy już request dla tools/list. Spójrzmy na strukturę odpowiedzi. Specyfikacja MCP mówi: w result.tools powinna wrócić tablica obiektów, z których każdy opisuje jedno narzędzie. Narzędzie musi mieć:
- name — unikalną nazwę, po której później będzie wywoływane tools/call;
- title — krótki tytuł (widoczny i dla człowieka, i dla modelu);
- description — bardziej rozbudowany opis tego, co robi narzędzie, jakbyście tłumaczyli to koledze;
- inputSchema — JSON Schema dla argumentów narzędzia.
Dla naszego suggest_gifts odpowiedź tools/list może wyglądać tak (mocno uproszczona):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas for a given occasion and budget.",
"inputSchema": {
"type": "object",
"properties": {
"occasion": { "type": "string" },
"budget": { "type": "number" },
"recipient": { "type": "string" }
},
"required": ["occasion", "budget"]
}
}
],
"nextCursor": null
}
}
Jeśli w Apps SDK pisaliście już inputSchema przy rejestracji narzędzia, to w praktyce widzieliście ten obiekt „z góry” — jako obiekt TypeScript. MCP po prostu przekazuje go po protokole klientowi.
5.1.2. Wywołanie narzędzia — tools/call
Format wywołania już poruszyliśmy. Specyfikacja MCP opisuje, że params musi zawierać:
- name — nazwę narzędzia;
- arguments — obiekt zgodny z inputSchema.
Na przykład:
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "wedding",
"budget": 150,
"recipient": "coworker from marketing"
}
}
}
A w odpowiedzi serwer zwraca result z content, structuredContent i opcjonalnie _meta (np. ze wskazaniem openai/outputTemplate, jeśli chcecie powiązać to narzędzie z konkretnym widżetem).
Ta para tools/list → tools/call to bazowy cykl pracy MCP‑tools: najpierw discovery, potem użycie.
Resources: dane z adresem
Resources w MCP to dowolne fragmenty danych, do których klient może odwoływać się przez URI: pliki, rekordy bazy, konfiguracje, katalogi itd.
Mają standardowy zestaw operacji:
- resources/list — aby dowiedzieć się, jakie zasoby istnieją;
- resources/read — aby odczytać konkretny zasób (lub jego część).
Wyobraźmy sobie zasób gift_catalog, który opisuje bazowy katalog prezentów: kategorie, marki, minimalne i maksymalne ceny. Serwer może zadeklarować go z URI "mcp://gift-server/resources/gift_catalog".
Odpowiedź na resources/list może wyglądać tak (uproszczona):
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"resources": [
{
"uri": "mcp://gift-server/resources/gift_catalog", // po prostu unikalny ciąg. mcp to nie protokół.
"name": "gift_catalog",
"description": "Base catalog of gifts with categories and prices",
"mimeType": "application/json"
}
],
"nextCursor": null
}
}
A odczyt zasobu — resources/read:
{
"jsonrpc": "2.0",
"id": 4,
"method": "resources/read",
"params": {
"uri": "mcp://gift-server/resources/gift_catalog"
}
}
Odpowiedź może zawierać samą treść i metadane:
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"contents": [
{
"uri": "mcp://gift-server/resources/gift_catalog",
"mimeType": "application/json",
"text": "{\"categories\":[\"boardgames\",\"books\"]}"
}
]
}
}
Główna idea: zasób to adresowalne dane, a tools to operacje. MCP sprawia, że jedno i drugie jest jawne w protokole.
Prompts: wielokrotnego użytku szablony
Prompts to „gotowe podpowiedzi” lub szablony, które serwer może udostępnić klientowi. MCP traktuje je jako prymityw, który ma:
- nazwę;
- ludzko czytelny tytuł/opis;
- zawartość (często szablon system‑promptu albo zestaw few‑shot przykładów).
I, przewidywalnie, są dwie metody:
- prompts/list — dowiedzieć się, jakie prompty są dostępne;
- prompts/get — pobrać zawartość jednego promptu.
Na przykład chcecie zadać szczególny styl generowania wiadomości z życzeniami razem z prezentem. Wtedy w serwerze MCP można zadeklarować prompt gift_congrats_style.
Odpowiedź na prompts/list może wyglądać tak:
{
"jsonrpc": "2.0",
"id": 10,
"result": {
"prompts": [
{
"name": "gift_congrats_style",
"description": "Style guide for birthday congratulations in a friendly tone"
}
]
}
}
A prompts/get zwróci już sam tekst (lub ustrukturyzowaną zawartość), który klient może następnie przekazać do LLM jako część system‑promptu. Przykład takiego żądania i odpowiedzi:
{
"jsonrpc": "2.0",
"id": 11,
"method": "prompts/get",
"params": {
"name": "gift_congrats_style"
}
}
{
"jsonrpc": "2.0",
"id": 11,
"result": {
"prompt": {
"name": "gift_congrats_style",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are a friendly assistant that writes short, warm birthday congratulations..."
}
]
}
]
}
}
}
6. Jak to się łączy z Apps SDK i naszym widżetem
Teraz MCP‑JSON może wciąż wyglądać nieco przyciężko. Powiążmy to z tym, co już robiliście przez Apps SDK.
Przypomnijmy, we frontendzie widżetu możecie mieć taki kod:
// wewnątrz komponentu React w piaskownicy ChatGPT
async function fetchGifts() {
const result = await window.openai.callTool("suggest_gifts", {
occasion: "birthday",
budget: 50,
recipient: "friend who loves sci-fi"
});
console.log(result);
}
Na poziomie Apps SDK to wygodna funkcja, która:
- zna URL serwera MCP (z konfiguracji aplikacji);
- umie po nazwie suggest_gifts znaleźć opis narzędzia;
- pakuje wasze wywołanie w MCP‑request tools/call;
- wysyła je wybranym transportem (HTTP/SSE);
- czeka na MCP‑reply, rozpakowuje result i zwraca go wam jako result w JavaScript.
Jeśli narysować to jako schemat, wyjdzie mniej więcej tak:
sequenceDiagram
participant Widget
participant AppsSDK as Apps SDK
participant MCP as MCP-serwer
Widget->>AppsSDK: window.openai.callTool("suggest_gifts", {...})
AppsSDK->>MCP: JSON { id:7, method:"tools/call", params:{...} }
MCP-->>AppsSDK: JSON { id:7, result:{ content, structuredContent } }
AppsSDK-->>Widget: result (ToolOutput)
Widget->>Widget: setState(toolOutput)
Zrozumienie formatu MCP daje wam dwie świetne umiejętności.
Po pierwsze, możecie sensownie patrzeć na surowe logi MCP (np. w MCP Inspector, o którym będzie osobny wykład) i widzieć: jaki dokładnie tools/call poleciał, jakie były w nim argumenty, co wróciło w result albo error.
Po drugie, projektując narzędzia i zasoby możecie myśleć nie tylko w kategoriach typów TypeScript, ale też w kategoriach schematów MCP: jak to będzie wyglądało w JSON, na ile to wygodne dla innych klientów (np. agentów, którzy również mogą podłączyć się do waszego serwera MCP).
7. Mini‑praktyka: czytamy i „naprawiamy” MCP‑JSON
Aby format MCP stał się „swój”, najlepiej raz rozebrać ręcznie na części parę wiadomości. Weźmy przykład pełnego dialogu tools/list → tools/call → rezultat.
Klient chce listę narzędzi
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
Co widzimy:
- to request (jest id);
- metoda tools/list, więc mowa o discovery narzędzi;
- parametry puste, bez stronicowania.
Serwer odpowiada:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "suggest_gifts",
"title": "Gift ideas generator",
"description": "Suggests gift ideas",
"inputSchema": { "type": "object", "properties": { "occasion": { "type": "string" } } }
}
]
}
}
Od razu widać, że to odpowiedź na to samo żądanie (to samo id: 1), protokół przeszedł poprawnie (result jest, error brak), i teraz klient wie, że istnieje narzędzie suggest_gifts.
Klient wywołuje narzędzie
Dalej klient robi tools/call:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "suggest_gifts",
"arguments": {
"occasion": "anniversary"
}
}
}
Jeśli serwer oczekuje jeszcze budget, ale model go nie podał, serwer może:
- albo zwrócić błąd protokołu (np. error z kodem „invalid params”);
- albo przyjąć wartość domyślną (np. użyć średniego budżetu) i zwrócić poprawny result.
W kategoriach, które wprowadziliśmy wyżej, pierwszy wariant to błąd protokołu (error na najwyższym poziomie), drugi — to już obszar logiki biznesowej: wciąż zwracacie poprawny result i decydujecie, czy taką sytuację traktować jako błąd biznesowy (isError: true) czy normalne zachowanie.
Odpowiedź w przypadku błędnych argumentów mogłaby wyglądać tak:
{
"jsonrpc": "2.0",
"id": 2,
"error": {
"code": -32602,
"message": "Missing required property 'budget' in arguments"
}
}
Znów odróżniamy to od błędu biznesowego: protokół został naruszony (argumenty nie odpowiadają schematowi), więc tutaj właściwy jest error.
Popsuty przykład: szukamy buga
Oto JSON, który czasem pojawia się u początkujących:
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"tool": "suggest_gifts",
"args": {
"occasion": "birthday",
"budget": 100
}
}
}
Na pierwszy rzut oka wygląda wiarygodnie, ale jeśli porównacie to ze specyfikacją MCP, zauważycie, że pola tool i args nie pokrywają się z oczekiwanymi name i arguments.
Klient/serwer MCP‑SDK najpewniej nigdy nie wygeneruje takiego JSON‑a, ale jeśli, nie znając specyfikacji, integrujecie się ręcznie, podobny bug jest całkiem realny. Właśnie dlatego w kursie rozbieramy protokół „wprost”, a nie tylko otoczki SDK.
8. Typowe błędy przy pracy z wiadomościami MCP
Błąd nr 1: mieszanie błędów protokołu i błędów biznesowych.
Często deweloperzy z przyzwyczajenia wszystko, co „poszło nie tak”, zawijają w górnopoziomowy error — i brak zasobu, i nieprawidłowe argumenty, i awarie bazy. W kontekście MCP warto rozdzielać: jeśli struktura JSON i schemat wywołania są naruszone (nie ta metoda, nie te pola, nieprawidłowe typy), to powód, by zwrócić error. Jeśli jednak narzędzie po prostu nie zdołało wykonać operacji domenowej (brak prezentów dla takiego budżetu, użytkownik nie znaleziony), lepiej zwrócić poprawny result z isError: true i zrozumiałą wiadomością w content. Wtedy model ChatGPT i debugery będą mogły poprawnie odróżniać „zepsuł się kanał komunikacji” od „serwer świadomie odmówił”.
Błąd nr 2: ignorowanie pola id i korelacji żądań.
Czasem w logach serwera MCP można zobaczyć ręczne wypisy bez id albo z powtarzającymi się wartościami id dla różnych aktywnych żądań. W jednowątkowym hello‑world to jeszcze może jakoś działać, ale gdy tylko pojawiają się wywołania równoległe albo retry, robi się trudno zrozumieć, która odpowiedź dotyczy którego żądania. JSON‑RPC specjalnie wymaga unikalnego id na czas życia żądania, a MCP polega na tej zasadzie. Jeśli używacie oficjalnych SDK, o id można nie myśleć, ale gdy tylko piszecie transport lub logowanie sami, nie zapominajcie zapisywać i wypisywać id — to pierwsza rzecz, po której będziecie debugować dziwne bugi.
Błąd nr 3: niestabilne struktury result dla tej samej metody.
Kusi, by „trochę” zmieniać format odpowiedzi w zależności od sytuacji: czasem zwrócić tablicę prezentów, czasem obiekt z jednym stringiem, czasem w ogóle tylko text bez structuredContent. Model może i zniesie takie sztuczki, ale wasze widżety i inni klienci MCP — raczej nie. Specyfikacja MCP dla każdej metody opisuje przewidywalną strukturę result; trzymajcie się jej. Jeśli potrzebny jest inny format, lepiej zadeklarować osobne narzędzie lub wersję, niż zmieniać schemat w locie.
Błąd nr 4: nadmiarowe lub brakujące pola w params.
Typowy problem własnych implementacji — dodać do params coś, czego MCP nie oczekuje, albo pominąć pole obowiązkowe. Na przykład wysłać toolName zamiast name w tools/call, albo resourceId zamiast uri w resources/read. MCP‑SDK zwykle takie rzeczy waliduje i rzuca zrozumiałe wyjątki, ale jeśli pracujecie bliżej protokołu, można długo szukać, czemu „serwer mnie nie rozumie”. Dobry trik — trzymać obok handlera przykład poprawnego JSON‑żądania ze specyfikacji lub z logów działającego klienta i porównywać z tym, co wysyłacie.
Błąd nr 5: próba używania notifications jako „drugiego kanału odpowiedzi”.
Czasem deweloperzy, zobaczywszy notifications, zaczynają wysyłać wyniki operacji przez powiadomienia zamiast zwykłych replies: „skoro już jesteśmy w MCP i mamy SSE, to wypchnijmy wszystko powiadomieniami”. Problem w tym, że powiadomienia JSON‑RPC z definicji nie są powiązane z konkretnym id i nie są odbierane przez klienta jako odpowiedź na żądanie. W rezultacie trudniej debugować i nie da się zrozumieć, do którego wywołania narzędzi odnosi się dana wiadomość. Powiadomienia świetnie nadają się do zdarzeń (zmieniła się lista tools/resources/prompts, pojawił się nowy postęp, przyszedł log), ale nie do zwykłych odpowiedzi na tools/call i podobne.
Błąd nr 6: brak wglądu w logi MCP i inspektory.
Najbardziej ludzki błąd — próba debugowania integracji wyłącznie przez interfejs ChatGPT: „kliknąłem, coś nie przyszło, kiedyś się tym zajmę”. Dopóki nie widzicie surowych wiadomości MCP (requests, replies, notifications), trudno zrozumieć, na jakim poziomie jest problem: model nie wywołał narzędzia, Apps SDK nie dotarł do serwera MCP, serwer zwrócił nie ten JSON, czy wszystko posypało się dopiero przy renderowaniu widżetu. MCP Inspector / Jam i strukturalne logowanie wiadomości MCP — wasz najlepszy przyjaciel. Gdy raz zobaczycie na żywo tools/call i tools/list w logach, format wiadomości MCP przestanie być „magią” i stanie się zwykłą inżynierską rutyną.
GO TO FULL VERSION