Wszystkie wpisy

1 sierpnia 2026

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.

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, wciąż powyżej progu zaawansowanej CKD.
  • 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