Claude Agent SDK nad własnym serwerem MCP — agent bez klucza API

Część 1 tej serii rozwiązała dostęp — Claude Desktop mógł wreszcie odpytać moje dane medyczne przez serwer MCP, bez terminala. Ale dostęp to nie to samo co pomoc. Pytanie „przygotuj mnie do wizyty u nefrologa” wymagało, żebym ja sam wiedział, które markery są nefrologicznie istotne — kreatynina, eGFR, mocznik, elektrolity — i sam złożył kilka osobnych zapytań o trend w jedną spójną całość. Serwer z części 1 czekał biernie na dokładne pytanie. Zabrakło warstwy, która sama planuje kroki. Zbudowałem ją na Claude Agent SDK, nad tym samym serwerem MCP, bez zmiany ani jednej linii w narzędziach z części 1.

To druga część serii — pierwszą, o serwerze MCP i decyzjach ADR-owych, znajdziesz w tekście o MCP serwerze nad danymi medycznymi. Ten artykuł działa też samodzielnie: cały potrzebny kontekst jest niżej.

Punkt wyjścia: dostęp już był, planowania nie było

Problem nie był już z dostępem do danych — V2 to rozwiązał. Problem był z planowaniem: to ja musiałem być „agentem” łączącym kroki, wiedzieć, które z dwóch narzędzi wywołać i w jakiej kolejności. Wersja czwarta nie dodaje nowej analityki ani nowych narzędzi MCP — dokłada warstwę, która sama orkiestruje dwa istniejące narzędzia, planując kolejność wywołań, żeby odpowiedzieć na jedno, otwarte pytanie.

Zapytanie: „przygotuj mnie do wizyty u nefrologa"
↓
Agent — planowanie kroków (Claude Agent SDK)
↓
get_marker_trend — kreatynina, eGFR
compare_markers — pozostałe markery
↓
Synteza — cytuje wyłącznie zwrócone wartości
↓
Raport data/agent/*.md + pytania do lekarza
Agent V4 nie dostaje żadnego nowego narzędzia — planuje kolejność wywołań dwóch istniejących narzędzi z serwera MCP z części 1.

Claude Agent SDK — wygrał z zerowym kluczem API

Trzech kandydatów na framework agentowy: LangGraph, OpenAI Agents SDK, Claude Agent SDK. Wybrałem trzeci nie dlatego, że jest „oficjalny” — sprawdziłem spike’em, zanim cokolwiek zapisałem w PRD, że jako jedyny z trzech działa wyłącznie na moim już opłaconym abonamencie Claude Pro, bez zakupu tokenów API. W realnym wywołaniu apiKeySource wraca jako 'none', a zużycie idzie z puli rate_limit_type: 'five_hour' — subskrypcja, nie billing per token z Anthropic Console. Twarda zasada tego projektu, „zero kluczy API”, istniała już od pierwszej wersji. Framework musiał się jej podporządkować, nie odwrotnie — więc testowałem to, zanim cokolwiek zbudowałem, nie po fakcie.

Jedno zastrzeżenie, zanim ktoś przeniesie ten trik do pracy. Działa, bo to mój agent, moje dane i moja subskrypcja — Anthropic wprost zakłada, że limity Pro i Max obejmują zwykłe, indywidualne użycie Claude Code i Agent SDK. Agent zbudowany dla klienta albo udostępniony innym użytkownikom to już produkt: wymaga klucza API z Claude Console, a puszczanie cudzych zapytań przez własną subskrypcję jest zabronione i Anthropic egzekwuje to bez uprzedzenia (zasady uwierzytelniania). „Bez klucza API” to cecha projektu osobistego, nie wzorzec wdrożeniowy.

Pięć pól, które zawężają agenta do dwóch narzędzi

Domyślna sesja Claude Agent SDK dziedziczy pełny toolset Claude Code — Bash, Write, Edit, Read i resztę. Odkryłem to w pierwszym, niezawężonym teście: agent miał dostęp do rzeczy, których żadna wcześniejsza wersja tego projektu nie potrzebowała. Gorzej — sesja Agent SDK to pełnoprawna sesja Claude Code, z własnym mechanizmem pamięci międzysesyjnej. memory_paths w metadanych sesji wskazywał dokładnie na ten sam katalog pamięci, którego używają moje zwykłe sesje Claude Code w tym repo. To nowy wektor ryzyka, którego V1 i V2 nie miały: agent przetwarzający realne dane medyczne w swoim rozumowaniu mógłby teoretycznie zapisać ich fragment jako trwałą pamięć.

def build_options() -> ClaudeAgentOptions:
    return ClaudeAgentOptions(
        tools=[],
        mcp_servers={
            SERVER_NAME: {
                "type": "stdio",
                "command": sys.executable,
                "args": [str(MCP_SERVER_PATH)],
            }
        },
        allowed_tools=[
            f"mcp__{SERVER_NAME}__get_marker_trend",
            f"mcp__{SERVER_NAME}__compare_markers",
        ],
        setting_sources=[],
        system_prompt={
            "type": "preset",
            "preset": "claude_code",
            "append": CITATION_INSTRUCTION,
            "exclude_dynamic_sections": True,
        },
    )

Pięć pól, każde osobną barierą: tools=[] wyłącza wbudowany toolset Claude Code. mcp_servers to jedyne źródło narzędzi — te same trzy z serwera części 1, uruchamiane przez sys.executable, nie literał "python", żeby podproces serwera na pewno odpalił się tym samym środowiskiem, które ma zainstalowane zależności projektu. allowed_tools auto-zatwierdza dokładnie dwa z trzech widocznych narzędzi. setting_sources=[] pomija CLAUDE.md i ustawienia projektu spoza tego, co jawnie skonfigurowane. system_prompt.exclude_dynamic_sections usuwa auto-memory z promptu systemowego — skoro agent nie ma też Read ani Write, fizycznie nie może nic dopisać do plików pamięci, nawet gdyby chciał. Dwie niezależne warstwy tej samej bariery, nie jedna.

Pułapka: tools i allowed_tools to nie to samo

Nazwy sugerują to samo. Nie są. allowed_tools=[] brzmi jak „nic nie pozwól” — ale wcale nie ogranicza dostępności narzędzi, to tylko lista auto-zatwierdzenia, która pomija prompt o uprawnienia. Polem, które faktycznie ogranicza, co w ogóle jest widoczne, jest tools — pusta lista oznacza brak wbudowanych narzędzi Claude Code. Odkryłem to sprawdzając dataclasses.fields(ClaudeAgentOptions) wprost w kodzie SDK, nie z dokumentacji, bo sama dokumentacja tych dwóch pól jasno nie rozróżnia. Ten sam typ pomyłki, który rozłożyłem na czynniki przy Claude Code Skills vs Rules: dwa mechanizmy, podobna nazwa, przeciwna rola — jedno decyduje, co jest w ogóle widoczne, drugie, co wolno zrobić bez pytania o zgodę.

Pułapka: narzędzie spoza listy nie zawiesza procesu

Zanim zweryfikowałem to realnym wywołaniem, założyłem gorszy scenariusz — że agent poproszony o narzędzie spoza allowed_tools w trybie jednorazowym utknie na prośbie o zgodę, bo nie ma nikogo, kto by ją zatwierdził. Test kontrastowy pokazał inaczej. Dwa wywołania na tej samej konfiguracji: get_marker_trend (na liście) wykonało się bez pytania o cokolwiek. ping — widoczny w toolsecie, bo tools=[] dotyczy tylko wbudowanych narzędzi Claude Code, nie narzędzi z mcp_servers, ale spoza allowed_tools — został cicho odrzucony przez SDK. Bez zawieszenia, bez wyjątku, IS_ERROR zostało False. Model sam, w tekście odpowiedzi, grzecznie wytłumaczył, że nie ma uprawnień.

Różnica ma znaczenie dla diagnozy na przyszłość. Gdybym kiedyś pominął allowed_tools przy dopisywaniu trzeciego narzędzia, objawem nie byłby zawieszony proces — byłby nim agent uprzejmie tłumaczący się, że nic nie może zrobić. Funkcjonalna porażka kryterium sukcesu, nie techniczny hang. Warto to wiedzieć, zanim zaczniesz szukać buga w złym miejscu.

Rachunek, którego nikt nie zapłacił

ResultMessage.total_cost_usd po wywołaniu get_marker_trend pokazało realną liczbę — około 0,29 dolara. Pierwsza reakcja: czy to nie przeczy „zero kluczy API”? Nie przeczy. To metryka „ile by to kosztowało, gdyby rozliczać per token”, nie faktyczna opłata — billing idzie z puli pięciogodzinnego limitu subskrypcji Pro, tej samej, którą dzielę ze zwykłymi sesjami Claude Code w tym samym oknie czasowym. Świadomie zaakceptowane ograniczenie z PRD, nie coś do naprawienia: jeśli intensywnie testuję agenta, zjadam limit, który normalnie idzie na kodowanie.

Sign-off na realnych danych — i poprawka dopisana po fakcie

Dwa kryteria sukcesu, rosnącej złożoności. Pierwsze: pojedynczy marker bez wskazania narzędzia czy kodu wprost — np. „jak wygląda mój poziom ferrytyny w czasie”. Agent sam rozpoznał, które narzędzie wywołać i jaki kod markera podać, dostał jeden pomiar i poprawnie zgłosił, że to za mało punktów, żeby mówić o trendzie. Drugie: zadanie otwarte, wieloetapowe — „przygotuj mnie do wizyty u nefrologa”. Bez wskazania jednego markera agent sam zaplanował zestaw istotny nefrologicznie, wywołał compare_markers dla całego zestawu, zsyntetyzował trend i przygotował pytania do lekarza.

Fragment poglądowy — dane fikcyjne, nie moje realne wyniki:

Podsumowanie — kreatynina, eGFR, mocznik

  • Kreatynina: 0,9 → 1,0 mg/dl (2025-01-12 → 2026-06-30) — lekki wzrost w normie.
  • eGFR: 92 → 84 ml/min/1,73m² — spadek do kategorii G2 (łagodnie obniżony).
  • Brak danych: wapń, fosfor — istotne przy dalszej ocenie gospodarki mineralnej.

Pytania do lekarza: czy spadek eGFR o tej dynamice wymaga częstszej kontroli?

Obie realne odpowiedzi cytowały wyłącznie wartości faktycznie zwrócone przez narzędzia — zero halucynowanych liczb w żadnym z dwóch przebiegów. Ale n=2 to obserwacja, nie gwarancja, a PRD wprost nazywało to ryzyko w tabeli zagrożeń, zanim jeszcze cokolwiek uruchomiłem. Więc po sign-offie, nie przed nim, dopisałem CITATION_INSTRUCTION do system_prompt — jawny nakaz cytowania wyłącznie wartości zwróconych przez narzędzia, bez zgadywania. Nie wróciłem po tej zmianie do ponownego uruchomienia obu kryteriów — kolejne zużycie limitu bez konkretnej potrzeby byłoby dokładnie tym scope creepem, przed którym ostrzegałem sam siebie w części pierwszej tej serii. Dowód z sign-offu dotyczy configu sprzed tej poprawki. To instrukcja w prompcie, nie twardy walidator kodu — model teoretycznie mógłby ją zignorować. Świadomie nazwana granica, nie zamknięty temat.

Co z tego wynika

Dla mnie ta część serii to dowód na coś węższego niż „zbudowałem agenta”. To ten sam agentic coding workflow co w części 1, tylko jedną warstwę wyżej — nie piszę już promptu ręcznie za każdym razem, konfiguruję agenta, który sam go sobie złoży. I to trening w rozpoznawaniu, gdzie kończy się konfiguracja z dokumentacji, a zaczyna fakt, który trzeba sprawdzić w środowisku — różnica między tools a allowed_tools nie wynikła z uważnego czytania, wynikła z odpalenia dataclasses.fields() i zobaczenia różnicy na własne oczy. Ta sama dyscyplina, którą opisałem przy MCP serwerze nad danymi medycznymi: nie ufaj mechanizmowi, dopóki nie sprawdziłeś go empirycznie, nie w dokumentacji.

Transport sieciowy i tryb wieloturowy (ClaudeSDKClient) to świadomie odłożone pytania — wersja czwarta trzyma się jednorazowego query(), bo to wystarcza do obu kryteriów sukcesu, a nic więcej nie było jeszcze potwierdzoną potrzebą. Zero narzędzi zapisu, zero pamięci międzysesyjnej, zero kluczy API — trzy granice, które zostają, dopóki nie pojawi się konkretny powód, żeby je przesunąć.

Powiązane wpisy