Skip to content

feat(mcp)!: port na MCP SDK 2.0 (FastMCP → MCPServer) + wydanie 0.3.0 - #19

Merged
mpasternak merged 4 commits into
mainfrom
feat/port-mcp-2.0
Aug 7, 2026
Merged

feat(mcp)!: port na MCP SDK 2.0 (FastMCP → MCPServer) + wydanie 0.3.0#19
mpasternak merged 4 commits into
mainfrom
feat/port-mcp-2.0

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Zdejmuje pin mcp<2 z 0.2.0 — port na natywne MCPServer z SDK 2.0.

Co się okazało

Port to w większości rename: .tool(), .prompt(), .custom_route(), lifespan, ctx.request_context i cała warstwa OAuth RS (AuthSettings, TokenVerifier, AccessToken) przechodzą bez zmian. Jedyna zmiana strukturalna: SDK 2.0 wyjęło host/port z konstruktora, więc _auth_kwargs rozcięte na nie i _http_kwargs (argumenty run()).

Zweryfikowane najpierw na próbnym porcie wykonanym na kopii repo w izolowanym środowisku, przed tknięciem prawdziwych plików.

Naprawa błędu, nie tylko rename

SDK 2.0 przestało doklejać ukośnik do URL bez ścieżki (url_preserve_empty_path). Normalizacja issuera była zduplikowanaAuthSettings po stronie PRM i własny AnyHttpUrl w authorization_server_metadata — i musiała dawać wynik bajt w bajt zgodny, bo tego wymaga RFC 8414 §3.3. Zmierzone przed poprawką:

PRM authorization_servers: ['http://127.0.0.1:8055']    <- AuthSettings (SDK)
AS  issuer              : 'http://127.0.0.1:8055/'      <- nasze

To wywracałoby „authorize" dokładnie w trybie PROXY, dla którego ten kod powstał — czyli kasowało niezmiennik, który 863b7f4 celowo ustawiał. Zamiast dopasować stałą, issuer płynie teraz z jednego obiektu AuthSettings, a authorization_server_metadata wypisuje go dosłownie. Rozjazd jest niemożliwy z konstrukcji, a nie dzięki czyjejś pamięci.

Trzy luki, które port otworzył albo obnażył

Gwarancja offline straciła pokrycie. docs/rozwoj.md obiecuje testy bez żywych wywołań i dotąd pilnował tego respx — ale SDK 2.0 chodzi po httpx2, którego respx nie przechwytuje (sprawdzone: żądanie wychodzi do sieci). Doszła blokada gniazd w conftest.py, z przepustką dla loopbacku (logowanie OAuth stawia serwer pętli zwrotnej).

Zero testów na poziomie protokołu. W repo nie było ani jednego ClientSession ani handshake'u, choć całe ryzyko zmiany SDK jest protokolarne. Nowe testy gadają z serwerem w podprocesie po stdio i już przy pisaniu złapały snake_case z 2.0 (serverInfoserver_info). Sieci nie ruszają, więc chodzą w zwykłym CI.

Izolacja tokenu na węższej podstawie. W 1.x lifespan wchodził per sesja; w 2.0 wchodzi RAZ i jego wynik dzielą wszystkie sesje. Jedyne, co trzyma bearery osobno, to ContextVar. Test zweryfikowany mutacją — po podmianie ContextVara na stan globalny pada.

Uwaga o zależnościach

mcp>=2,<3 — sufit zostaje. Zdjęcie go odtwarzałoby mechanizm awarii 0.1.1: test-newest-deps jest continue-on-error, więc nie zatrzyma wydania, a canary chodzi raz w tygodniu i już po publikacji. Żadne z nich nie jest bramką.

uv.lock przegenerowany — bez tego blokujący job test szedłby dalej po zamrożonym 1.28.1 i cała matryca nie dotknęłaby portu. Odpadło extra [cli] (mamy własny argparse), doszedł jawny starlette.

Weryfikacja

211 testów zielonych (204 + 7 nowych), ruff czysty, uv sync --locked spójny, twine check --strict przechodzi. uvx --from . na świeżym rozwiązaniu startuje na 3.10 i 3.13. Test dymny po stdio na żywym bpp.umlub.pl zwraca prawdziwe dane; tryb HTTP wykrywa passthrough i oddaje 401 z WWW-Authenticate.

Żaden test nie został usunięty ani osłabiony. Zmienione asercje dotyczą wyłącznie kanonicznej postaci issuera — czyli świadomej zmiany zachowania SDK — i są wymienione w commitach.

Przed tagiem

Spec zawiera bramki (ręczny workflow_dispatch na mcpb.yml, bo .mcpb jest dziś budowana dopiero na tagu, czyli po kroku nieodwracalnym) oraz dwustopniową ścieżkę wycofania: yank na PyPI cofa do sprawnego 0.2.0, ale nie rusza linku .mcpb z README.

🤖 Generated with Claude Code

mpasternak and others added 4 commits August 7, 2026 14:50
Spec powstał po rozpoznaniu API 2.0 w źródłach i zweryfikowaniu każdego
twierdzenia na próbnym porcie wykonanym na kopii repo w izolowanym
środowisku — nie na podstawie samej lektury dokumentacji.

Najważniejsze ustalenie: port to w większości rename (`.tool()`,
`.prompt()`, `.custom_route()`, lifespan, `ctx.request_context` i cała
warstwa OAuth RS przechodzą bez zmian), ale kryje jeden realny błąd.
SDK 2.0 przestało doklejać ukośnik do URL bez ścieżki, przez co PRM
ogłaszałby inny issuer niż nasz dokument AS — dokładnie ta niezgodność,
którą 863b7f4 celowo eliminował. Powód jest strukturalny: normalizacja
issuera była zduplikowana po obu stronach i musiała dawać wynik bajt
w bajt zgodny. Spec zamyka to jednym źródłem prawdy zamiast dopasowania
stałej.

Migracja na httpx2 świadomie poza zakresem: respx nie przechwytuje
httpx2, więc oznaczałaby przepisanie 138 wywołań w 15 plikach testów
jednocześnie z portem, który te testy mają pilnować.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`mcp.server.fastmcp` zniknęło w 2.0 bez warstwy zgodności, a gałąź 1.x jest
w trybie maintenance. Zależność: `mcp>=2,<3`, bez extra `[cli]` (mamy własny
argparse, extra ciągnęło typer + python-dotenv bez powodu). Doszedł jawny
`starlette` — importujemy `JSONResponse` wprost, a tranzytywność nie jest
kontraktem; dokładnie tak straciliśmy `httpx`, gdy SDK przeszło na `httpx2`.

Sufit `<3` zostaje świadomie. Zdjęcie go odtwarzałoby mechanizm awarii 0.1.1:
`test-newest-deps` jest informacyjny (`continue-on-error`), więc nie zatrzyma
wydania, a canary chodzi raz w tygodniu i JUŻ PO publikacji — żadne z nich
nie jest bramką. Sufit podnosimy ręcznie, po porcie.

`uv.lock` przegenerowany. Bez tego blokujący job `test` szedłby dalej po
zamrożonym 1.28.1 i cała matryca 3.10–3.13 nie dotknęłaby portu.

Jedyna zmiana strukturalna: SDK 2.0 wyjęło host/port z konstruktora, więc
`_auth_kwargs` rozcięte na nie (argumenty konstruktora) i `_http_kwargs`
(argumenty `run()`). `host` musi zostać w tym drugim, bo `run()` przekazuje
go do `streamable_http_app(host=...)`, skąd bierze się ochrona przed
DNS-rebinding opisana w docs/uwierzytelnianie.md.

NAPRAWA BŁĘDU, nie tylko rename: SDK 2.0 przestało doklejać ukośnik do URL
bez ścieżki (`url_preserve_empty_path`). Normalizacja issuera była
zduplikowana — `AuthSettings` po stronie PRM i własny `AnyHttpUrl`
w `authorization_server_metadata` — i musiała dawać wynik bajt w bajt zgodny
(RFC 8414 §3.3). Po zmianie w SDK obie ciche rozjechałyby się, wywracając
„authorize" dokładnie w trybie PROXY, dla którego ten kod powstał. Zamiast
dopasowywać stałą: issuer płynie z JEDNEGO obiektu AuthSettings, a
`authorization_server_metadata` wypisuje go dosłownie. Rozjazd staje się
niemożliwy z konstrukcji, nie dzięki czyjejś pamięci.

Przy okazji `version=` w konstruktorze: 2.0 domyśla w serverInfo pusty string
(1.x podstawiało wersję SDK, co i tak wprowadzało w błąd).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trzy luki, które port na SDK 2.0 albo otworzył, albo obnażył.

1. Gwarancja offline przestała mieć pokrycie. `docs/rozwoj.md` obiecuje, że
   pakiet testów nie robi żywych wywołań, i dotąd pilnował tego respx. Ale
   SDK 2.0 chodzi po `httpx2`, którego respx NIE przechwytuje (sprawdzone:
   żądanie wychodzi do sieci). Test idący przez stos SDK odpytałby prawdziwy
   serwer i przeszedł. Blokada gniazd w conftest zostawia loopback, bo testy
   logowania OAuth stawiają serwer pętli zwrotnej (RFC 8252). Dwa testy
   pilnują samej blokady — inaczej mogłaby przestać działać niezauważona.

2. W repo nie było ANI JEDNEGO testu na poziomie protokołu: zero
   `ClientSession`, zero handshake'u. Całe ryzyko zmiany SDK jest tymczasem
   protokolarne. Nowe testy gadają z serwerem w podprocesie po stdio i już
   przy pisaniu złapały snake_case z 2.0 (`serverInfo` → `server_info`).
   Sieci nie ruszają — initialize i listowanie obsługuje sam serwer — więc
   chodzą w zwykłym CI, nie pod markerem „live".

3. Izolacja tokenu między użytkownikami stoi teraz na węższej podstawie.
   W 1.x lifespan wchodził per sesja; w 2.0 wchodzi RAZ, a jego wynik dzielą
   wszystkie sesje i żądania. Jedyne, co trzyma bearery osobno, to ContextVar.
   Test zweryfikowany mutacją: po podmianie ContextVara na stan globalny pada,
   czyli faktycznie broni niezmiennika, a nie tylko go opisuje.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Port na MCP SDK 2.0. Dla użytkownika funkcjonalnie bez zmian, ale podmiana
głównej wersji SDK pod spodem to za dużo na patcha.

Spec uzupełniony o bramki przed tagiem (świeże `uvx`, ręczny `workflow_dispatch`
na mcpb.yml, test dymny na żywym BPP) i o ścieżkę wycofania. Ta druga ma dwa
kroki, nie jeden: `yank` na PyPI cofa resolvery do sprawnego 0.2.0, ale NIE
rusza linku /releases/latest/download/bpp-mcp.mcpb, do którego prowadzi
przycisk „Zainstaluj w Claude Desktop" w README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mpasternak
mpasternak merged commit 76cb1e2 into main Aug 7, 2026
5 checks passed
@mpasternak
mpasternak deleted the feat/port-mcp-2.0 branch August 7, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant