1. Dlaczego w ogóle mówić o integracji i migracjach
Do tej pory projektowaliśmy API i narzędzia głównie tak, jak nam wygodnie. W realnym życiu prawie zawsze jest odwrotnie: już macie:
- monolit lub paczkę mikroserwisów;
- REST/GraphQL API;
- logikę biznesową, która działa na produkcji od lat.
I nagle pojawia się zadanie: „Podłączcie nasz produkt do ChatGPT przez Apps SDK i MCP”.
Przepisać wszystko pod „idealny serwer MCP” — to nie wchodzi w grę. Trzeba delikatnie „nałożyć” na istniejący świat cienką warstwę, która przełoży język waszego backendu na język ChatGPT: narzędzia, zasoby i schematy.
Drugi problem: produkt żyje. Schematy i API się zmieniają. W zwykłym froncie przynajmniej od razu dostajecie błąd TypeScript, gdy zmienicie pole. W świecie LLM‑Apps jest to bardziej zdradliwe: model będzie pewnie wysyłał stary format, tool zacznie się wywalać, a zamiast ładnie padającej kompilacji dostaniecie:
- błędy w czasie wykonania na serwerze MCP;
- halucynacje „no mniej więcej zgadłem, o co chodziło w tym polu”;
- przykre incydenty jakości.
Dlatego w tym wykładzie patrzymy na warstwę MCP+Apps jako na:
- adapter do istniejącego backendu;
- kontrakt, który trzeba utrzymywać latami;
- obiekt migracji: wersji, adnotacji, scopes i SDK.
2. Architektura integracji: MCP jako adapter nad istniejącym backendem
Obraz bazowy
Przypomnijmy stos, ale już przez pryzmat produkcji:
flowchart LR U[Użytkownik w ChatGPT] --> G[Model ChatGPT] G -->|wywołuje App| W["Widżet (Apps SDK, Next.js)"] G -->|tools.call| MCP[Serwer MCP / Gateway] MCP --> S1["Gift Service (twój istniejący serwis)"] MCP --> S2["Commerce Service (zamówienia, ACP)"]
ChatGPT komunikuje się z waszym światem nie bezpośrednio, lecz przez protokół MCP: listę tools/resources, wywołania tools/call, strumieniowanie zdarzeń.
Serwer MCP w tym układzie to dokładnie ten właśnie adapter: zna ChatGPT (JSON‑RPC, narzędzia) i wasze serwisy (REST/DB/kolejki) i tłumaczy jedno na drugie.
MCP jako Gateway/Adapter
Klasyczna sytuacja: macie już Gift Service z endpointami REST:
// Przykład istniejącego REST API
GET /api/gifts/recommendations?budget=100&occasion=birthday
POST /api/orders
Zamiast pisać nową logikę biznesową, warstwa MCP po prostu opakowuje to jako Tool:
// mcp/tools/recommendGifts.ts
import { z } from "zod";
import { server } from "./mcpServer"; // przykładowa instancja SDK
const recommendGiftsInput = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
});
server.registerTool({
name: "recommend_gifts",
description: "Dobiera pomysły na prezenty w ramach budżetu",
inputSchema: recommendGiftsInput,
async execute(args) {
const { occasion, budgetUsd } = recommendGiftsInput.parse(args);
const res = await fetch(
`https://api.myapp.com/gifts/recommendations?budget=${budgetUsd}&occasion=${occasion}`,
);
return res.json(); // ważne: zwracamy JSON wygodny zarówno dla modelu, jak i dla widżetu
},
});
Cała logika doboru prezentów pozostaje w waszym istniejącym serwisie. Warstwa MCP to „cienki tłumacz” z języka ChatGPT na język waszych API.
Czasem warstwa MCP także trasuje żądania do kilku serwisów backendowych. Wtedy staje się pełnoprawnym MCP Gateway — jego rolę omówicie szerzej w module o produkcji i sieci.
Monolith-integrated MCP vs Sidecar MCP
Są dwa podstawowe warianty, gdzie „przykręcić” tę warstwę MCP.
Tekstowo wygląda to tak:
| Wariant | Opis | Gdzie mieszka kod MCP |
|---|---|---|
| Monolith-integrated | Wszystko w jednym serwisie Next.js/Node | W trasach API Next.js lub Express |
| Sidecar MCP | Oddzielny kontener/serwis, komunikujący się z API | Oddzielna aplikacja Node/Go |
W małych projektach często wystarcza pierwszy wariant: aplikacja Next.js, deploy na Vercel, tamże trasa /mcp lub /api/mcp, i serwer MCP żyje obok pozostałych API.
Przykład (mocno uproszczony):
// app/api/mcp/route.ts (Next.js 16)
import { NextRequest } from "next/server";
import { mcpHandler } from "@/mcp/server";
export async function POST(req: NextRequest) {
const body = await req.json();
const response = await mcpHandler.handle(body); // żądanie JSON-RPC
return new Response(JSON.stringify(response), {
headers: { "content-type": "application/json" },
});
}
W poważniejszej architekturze, gdzie są liczne serwisy domenowe (Gift, Commerce, Analytics), wygodniej wynieść warstwę MCP do oddzielnego serwisu Gateway. Będzie przyjmował ruch MCP od ChatGPT i sam trasował wywołania do różnych backendów po nazwie narzędzia.
Warto pamiętać: z punktu widzenia ChatGPT i Apps SDK to wciąż jeden serwer MCP. Gdzie dokładnie się kręci — w monolicie czy jako osobny mikroserwis — to już wasza decyzja architektoniczna.
Z architekturą warstwy MCP się uporaliśmy: może mieszkać w monolicie lub jako osobny Gateway. Dalej pojawia się pytanie, co dokładnie ta warstwa przyjmuje i zwraca — i tu na scenę wchodzą schematy i kontrakty.
3. Single Source of Truth: schematy, typy i testy kontraktowe
Jeśli macie wewnętrzne DTO, zewnętrzne kontrakty REST i jeszcze schematy MCP dla narzędzi — pokusa, by „narysować schematy na oko”, jest ogromna. Rezultat przewidywalny:
- zmieniacie pole w backendzie, zapominacie zaktualizować schemę narzędzia;
- model wciąż wysyła stary format;
- dostajecie wesoły runtime‑zoo.
Rozsądna droga: zrobić jedno źródło prawdy dla struktury danych i używać go wszędzie. W świecie TypeScript bardzo wygodnie robić to przez Zod lub podobne biblioteki, które MCP SDK potrafi konwertować do JSON Schema.
Wspólna Zod‑schema dla GiftGenius
Załóżmy, że wasz serwis Gift w naszym szkoleniowym GiftGenius już używa Zod do walidacji wejścia:
// domain/gifts.ts
import { z } from "zod";
export const giftRecommendationInputSchema = z.object({
occasion: z.string().describe("Okazja: birthday, wedding itp."),
budgetUsd: z.number().int().positive(),
recipientProfile: z.string().describe("Krótki opis osoby"),
});
export type GiftRecommendationInput = z.infer<
typeof giftRecommendationInputSchema
>;
Ta sama schema jest używana:
- w endpointzie REST (do sprawdzenia ciała żądania);
- w narzędziu MCP (jako inputSchema);
- w testach (jako baza dla fikstur).
Podpinamy schemę do narzędzia MCP
// mcp/tools/recommendGifts.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
import { server } from "../mcpServer";
server.registerTool({
name: "recommend_gifts",
description: "Dobór prezentów według profilu i budżetu",
inputSchema: giftRecommendationInputSchema,
async execute(args) {
const input = giftRecommendationInputSchema.parse(args);
const res = await fetch("https://api.myapp.com/gifts/recommendations", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(input),
});
return res.json();
},
});
SDK samo skonwertuje schemę Zod do JSON Schema, którą ChatGPT zobaczy w tools/list. To rozwiązuje dwa problemy naraz:
- typy argumentów narzędzia i kod są ściśle powiązane;
- przy zmianie schematu kompilator TypeScript zmusi was do zaktualizowania także handlera.
Testy kontraktowe dla MCP ↔ backend
Testy kontraktowe to tu nie straszne słowo, a kilka całkiem przyziemnych sprawdzeń.
Najprostszy test unit/contract może wyglądać tak:
// tests/mcp/recommendGifts.contract.test.ts
import { giftRecommendationInputSchema } from "@/domain/gifts";
test("przykładowe żądanie odpowiada schematowi narzędzia", () => {
const sample = {
occasion: "birthday",
budgetUsd: 150,
recipientProfile: "kolega, lubi gadżety",
};
expect(() => giftRecommendationInputSchema.parse(sample)).not.toThrow();
});
Taki test nie gwarantuje, że świat jest idealny, ale przynajmniej wyłapuje rozjazd między oczekiwaniami backendu i warstwy MCP, jeśli zmienicie schemę i zapomnicie odświeżyć fikstury.
Potem ten sam pomysł łatwo rozszerzyć na:
- zamockowane odpowiedzi zewnętrznych API (Stripe, CMS);
- przepuszczenie klienta MCP przeciwko prawdziwemu serwerowi MCP w środowisku testowym.
4. Strategie wersjonowania tools i resources
Schematy prędzej czy później się zmieniają. Najważniejsze — nie robić tego w stylu „po prostu zmienię nazwę pola, co może pójść nie tak”. W świecie LLM można tak zepsuć nie tylko build, ale i zachowanie modelu: stare prompty, zapisane dialogi i golden case’y nadal będą oczekiwać starego kontraktu.
Zmiany addytywne vs breaking‑zmiany
Umownie zmiany dzielimy na dwie kategorie.
Zmiany addytywne — coś dodajecie, ale niczego nie psujecie:
- nowe opcjonalne pole w odpowiedzi;
- nowy opcjonalny argument z wartością domyślną;
- dodatkowe wartości enum, do których UI i model mogą podejść neutralnie.
Na przykład dodaliście do odpowiedzi narzędzia pole deliveryEstimateDays, ale stary widżet je po prostu ignoruje. To bezpieczne: schema może się rozszerzyć, ale nikt nie musi jej używać.
Zmiany breaking — łamiecie istniejące oczekiwania:
- robicie pole obowiązkowym, choć wcześniej go nie było;
- zmieniacie typ (string → obiekt);
- zmieniacie sens argumentów (budżet w USD → budżet w walucie lokalnej, ale bez zmiany nazw pól).
W takich przypadkach jedyna bezpieczna droga — wprowadzić nową wersję narzędzia.
Wzorzec Tool_v2
Klasyczny wzorzec: mieliście recommend_gifts, chcecie poważnie zmienić schemę. Nie dotykacie starego narzędzia, tylko tworzycie nowe — recommend_gifts_v2.
// v1
const recommendGiftsInput_v1 = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
});
// v2: obsługa walut i filtrów dostawy
const recommendGiftsInput_v2 = z.object({
occasion: z.string(),
maxPrice: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
deliverByDate: z.string().optional(); // łańcuch w ISO
});
server.registerTool({
name: "recommend_gifts",
description: "DEPRECATED: użyj recommend_gifts_v2",
inputSchema: recommendGiftsInput_v1,
async execute(args) { /* stara logika */ },
});
server.registerTool({
name: "recommend_gifts_v2",
description:
"Dobór prezentów według budżetu, waluty i terminu dostawy",
inputSchema: recommendGiftsInput_v2,
async execute(args) { /* nowa logika */ },
});
Model i stare prompty/agenci będą dalej używać recommend_gifts, dopóki ich nie zaktualizujecie. Nowe scenariusze piszecie już pod recommend_gifts_v2.
Po okresie migracji:
- golden case’y i agenci przełączeni na v2;
- metryki pokazują, że v1 prawie nie jest wywoływane;
można zacząć ostrożnie zwijać v1 (np. najpierw ukryć je z listy narzędzi w dev/staging, potem — na produkcji).
Wersjonowanie zasobów
Tools to nie jedyne, co wymaga wersji. Jeśli macie zasoby (resources) — np. statyczny katalog prezentów — je także warto wersjonować.
Popularne warianty:
- wpisać wersję w nazwie zasobu: gift_catalog.v1.json, gift_catalog.v2.json;
- albo przekazywać wersję w URI parametrze: /api/catalog?version=1.
Sens ten sam: nie podmieniać danych „pod nogami” już uruchomionych scenariuszy, tylko dawać im jasno zafiksowaną wersję katalogu.
Migracje bez przestoju
Typowy cykl migracji narzędzia:
- Dodajecie nową wersję narzędzia (_v2) równolegle do starej.
- Aktualizujecie App/agentów/system‑prompt tak, aby używali nowej wersji.
- Przepuszczacie golden case’y i LLM‑eval dla obu wariantów i upewniacie się, że dla krytycznych scenariuszy jakość nie spadła.
- Obserwujecie metryki użycia v1 vs v2 (oraz błędy).
- Gdy ruch na v1 zbliża się do zera, zaczynacie je dezaktywować.
Takie podejście dobrze działa zarówno dla migracji schematów, jak i aktualizacji SDK/protokołu, i dla zmian w Auth. Omówiliśmy już, jak ewoluują same narzędzia i zasoby — przez v1/v2 i ostrożne zmiany addytywne. Druga duża część kontraktu — uwierzytelnianie i autoryzacja: OAuth, scopes i .well-known. One również żyją latami i wymagają uważnych migracji.
5. Ewolucja uwierzytelniania: .well-known, scopes i istniejący OAuth
Jeśli wasz produkt już działa w świecie OAuth 2.1/OpenID Connect, integracja z ChatGPT przez MCP to nie „jeszcze jeden login”, lecz nowy klient, który musi rozmawiać z waszym Authorization Server według wspólnych zasad.
MCP i .well-known/oauth-protected-resource
Pełny OAuth 2.1/OpenID Connect i konfigurację Auth Server omawiamy szczegółowo w osobnym module kursu (zob. moduł o uwierzytelnianiu). Tu interesuje nas aspekt praktyczny: jak zasób MCP informuje ChatGPT, że jest chroniony przez OAuth, i jak uruchomić flow łączenia konta.
Standardowy wzorzec dla chronionych zasobów MCP:
- wasz serwer MCP eksponuje specjalny endpoint /.well-known/oauth-protected-resource;
- w odpowiedzi mówi, jaki to zasób i przez jakie AS (Authorization Server) jest chroniony;
- przy 401 na wywołaniu MCP serwer zwraca nagłówek WWW-Authenticate z linkiem do tego .well-known, a ChatGPT sam uruchamia flow OAuth („Link account”).
Minimalny przykład na Express:
// mcp-auth/.well-known.ts
import express from "express";
const app = express();
app.get("/.well-known/oauth-protected-resource", (_req, res) => {
res.json({
resource: "https://mcp.myapp.com",
authorization_servers: [
"https://auth.myapp.com/.well-known/openid-configuration",
],
});
});
app.listen(3000);
Oraz handler 401 z podpowiedzią dla klienta:
res
.status(401)
.set(
"WWW-Authenticate",
'Bearer resource_metadata="https://mcp.myapp.com/.well-known/oauth-protected-resource"',
)
.end();
ChatGPT, widząc ten nagłówek, rozumie, do którego AS iść i jak uruchomić flow OAuth dla waszego zasobu MCP.
Scopes i migracje autoryzacji
Scopes — kolejny powód migracji. Omawialiśmy to szerzej w module o Auth, ale w kontekście integracji/migracji ważnych jest kilka punktów.
Wyobraźcie sobie, że GiftGenius początkowo potrafił tylko czytać katalog (gifts.read), a później dodaliście gifts.write do tworzenia zamówień. Musicie:
- dodać nowy scope do konfiguracji klienta (ChatGPT App);
- zaktualizować serwer MCP, by wymagał tego scope tylko dla narzędzi, które faktycznie coś zmieniają;
- opisać zmiany w .well-known, jeśli to konieczne.
Z punktu widzenia UX użytkownik przy następnej próbie użycia nowej funkcji może zobaczyć prośbę o „rozszerzenie uprawnień” dla aplikacji ChatGPT. Nie chcecie, aby to działo się w środku trwającej rozmowy bez ostrzeżenia — dlatego takie zmiany trzeba:
- zapowiedzieć (release notes, dokumentacja);
- przetestować na staging z testowym AS;
- zestroić z aktualizacją opisów narzędzi (destructiveHint itd.), aby model świadomie wywoływał „niebezpieczne” tools.
6. Metadane i adnotacje: warstwa hintów nad kontraktem
Warstwa Auth odpowiada na pytanie, kto i co może robić przez wasz App. Ale nawet przy poprawnych tokenach i scopes ważne jest, jak model będzie wywoływał wasze narzędzia i tłumaczył działania użytkownikowi. Tu wchodzi dodatkowa warstwa hintów: metadane i adnotacje.
Kontrakt (schema) mówi, co narzędzie przyjmuje i zwraca. Metadane i adnotacje pomagają modelowi zrozumieć, jak i kiedy je wywoływać. Staje się to szczególnie ważne, gdy ewoluujecie App: dodajecie nowe działania destrukcyjne, zmieniacie UI, wprowadzacie integracje ze światem zewnętrznym.
_meta["openai/widgetDescription"] i widgetCSP
W Apps SDK i opisach MCP jest pole specjalne _meta, w które OpenAI dodaje własne rozszerzenia protokołu. Na przykład:
- _meta["openai/widgetDescription"] — krótki opis tego, co pokazuje wasz widżet; model może go używać, by nie „opowiadać” UI i poprawnie anonsować App;
- _meta["openai/widgetCSP"] — deklaracja domen CSP potrzebnych waszemu widżetowi (do fetch/obrazków/skryptów).
Gdy zmieniacie UI (np. dodajecie nowy krok finalizacji zamówienia), warto zaktualizować widgetDescription, by model nadal poprawnie objaśniał użytkownikowi, co się dzieje.
Adnotacje narzędzi (readOnlyHint, destructiveHint, openWorldHint)
Adnotacje to proste flagi boolowskie, które znacząco wpływają na UX i bezpieczeństwo:
- readOnlyHint: true — narzędzie niczego nie zmienia (tylko odczyt). Model może je wywoływać bez zbędnych potwierdzeń.
- destructiveHint: true — narzędzie może coś usunąć/zmienić. ChatGPT poprosi o wyraźne potwierdzenie.
- openWorldHint: true — narzędzie publikuje dane na zewnątrz albo może zwrócić „bardzo dużo wszystkiego”, co wymaga sumaryzacji.
Przykład deskryptora narzędzia z adnotacjami:
server.registerTool({
name: "delete_saved_gift",
description: "Usuwa zapisany prezent użytkownika",
inputSchema: z.object({ giftId: z.string() }),
annotations: {
readOnlyHint: false,
destructiveHint: true,
openWorldHint: false,
},
async execute({ giftId }) {
// ...usuwamy prezent
},
});
Przy migracji, gdy dodajecie nowe „niebezpieczne” narzędzia, adnotacje są waszym sprzymierzeńcem: pomagają ChatGPT nie wykonywać ich po cichu i skłaniają do ostrożniejszego zachowania.
Ważne: adnotacje nie są „prawdziwą” ochroną. Wpływają tylko na zachowanie klienta i modelu. Prawdziwe bezpieczeństwo zapewnia nadal wasz serwer (Auth, scopes, walidacja).
7. Migracje SDK i specyfikacji MCP
MCP i Apps SDK szybko się rozwijają — pojawiają się nowe pola w capabilities, nowe typy wiadomości, nowe _meta/annotations. Dokumentacja uczciwie ostrzega: „na stan na rok 2025” — i z tym trzeba żyć.
Dlatego migracje wersji SDK i spec to normalna część życia App, a nie rzadkie zdarzenie „kiedyś tam”.
Typowy proces aktualizacji
Zdrowy scenariusz aktualizacji wygląda mniej więcej tak:
- Czytacie changelog nowej wersji Apps SDK/MCP SDK. Zaznaczacie wszystkie potencjalne zmiany breaking.
- Aktualizujecie zależności w środowisku dev/staging, nie dotykając produkcji.
- Przepuszczacie MCP Inspector / Jam lub innego klienta:
- sprawdzacie handshake;
- tools/list / resources/list;
- kilka testowych tools/call.
- Aktualizujecie opisy narzędzi i _meta zgodnie z nowymi możliwościami:
- np. dodajecie nowe annotations albo widgetDescription.
- Przepuszczacie golden case’y i LLM‑eval, o których mówiliśmy w poprzednich wykładach, aby upewnić się, że zachowanie App pod względem jakości się nie pogorszyło.
- Dopiero potem wypuszczacie aktualizacje na produkcję, najlepiej używając canary/feature‑flag dla podzbioru ruchu.
Przykład: dodajemy openWorldHint w nowej wersji SDK
Załóżmy, że nowa wersja Apps SDK dodała wsparcie dla openWorldHint, i postanowiliście oznaczyć nim narzędzie search_public_reviews, które przeszukuje zewnętrzne recenzje i może zwrócić dużo szumu.
Kroki wyglądają tak:
- aktualizujecie SDK i typy;
- dopiszcie annotations.openWorldHint = true w deskryptorze narzędzia;
- aktualizujecie system‑prompt, by agent jawnie objaśniał użytkownikowi, że zaraz nastąpi zapytanie do świata zewnętrznego;
- przepuszczacie safety‑golden case’y (zwłaszcza dotyczące prywatności/PII), aby upewnić się, że model nie stał się nadmiernie gadatliwy.
Omówiliśmy ogólny proces aktualizacji SDK i adnotacji. Spójrzmy teraz na to w jednym konkretnym scenariuszu — ewolucji narzędzia recommend_gifts.
8. Mini‑case: ewolucja recommend_gifts w GiftGenius
Zbierzmy wszystko razem na konkretnym scenariuszu.
Wersja wyjściowa
Podstawowe narzędzie wyglądało tak:
const recommendGiftsInput_v1 = z.object({
occasion: z.string(),
budgetUsd: z.number().int().positive(),
recipientProfile: z.string(),
});
server.registerTool({
name: "recommend_gifts",
description: "Dobiera pomysły na prezenty w USD",
inputSchema: recommendGiftsInput_v1,
async execute(args) {
const input = recommendGiftsInput_v1.parse(args);
return giftService.recommend(input); // wewnętrzna funkcja
},
});
Wszystko działa świetnie, dopóki macie tylko użytkowników z USA i jedną walutę.
Nowe wymagania biznesowe: wielowalutowość i deadline
Zespół produktowy przychodzi z nowymi wymaganiami:
- trzeba wspierać EUR/GBP;
- trzeba uwzględniać deadline dostawy (nie pokazywać prezentów, które przyjdą za miesiąc, jeśli urodziny za trzy dni);
- warto dodać do odpowiedzi estymację czasu dostawy.
Naiwne podejście: po prostu zmieniamy pola:
- budgetUsd zmieniamy na maxPrice;
- dodajemy currency;
- do odpowiedzi dodajemy deliveryEstimateDays.
Co pójdzie nie tak?
Stare prompty (w tym golden case’y i opis w system‑prompt) oraz zapisane dialogi nadal wysyłają budgetUsd. Model nie wie, że tego już nie ma. Warstwa MCP zacznie się wywracać przy próbie parse. Zachowanie ChatGPT App niespodziewanie psuje się u realnych użytkowników.
Prawidłowa droga:
- Dodajemy nową schemę i nowe narzędzie _v2.
const recommendGiftsInput_v2 = z.object({
occasion: z.string(),
maxPrice: z.number().int().positive(),
currency: z.enum(["USD", "EUR", "GBP"]),
recipientProfile: z.string(),
deliverByDate: z.string().optional(),
});
server.registerTool({
name: "recommend_gifts_v2",
description:
"Dobór prezentów z uwzględnieniem waluty i oczekiwanej daty dostawy",
inputSchema: recommendGiftsInput_v2,
async execute(args) {
const input = recommendGiftsInput_v2.parse(args);
return giftService.recommendV2(input); // nowa logika
},
});
- Pozostawiamy recommend_gifts bez zmian, dodając w description adnotację DEPRECATED.
- Aktualizujemy system‑prompt i opisy App tak, aby model preferował recommend_gifts_v2 (można wskazać to wprost w instrukcjach).
- Aktualizujemy widżet GiftGenius, by rozumiał nowy format odpowiedzi: pole deliveryEstimateDays itd.
- Przepuszczamy golden case’y dla typowych scenariuszy (dobór prezentów do określonej daty) przez LLM‑eval.
Testy i obserwowalność
Kilka testów, które warto mieć:
Test kontraktowy dla nowego wejścia:
test("v2 przyjmuje scenariusz z EUR i deadlinem", () => {
const sample = {
occasion: "birthday",
maxPrice: 100,
currency: "EUR",
recipientProfile: "kolega",
deliverByDate: "2025-12-24",
};
expect(() => recommendGiftsInput_v2.parse(sample)).not.toThrow();
});
Obserwowalność na produkcji:
- metryka udziału wywołań recommend_gifts_v2 vs recommend_gifts;
- współczynnik błędów dla v1 (oczekujemy, że nie rośnie);
- wynik LLM‑eval dla golden case’ów przed/po migracji (po poprzednich wykładach już wiecie, jak to robić).
Gdy v2 „wygrywa” i jakościowo, i w metrykach użycia, można ostrożnie planować dezaktywację v1.
Jeśli uprościć do trzech myśli: (1) MCP to cienki adapter, a nie nowy monolit; (2) schematy, auth i adnotacje to długowieczny kontrakt między ChatGPT a waszym backendem, który trzeba wersjonować i testować równie starannie jak zwykłe API; (3) wszelkie migracje SDK/spec to normalny proces inżynierski ze stagingiem, golden case’ami i obserwowalnością, a nie „zaktualizowaliśmy paczkę w piątek wieczorem”. Jeśli będziecie patrzeć na ChatGPT App przez tę pryzmat, integracje z istniejącym produktem przestaną wyglądać jak chaos.
9. Typowe błędy przy integracji i migracjach MCP/SDK
Błąd nr 1: MCP jako „nowy backend”, a nie cienki adapter.
Czasem kusi, by do warstwy MCP przenieść całą logikę biznesową: dostępy do DB, reguły domenowe, wyliczenia. To zamienia serwer MCP w kolejny monolit, który trudno zgrywać z resztą backendu. Zdrowiej trzymać MCP jako Gateway/Adapter nad istniejącymi serwisami: cała logika domenowa żyje tam, gdzie przed ChatGPT, a MCP tylko tłumaczy JSON tam‑i‑z‑powrotem.
Błąd nr 2: Różne schematy dla tego samego obiektu.
Częsty antywzorzec — mieć trzy definicje „prezentu”: jedną w DB, jedną w REST‑API, jedną w narzędziu MCP, i każda trochę inna. W efekcie psuje się statyczna typizacja, kontrakty, testy i zdrowy rozsądek. Użycie jednej wspólnej schemy (Zod/TypeBox itd.) jako Single Source of Truth i generowanie JSON Schema dla MCP znacząco zmniejsza to ryzyko.
Błąd nr 3: Nieprawidłowe migracje schem — „cichy” breaking change.
Zmiana nazwy pola lub jego sensu bez zmiany nazwy narzędzia to prosta droga do ukrytego regresu. Model będzie wciąż wysyłał stary format, a incydent ujawni się tylko u części użytkowników i to nie od razu. Przy poważnych zmianach wprowadzajcie *_v2, zostawcie starą wersję równolegle, używajcie oznaczeń deprecation i monitoringu.
Błąd nr 4: Ignorowanie zmian w Auth i scopes.
Dodaliście nowe narzędzie z efektami ubocznymi, ale zapomnieliście zaktualizować scopes i .well-known? Użytkownik może dostać 401 w środku scenariusza albo, przeciwnie, wasz MCP zacznie wykonywać operacje destrukcyjne bez adekwatnej autoryzacji. Planujcie migracje warstwy auth równie starannie jak migracje schem: przez staging, testy i płynne rozszerzanie uprawnień.
Błąd nr 5: Brak użycia adnotacji (destructiveHint, readOnlyHint, openWorldHint).
Jeśli nie podpowiecie modelowi, które narzędzia są bezpieczne, a które potencjalnie niebezpieczne, może zachowywać się nieprzewidywalnie: prosić o potwierdzenie dla niewinnego get_catalog i bez ostrzeżenia wykonywać usuwanie danych. Właściwe adnotacje czynią zachowanie przewidywalnym dla użytkownika i zmniejszają ryzyko incydentów jakości oraz bezpieczeństwa.
Błąd nr 6: Aktualizacja SDK „na produkcji” bez przepuszczenia golden case’ów.
Nowa wersja SDK/spec może dodawać pola, zmieniać zachowanie handshake lub strukturę wiadomości. Jeśli po prostu „zaktualizujecie zależności i wdrożycie”, ryzykujecie regres jakości (model przestał wywoływać właściwe narzędzie, zmieniła się treść błędów itd.). Najpierw — dev/staging, MCP Inspector, potem golden case’y i LLM‑eval, i dopiero potem — produkcja.
Błąd nr 7: Sztywne powiązanie logiki biznesowej z jedną wersją narzędzia.
Gdy wewnętrzna logika Gift Service bezpośrednio zależy od konkretnego recommend_gifts, ciężko migrować na recommend_gifts_v2 bez bólu. Najlepsza praktyka — mieć wewnętrzny serwis, który ewoluuje według własnych reguł, a narzędzia *_v1, *_v2 to tylko cienkie adaptery mapujące stare i nowe kontrakty zewnętrzne na wspólne struktury domenowe.
Błąd nr 8: Brak obserwowalności względem wersji narzędzi.
Jeśli w logach i metrykach nie rozróżniacie, które konkretne narzędzie i wersja zostały wywołane, debugowanie migracji zamienia się w zgadywanie. Logujcie nazwę narzędzia, wersję schemy/SDK i kluczowe parametry — wtedy każdy regres łatwiej powiążecie z konkretną zmianą.
GO TO FULL VERSION