W zeszłym roku zbudowałem dla siebie pięć MCP serwerów - Garmina, kalendarz, taski, własną bazę notatek i jeden pod konkretnego klienta, który chciał spiąć Claude'a ze swoim CRM-em w Laravelu. Z każdym kolejnym projektem czas od pomysłu do działającego prototypu spadał - z dwóch dni na pierwszym do dwóch godzin na ostatnim. Nie dlatego, że MCP jest skomplikowane. Dlatego, że oficjalne tutoriale pokazują "hello world", a w realnym setupie wywalają się rzeczy, których w hello worldzie nie ma.
Ten wpis to tutorial, który chciałbym znaleźć rok temu. Krok po kroku, z działającym kodem, w Pythonie 3.10+, z FastMCP 3 (wyszedł w lutym 2026 i mocno uprościł API), z podłączeniem do Claude Desktop i debugiem w MCP Inspectorze. Bez teorii, której nie potrzebujesz, żeby zacząć - jeśli chcesz teorii, mam osobny wpis o MCP.
Przykładem przewodnim będzie serwer pogodowy spinający Open-Meteo (darmowe, bez klucza API). Po przejściu tego tutoriala będziesz mieć działający serwer, który Claude może wywoływać, i będziesz wiedzieć, co zmienić, żeby zamiast pogody serwować dane z własnego systemu.
Co to jest MCP w dwie minuty
Model Context Protocol to standard otwarty przez Anthropic w listopadzie 2024, który pozwala modelom (Claude, ale też inne) gadać ze światem zewnętrznym przez ujednolicony interfejs. Zamiast pisać własny adapter do każdej integracji, wystawiasz MCP server i każdy klient zgodny z protokołem (Claude Desktop, Claude Code, IDE pluginy, własne aplikacje) potrafi z niego skorzystać.
Server eksponuje trzy rzeczy:
- Tools - funkcje, które model może wywołać (np.
get_weather(city)) - Resources - dane do pobrania (pliki, dokumenty, wyniki query)
- Prompts - parametryzowane szablony promptów
W 2026 publicznych MCP serwerów jest już ponad 7 tysięcy. Anthropic, Google, OpenAI - wszyscy dorobili się klientów. Standard się przyjął i nie zniknie.
Wymagania
- Python 3.10 lub nowszy (FastMCP wymaga, type hints na pełen gaz)
- Klient MCP - polecam Claude Desktop (najłatwiejszy do testów) albo Claude Code
- Edytor tekstu, terminal
- 10-15 minut
Zakładam, że umiesz Pythona na poziomie podstawowym i radzisz sobie z pip, virtualenv, plikami JSON konfiguracyjnymi.
Krok 1: instalacja FastMCP
FastMCP to wrapper na oficjalny modelcontextprotocol/python-sdk Anthropic, który redukuje boilerplate o jakieś 80%. Zamiast definiować schematy JSON ręcznie, używasz dekoratorów na funkcjach Pythona. Type hinty same się tłumaczą na schematy.
mkdir mcp-pogoda
cd mcp-pogoda
python -m venv .venv
source .venv/bin/activate httpx dorzucam, bo będę robił async HTTP do Open-Meteo. Standardowo używam go zamiast requests w nowoczesnych projektach.
Krok 2: pierwszy tool
Tworzę plik server.py:
from fastmcp import FastMCP
import httpx
mcp = FastMCP(Co tu się dzieje:
FastMCP("Pogoda PL")tworzy instancję serwera z nazwą widoczną dla klienta.@mcp.tool()rejestruje funkcję jako tool dostępny dla modelu. FastMCP czyta type hinty (city: str,-> dict) i docstring, generuje schemat JSON automatycznie.- Docstring jest krytyczny. Model widzi go jako opis tool'a i decyduje na podstawie tego, czy wywołać. Pisz konkretnie - nie "pobiera dane" tylko "pobiera aktualną pogodę dla miasta".
asynctutaj jest po to, że robię HTTP. FastMCP obsługuje sync i async tooly bez różnicy.mcp.run()startuje serwer w trybie stdio (default) - klient wystartuje proces i będzie gadał przez standard input/output.
Sprawdź, czy startuje:
pythonPowinien zawisnąć (czeka na klienta na stdio). Ctrl+C, idziemy dalej.
Krok 3: dodanie zasobów
Resources to dane, które model może czytać bez wywoływania funkcji. Typowy use case: model dostaje URI typu weather://warsaw/today i FastMCP zwraca treść.
Dodaję do server.py:
from datetime import datetime
Różnica między tool a resource jest taka: tool jest akcją (model decyduje, czy wywołać), resource jest danymi (klient może załadować bez decyzji modelu, np. dołączyć do kontekstu na start). W praktyce 80% projektów potrzebuje głównie tooli, resources są dodatkiem.
Krok 4: podłączenie do Claude Desktop
Claude Desktop czyta konfigurację z pliku claude_desktop_config.json. Lokalizacja:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Dodaję serwer:
{
Ścieżki muszą być absolutne. Claude Desktop nie rozwiązuje ~ ani relatywnych ścieżek. Restart Claude Desktopa (pełny quit, nie tylko zamknięcie okna) i serwer powinien się pojawić w UI - ikonka młotka w prawym dolnym rogu okna chatu pokaże dostępne tooly.
Jeśli się nie pojawia, sprawdź logi:
- macOS:
~/Library/Logs/Claude/mcp-server-pogoda.log - Linux:
~/.local/share/Claude/logs/mcp-server-pogoda.log
Najczęstszy błąd, który sam popełniałem: Python z venv ma inną ścieżkę niż systemowy, a w configu mam systemowy. Wtedy fastmcp nie jest zainstalowany i serwer pada na imporcie.
Krok 5: debug z MCP Inspector
MCP Inspector to oficjalne narzędzie do testowania serwerów bez podłączania do prawdziwego klienta. Zachowuje się jak interaktywny REPL - widzisz każdy request i response w surowej formie.
npx @modelcontextprotocol/inspector python /sciezka/Otworzy się przeglądarka na http://localhost:5173. Tam widzisz listę tooli, możesz wywołać je z konkretnymi argumentami i zobaczyć surowy JSON-RPC, który leci między stronami.
Gdy jakiś tool wywala się "u Claude'a", zawsze najpierw sprawdzam w Inspectorze. Jeśli tam działa - problem jest po stronie klienta (config, format argumentów, uprawnienia). Jeśli tam pada - problem jest w samym serwerze i mogę debugować na poziomie kodu Python bez pośrednika.
Drugi tip: Inspector ma podgląd schematów. Jeśli twój type hint generuje dziwny schemat JSON (np. Optional[str] robi się {"type": ["string", "null"]} co modele czasem mylą), tutaj to wyłapiesz.
Krok 6: stdio vs HTTP
Domyślny tryb FastMCP to stdio - klient startuje proces serwera lokalnie i gada przez stdin/stdout. Działa świetnie dla osobistych integracji (jak mój Garmin/kalendarz/taski). Ograniczenie: server musi żyć na tej samej maszynie co klient.
Dla deploymentu zdalnego (np. firmowy serwer wystawiający MCP wielu pracownikom) FastMCP wspiera HTTP/SSE:
ifKlient (Claude Desktop od wersji 2026.03 wspiera remote MCP) podpina się przez URL zamiast komendy:
{
Trzy uwagi praktyczne, kiedy idziesz na HTTP:
- Uwierzytelnianie. Stdio jest bezpieczne z definicji - proces lokalny, ten sam użytkownik. HTTP nie. Dodaj OAuth (FastMCP 3 ma wbudowane wsparcie) albo bearer tokeny, inaczej serwer będzie publiczny dla każdego, kto trafi na URL.
- CORS i SSE. Klient negocjuje SSE (Server-Sent Events) dla streamingu. Jeśli stoi za reverse proxy, ustaw
proxy_buffering offw nginx, inaczej klient się rozłączy po pierwszym evencie. - Stateless tools. Stdio może mieć stan w pamięci procesu. HTTP - z założenia - nie. Każdy request może trafić na inny worker. Trzymaj stan w bazie/Redisie.
Test: wywołanie z Claude Desktop
Po restarcie Claude Desktopa otwieram nowy chat i piszę:
Sprawdź pogodę w Krakowie.
Claude wyświetli pytanie o uprawnienie wywołania get_weather (Claude Desktop pyta domyślnie pierwszy raz dla każdego tool'a). Po zatwierdzeniu zobaczysz w UI rozwijaną sekcję z wywołaniem - argument {"city": "Kraków"} i wynik JSON z polami temperature_c, humidity, wind_kph. Model przerobi to na ludzką odpowiedź typu "W Krakowie mamy 12 stopni, wilgotność 64%, wiatr 8 km/h".
Jeśli widzisz tylko odpowiedź modelu bez wywołania tool'a - znaczy że Claude wziął odpowiedź z własnej wiedzy zamiast użyć serwera. Przyczyny:
- Tool nie został zarejestrowany (sprawdź ikonkę młotka w UI - pokazuje listę dostępnych tooli)
- Docstring jest zbyt ogólny - model nie skojarzył "pogoda" z funkcją
get_weather. Dopisz w docstringu konkretne triggery: "wywołaj zawsze gdy użytkownik pyta o pogodę, temperaturę, wiatr w konkretnym mieście". - Tool ma za długi opis - model zignorował go, bo nie zmieścił się w prompcie systemowym (rzadkie, ale przy 50+ toolach realne).
Pułapki, na które wpadałem
Asynchroniczność tooli mieszana z synchroniczną. Jeden tool jest async def, drugi def, trzeci wywołuje synchroniczny kod blokujący w środku async. FastMCP sobie z tym poradzi, ale bloking calls w async tool'u zatrzymują event loop i cały serwer staje. Reguła: w async tool'u używaj async libów (httpx.AsyncClient, asyncpg), nie requests i psycopg2.
Timeouty. Domyślnie klient czeka na tool 30 sekund. Jeśli twój tool robi długie zapytanie do API, pobierając plik 100MB, klient go ubije. Buduj tool, żeby zwracał w < 10 sekund. Jeśli musi dłużej - wystaw status job'a jako resource i niech model pollu je.
Format argumentów. Model czasem podaje argumenty jako string ("True", "5"), zamiast jako bool/int. FastMCP robi coercion, ale jeśli definiujesz złożone typy (Pydantic models, dicty zagnieżdżone), tutaj się gubi. Sprawdzaj w Inspectorze, co dokładnie przychodzi.
Permissions na plikach. Jeśli MCP server czyta pliki użytkownika (RAG nad Documents), pamiętaj, że proces dziedziczy uprawnienia od klienta, który go wystartował. Claude Desktop na macOS chodzi w sandboxie - próba czytania ~/Documents bez explicit permissions z systemu wywoli pyt o dostęp lub błąd.
JSON config. Brak przecinka, niepoprawny escape backslashy w ścieżkach Windows, BOM w pliku - każdy z tych błędów objawia się tym, że Claude Desktop ignoruje config bez komunikatu. Walidacja przez jq . claude_desktop_config.json ratuje życie.
Zwracanie surowych obiektów, których FastMCP nie umie zserializować. Zwróciłem kiedyś z tool'a obiekt datetime bezpośrednio. FastMCP używa standardowego JSON serializera, który nie umie datetime - klient dostał błąd "Object of type datetime is not JSON serializable" i tool wyglądał na zepsuty. Reguła: zwracaj tylko stringi, liczby, boole, listy, dicty z tych typów. Daty konwertuj do ISO string (dt.isoformat()).
Co dalej
Powyższy serwer jest funkcjonalny, ale to dopiero szkielet. Przy produkcyjnym wdrożeniu dochodzą dodatkowe rzeczy:
- Testy. FastMCP daje
fastmcp.testing.Client- możesz wywoływać tooly w pytestach bez startowania prawdziwego serwera. Pisz testy od razu, bo bez nich dowiesz się o regresji od użytkownika. - Logging i observability. Loguj każde wywołanie tool'a (input, output, latency). Bez tego debugowanie problemów u klienta to zgadywanka. Ja używam
structlogz JSON output i wpinam do Grafany. - Rate limiting. Jeśli tool uderza w zewnętrzne API z limitami, zaimplementuj backoff i circuit breaker, inaczej klient dostanie kaskadę błędów.
- Caching. Wynik tool'a, który nie zmienia się szybko (np. lista produktów z katalogu), cache'uj. Redis z TTL 5-15 minut robi robotę.
- Publikacja. Jeśli serwer ma sens publiczny - opublikuj na PyPI (
pip install moj-mcp-server) plus dodaj do oficjalnego registru MCP. Claude Desktop ma od marca 2026 wbudowaną przeglądarkę registru, użytkownicy klikają "install" i mają. - Wersjonowanie tooli. Jeśli zmieniasz sygnaturę tool'a, którego ktoś już używa, dodaj nowy tool obok zamiast łamać stary. Model nie rozróżnia "to samo, ale lepsze" - widzi po prostu zmieniony schemat i może zacząć generować błędne wywołania. Stary deprecuj w docstringu, dopisując "Deprecated, use X instead".
- Bezpieczeństwo input'u. Tool przyjmuje stringi od modelu. Jeśli wewnątrz robisz query SQL, exec shell command, czytasz pliki po path - sanityzuj. Nie zakładaj, że "model nie zrobi prompt injection". Atakujący może podsunąć użytkownikowi treść, która namówi model na wywołanie twojego tool'a w niezamierzony sposób.
Dla porównania, jak to wygląda w realnym projekcie - mój własny serwer Garmin (czyta dane treningowe, sen, HRV i podaje agentowi biegowemu) ma w sumie 800 linii Pythona, 12 tooli, własną warstwę cache i autentykację OAuth. Zajęło mi to weekend. Bez FastMCP zajęłoby tydzień. Pisałem o tym w case study osobistego asystenta, jeśli chcesz zobaczyć, jak wygląda szerszy kontekst takiego serwera w produkcji.
Drugi przykład - integracja Claude Code z lokalną bazą notatek przez MCP - opisałem w wpisie o Claude Code w codziennej pracy. Tam pokazuję, jak agent w terminalu sięga po MCP server tylko wtedy, kiedy faktycznie potrzebuje, zamiast trzymać całą wiedzę w prompcie.
Potrzebujesz MCP serwera spiętego z systemem firmy (CRM, ERP, własna baza wiedzy)? Napisz do mnie - projektuję i buduję pod konkretny use case.