MCP 2026protokół od zera · wydanie 2026-07-28
część II · rewizja 2026-07-28 · 9 zmian głównych

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-2567

Usunię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:

Pod A
bezczynny
Pod B
bezczynny
Pod C
bezczynny

Tryb: sesja. Kliknij „wyślij" — pierwszy request „przywiąże" Cię do losowego poda.

✅ Co to daje Twojemu serwerowi

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"
    }
  }
}
inspektor _meta

Kliknij dowolne pole _meta po lewej — pokażę, co robi i co się dzieje, gdy go zabraknie.

✅ Co to daje Twojemu serwerowi

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
✅ Co to daje Twojemu serwerowi

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"
✅ Co to daje Twojemu serwerowi

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ętoZastąpione przez
pingZwykły request-response robi to samo — brak odpowiedzi = problem transportu.
logging/setLevelio.modelcontextprotocol/logLevel w _meta KAŻDEGO requestu — poziom logowania per-żądanie, nie globalny.
notifications/roots/list_changedBrak 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-2663

Eksperymentalne 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.

Zobacz Tasks w Części III →

7. MRTR — Multi Round-Trip Requests

SEP-2322 · najważniejsza zmiana

To 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.

symulator krok po kroku

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/callnowy 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."}] }
🧠 Sprawdź

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)])
✅ Co to daje Twojemu serwerowi

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-2575

Last-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 + cacheScope

Nowy 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ć.

Deterministyczna kolejność tools/list

Serwery 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.

Nagłówki Mcp-Method / Mcp-Name

Wymagane na Streamable HTTP — lustro pól method/params.name z ciała, żeby load balancer mógł routować bez parsowania JSON-a.

x-mcp-header

Moż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ł.

JSON Schema 2020-12

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).

OpenTelemetry w _meta

traceparent, 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:

ZakresZnaczenie
-32000-32019Legacy — kody przydzielone przed tą polityką. Nowe implementacje NIE POWINNY ich w ogóle używać.
-32020-32099Zarezerwowane wyłącznie dla specyfikacji MCP.
KodNazwaUwaga
-32020HeaderMismatchnagłówek HTTP nie zgadza się z ciałem żądania
-32021MissingRequiredClientCapabilityserwer potrzebuje capability, której klient nie zadeklarował
-32022UnsupportedProtocolVersionklient chce wersji, której serwer nie obsługuje
-32602Invalid Paramsprzejęł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:

dni
godz
min
sek

…do końca minimalnego okna deprecacji dla Roots, Sampling, Logging i HTTP+SSE.

Roots, Sampling, Logging

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.

HTTP+SSE (transport z 2024-11-05)

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.

Dynamic Client Registration (RFC 7591)

Ustępuje Client ID Metadata Documents. Zostaje dla kompatybilności ze starszymi authorization serverami — więcej w Części IV.

Sprawdź się: rewizja 2026-07-28

Co zastąpiło uścisk dłoni initialize?
Jak serwer prosi klienta o dodatkowe dane w modelu 2026-07-28?
Co musi zrobić klient z polem requestState?
Które trzy funkcje zostały zdeprecjonowane (SEP-2577)?