To nie jest changelog. To jest inna architektura.
Dziewięć zmian głównych z oficjalnego changeloga, każda w tym samym schemacie: co było, co jest, dlaczego, i co to daje serwerowi, który budujesz.
1. Koniec sesji
SEP-2567Usunięto Mcp-Session-Id z transportu Streamable HTTP. tools/list, resources/list, prompts/list przestały zależeć od tego, z którą kopią serwera rozmawiasz — bo żadna kopia niczego już nie pamięta między requestami. Jeśli serwer potrzebuje stanu między wywołaniami, dostaje go z powrotem jako zwykły, jawny argument narzędzia (uchwyt, ID zasobu) — nie z pamięci procesu.
Zobacz to na żywo — trzy kopie tego samego serwera za load balancerem, w obu modelach:
Tryb: sesja. Kliknij „wyślij" — pierwszy request „przywiąże" Cię do losowego poda.
Możesz skalować poziomo bez konfigurowania sticky sessions na load balancerze i bez dzielonej pamięci (Redis, sticky cookies) tylko po to, żeby przetrwać restart poda. Jeśli dziś Twój serwer trzyma coś w zmiennej globalnej między wywołaniami — to pierwsza rzecz do przepisania na jawny handle.
2. Bezstanowość — koniec initialize
SEP-2575
Skoro nie ma sesji, to nie ma sensu jednorazowy handshake, który tę sesję zakłada. Zamiast tego każdy pojedynczy request niesie w polu _meta to, co kiedyś ustalał initialize: wersję protokołu i capabilities klienta. Kliknij pola w JSON-ie poniżej:
{ "method": "tools/call", "params": { "name": "get_weather", "_meta": { "io.modelcontextprotocol/protocolVersion": "2026-07-28", "io.modelcontextprotocol/clientInfo": {"name":"ExampleClient","version":"1.0.0"}, "io.modelcontextprotocol/clientCapabilities": {}, "io.modelcontextprotocol/logLevel": "info" } } }
Kliknij dowolne pole _meta po lewej — pokażę, co robi i co się dzieje, gdy go zabraknie.
Twój handler przestaje mieć fazę „przed initialize" i „po initialize" — każde żądanie jest samowystarczalne i możesz je przetworzyć na dowolnym workerze, bez pamiętania historii połączenia. To upraszcza testy: każdy test to jeden request, zero setupu handshake'u.
server/discover
obowiązkowy RPC
Skoro nie ma initialize, potrzebny jest inny sposób, żeby zapytać serwer „co w ogóle umiesz, w jakich wersjach". server/discover jest właśnie do tego — i w odróżnieniu od starego handshake'u jest opcjonalny dla klienta: możesz strzelić dowolnym RPC od razu i obsłużyć błąd UnsupportedProtocolVersionError, jeśli wersja się nie zgadza.
{ "jsonrpc":"2.0","id":"discover-1", "method":"server/discover" } ↓ { "result": { "resultType":"complete", "supportedVersions": ["2026-07-28"], "capabilities": {"tools":{},"resources":{}}, "instructions":"Ten serwer udostępnia pogodę i pliki.", "ttlMs": 3600000, "cacheScope":"public" } }
# Python SDK v2 — realna logika negocjacji wersji, # dokładnie tak jak to robi ClientSession.discover(): try: raw = await self.send_discover(LATEST_MODERN_VERSION) except MCPError as e: if e.code != UNSUPPORTED_PROTOCOL_VERSION: raise data = types.UnsupportedProtocolVersionErrorData.model_validate(e.error.data) # przecina to, co WE wspieramy, z tym, co on wspiera mutual = [v for v in MODERN_PROTOCOL_VERSIONS if v in data.supported] if not mutual: raise raw = await self.send_discover(mutual[-1]) # najnowsza wspólna
server/discover jest cache'owalny (ttlMs, cacheScope) — gateway albo proxy przed Twoim serwerem może zapytać raz i obsłużyć nim tysiące klientów, zamiast każdy z nich musiał sam odpytywać tools/list, resources/list i prompts/list osobno tylko po to, by poznać Twoje capabilities.
4. subscriptions/listen
SEP-2575
Osobny endpoint GET SSE i metoda resources/subscribe zniknęły. Zamiast tego jest jeden request — subscriptions/listen — którego odpowiedź to długo żyjący strumień. Klient z góry mówi, jakich typów notyfikacji chce (opt-in), a serwer potwierdza to w pierwszej wiadomości strumienia. Zbuduj żądanie sam:
// Zaznacz filtr i kliknij „wygeneruj"…
from mcp.server.mcpserver import Context, MCPServer mcp = MCPServer("Notebook") NOTES = {"todo": "buy milk"} @mcp.tool() async def edit_note(name: str, text: str, ctx: Context) -> str: NOTES[name] = text # leci do wszystkich subskrybentów resourceSubscriptions dla tego URI await ctx.notify_resource_updated(f"note://{name}") return "saved"
Jeden strumień, jawnie wynegocjowany filtr — klient nigdy nie dostanie notyfikacji, o które nie prosił. Wywołujesz ctx.notify_resource_updated(uri) ze zwykłego handlera narzędzia i SDK samo domarszuje wiadomość do właściwych, aktywnych subskrypcji.
5. Usunięte metody
SEP-2575| Usunięto | Zastąpione przez |
|---|---|
ping | Zwykły request-response robi to samo — brak odpowiedzi = problem transportu. |
logging/setLevel | io.modelcontextprotocol/logLevel w _meta KAŻDEGO requestu — poziom logowania per-żądanie, nie globalny. |
notifications/roots/list_changed | Brak odpowiednika rdzeniowego — Roots jest zdeprecjonowane (patrz sekcja „Cmentarz"). |
Reguła: serwer MUSI NIE emitować notifications/message dla requestów, które nie zawierały logLevel w _meta.
6. Tasks przenosi się z rdzenia do extension
SEP-2663Eksperymentalne dotąd Tasks (długo trwające operacje) zostały wyjęte z core protokołu do osobnego, oficjalnego rozszerzenia io.modelcontextprotocol/tasks. Blokujące tasks/result zastąpiło pollingowe tasks/get, doszło tasks/update do przekazywania inputu w trakcie trwania zadania, a tasks/list zniknęło całkiem. Pełny cykl życia zadania — w Części III.
7. MRTR — Multi Round-Trip Requests
SEP-2322 · najważniejsza zmianaTo jest serce całej przebudowy. Przypomnij sobie sekcję I.7: serwer sam wysyłał NOWY request do klienta (sampling/createMessage, elicitation/create, roots/list) i musiał pamiętać, że czeka na odpowiedź, zanim dokończy oryginalne żądanie. Ten kanał przestał istnieć. Zamiast niego: serwer przerywa własną odpowiedź, mówiąc „potrzebuję więcej informacji" — a klient ponawia oryginalne żądanie, tym razem z odpowiedziami w bagażu.
Krok 0. Klient chce utworzyć repozytorium, ale nie podał jeszcze nazwy regionu bazy danych.
tools/call id:1 arguments: { "name": "my-db" }
Krok 1. Serwer (funkcja call_tool z przykładu SDK powyżej) widzi, że input_responses jest puste — zwraca InputRequiredResult.
{ "resultType":"input_required", "inputRequests": { "region": {"method":"elicitation/create", …} }, "requestState": "provision-v1" // ⚠ opaque, chronione integralnościowo }
Krok 2. Klient pyta użytkownika o region i dostaje odpowiedź „eu-central-1". Serwer o tym kroku nic nie wie — nie ma z nim żadnego połączenia.
{ "action":"accept", "content":{"region":"eu-central-1"} }
Krok 3. Klient PONAWIA tools/call — nowy id:2, te same argumenty, plus odpowiedzi i echo requestState.
tools/call id:2 input_responses: { "region": {"action":"accept","content":{"region":"eu-central-1"}} } request_state: "provision-v1" // dokładnie to samo, co dostał w kroku 1
Krok 4 — koniec. Serwer odczytuje region z input_responses, kończy pracę.
{ "content": [{"type":"text","text":"Provisioned 'my-db' in eu-central-1."}] }
Ile zmiennych globalnych, sesji czy cache'u serwer potrzebował, żeby to zadziałało? Zero. Cały kontekst „co robimy" przejechał tam i z powrotem wewnątrz requestState — serwer mógł nawet zostać zrestartowany między krokiem 1 a 3, i nic by się nie zepsuło.
from mcp_types import ( CallToolResult, ElicitRequest, ElicitRequestFormParams, ElicitResult, InputRequiredResult, TextContent, ) from mcp.server import Server, ServerRequestContext ASK_REGION = ElicitRequest(params=ElicitRequestFormParams( message="Which region should the database live in?", requested_schema={"type": "object", "properties": {"region": {"type": "string"}}, "required": ["region"]}, )) async def call_tool(ctx: ServerRequestContext, params) -> CallToolResult | InputRequiredResult: answer = (params.input_responses or {}).get("region") if not isinstance(answer, ElicitResult) or answer.content is None: # Pierwsze wywołanie (albo klient nic nie odesłał) — przerywamy. return InputRequiredResult(input_requests={"region": ASK_REGION}, request_state="provision-v1") name = (params.arguments or {})["name"] text = f"Provisioned {name!r} in {answer.content['region']}." return CallToolResult(content=[TextContent(type="text", text=text)])
Twój handler narzędzia to teraz zwykła, bezstanowa funkcja — dowolny worker, dowolny pod, żadnego „otwartego kanału czeka na odpowiedź". Cena: musisz sam decydować, co włożyć do requestState i chronić je integralnościowo (HMAC/AEAD), bo klient może je manipulować. Traktuj je jak podpisany token, nie jak zaufany magazyn.
8. resultType na każdym wyniku
SEP-2322
Skoro odpowiedź może teraz znaczyć „gotowe" albo „daj mi więcej", każdy result niesie znacznik resultType. Dla kompatybilności wstecz: brak pola = traktuj jak "complete".
{ "resultType": "complete", "content": [...] } // gotowe, koniec{ "resultType": "input_required", "inputRequests": {...} } // klient musi ponowić9. Koniec resumability
SEP-2575Last-Event-ID i numerowanie zdarzeń SSE zniknęły. Jeśli strumień się zerwie w trakcie żądania — nie ma „doczytywania od miejsca, w którym stanąłeś". Jest tylko jedno wyjście: ponów cały request z nowym id.
Drobne zmiany, które i tak zmienią Twój kod
ttlMs + cacheScopeNowy interfejs CacheableResult na tools/list, resources/list, resources/read i innych. ttlMs — jak długo wynik jest świeży; cacheScope ("public"/"private") — czy pośrednicy (CDN, gateway) mogą go cache'ować.
tools/listSerwery POWINNY zwracać narzędzia w tej samej kolejności za każdym razem. Bezpośredni zysk: wyższy cache-hit promptu LLM po stronie hosta — ten sam prefiks kontekstu = tańsze zapytania.
Mcp-Method / Mcp-NameWymagane na Streamable HTTP — lustro pól method/params.name z ciała, żeby load balancer mógł routować bez parsowania JSON-a.
x-mcp-headerMożesz oznaczyć parametr narzędzia jako „mirroruj do nagłówka HTTP" (np. region do routingu). Klient MUSI to respektować, nawet jeśli Twój serwer o to nie prosił.
inputSchema/outputSchema mogą teraz używać dowolnych słów kluczowych 2020-12, a structuredContent — dowolnej wartości JSON. Nowe zasady dot. $ref do zasobów sieciowych (domyślnie zablokowane).
_metatraceparent, tracestate, baggage — jedyny wyjątek od reguły prefiksów w _meta, bo muszą być zgodne z W3C Trace Context.
Polityka kodów błędów
JSON-RPC rezerwuje -32000…-32099 dla implementacji. MCP dzieli tę przestrzeń na dwie strefy:
| Zakres | Znaczenie |
|---|---|
-32000…-32019 | Legacy — kody przydzielone przed tą polityką. Nowe implementacje NIE POWINNY ich w ogóle używać. |
-32020…-32099 | Zarezerwowane wyłącznie dla specyfikacji MCP. |
| Kod | Nazwa | Uwaga |
|---|---|---|
-32020 | HeaderMismatch | nagłówek HTTP nie zgadza się z ciałem żądania |
-32021 | MissingRequiredClientCapability | serwer potrzebuje capability, której klient nie zadeklarował |
-32022 | UnsupportedProtocolVersion | klient chce wersji, której serwer nie obsługuje |
-32602 | Invalid Params | przejęło rolę starego -32002 (zasób nie znaleziony) — klienci POWINNI nadal akceptować -32002 od serwerów legacy |
Cmentarz — co zdeprecjonowano, nie usunięto (jeszcze)
Polityka wprowadzona tą rewizją (SEP-2596) daje minimum 12 miesięcy na deprecację przed usunięciem. Licznik poniżej liczy realny czas od 28 lipca 2026 do 28 lipca 2027 — sprawdź, ile zostało:
…do końca minimalnego okna deprecacji dla Roots, Sampling, Logging i HTTP+SSE.
Działają nadal, ale nowe implementacje NIE POWINNY ich dodawać. Migracja: katalogi/pliki → argumenty narzędzia lub URI zasobu; sampling → bezpośrednia integracja z API dostawcy modelu; logging → stderr (stdio) albo OpenTelemetry.
Zdeprecjonowany od 2025-03-26, teraz formalnie w rejestrze deprecacji. Migruj do Streamable HTTP.
includeContext: "thisServer"/"allServers"Pomiń pole albo użyj "none". Znikną najpóźniej razem z Sampling.
Ustępuje Client ID Metadata Documents. Zostaje dla kompatybilności ze starszymi authorization serverami — więcej w Części IV.