gdamian.pl
← Wszystkie wpisy
11 min

MCP od zera: napisz własny serwer MCP w Pythonie i podłącz do Claude Code 

Damian GębalaAutor

Serwer MCP w Pythonie piszesz jako zwykłe funkcje z docstringami, a do Claude Code podłączasz go jedną komendą claude mcp add. Nie potrzebujesz płatnej platformy ani rejestracji - wystarczy Python 3.10+, pakiet mcp i kilkadziesiąt linii kodu. Poniżej pełna ścieżka: od pustego pliku do działającego narzędzia, które Claude wywoła w sesji.

Zacznę od tego, czego MCP nie jest, bo to najczęstsze rozumienie na starcie. MCP to protokół, nie biblioteka i nie usługa w chmurze. To sposób, w jaki klient (Claude Code) odkrywa, czym serwer potrafi, i wywołuje narzędzia z modelu językowego. Serwer to po prostu proces, który w odpowiedzi na żądanie oddaje JSON. Jeśli piszesz w Pythonie i umiesz przeczytać plik, masz wszystko, czego ten protokół wymaga.

Co potrzebujesz, zanim napiszesz pierwszą linię

Oficjalny SDK wymaga Pythona 3.10 lub nowszego. W praktyce tutoriale Microsoft-a zakładają 3.12 i taki obraz bazowy pojawia się w ich kontenerach, ale to wymaganie środowiska projektu, nie samego SDK.

Instalacja wygląda tak:

uv add "mcp[cli]"
# albo
pip install "mcp[cli]"

Ten nawias [cli] nie jest ozdobnikiem. Dodaje komendę mcp z podkomendami dev, run i install - bez niej nie odpalisz ani Inspectora, ani serwera przez CLI. Dodatek [cli] daje Ci dokładnie to, czego potrzebujesz do testowania. Jeśli pracujesz w venv i wolisz pip, to samo działa przez uv pip install mcp - wtedy dostajesz tylko bibliotekę, bez narzędzi deweloperskich.

Jedna decyzja, którą warto podjąć od razu: jeśli masz kod napisany jeszcze na pierwszej linii SDK, zostaw w wymaganiach górną granicę, na przykład mcp>=1.28,<2. Aktualna linia to już druga generacja SDK, a kod pisany pod pierwszą przestanie działać po migracji. Opisuję to niżej, bo to najczęstsze źródło zaskoczenia przy nauce.

Pierwszy serwer - czterna funkcje, zero boilerplate'u

W aktualnej, drugiej linii SDK klasa serwera nazywa się MCPServer. Cały serwer z jednym narzędziem i jednym zasobem wygląda tak:

from mcp.server import MCPServer

mcp = MCPServer("Demo")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b

@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
    """Greet someone by name."""
    return f"Hello, {name}!"

To jest kompletny serwer MCP. Udostępnia jedno narzędzie, add, i jeden szablonowy zasób, greeting://{name}. Żadnego kodu protokołu, parsowania ani walidacji.

Jeśli w internecie trafisz na from mcp.server.fastmcp import FastMCP, to nie inna biblioteka. To ta sama linia SDK, tylko z wysokopoziomową warstwą dekoratorową, która była domyślna w pierwszej generacji. Pakiet zawiera w sobie framework FastMCP, niskopoziomową implementację protokołu i handlery transportów. Dlatego kod FastMCP w większości tutoriali wciąż działa po pip install mcp, a przykłady dla Microsoft Learn pinują mcp[cli]>=1.2.0 właśnie dlatego, że napisano je pod pierwszą generacją.

Narzędzie, zasób, prompt - trzy różne rzeczy

W MCP są trzy rodzaje prymitywów i różnią się tym, co model może z nimi zrobić. To rozróżnienie decyduje o tym, gdzie umieścić daną funkcję.

Narzędzie (tool) wykonuje akcję. To ono pozwala modelowi faktycznie coś zrobić - policzyć, pobrać, zmienić. Dekorator @mcp.tool() automatycznie udostępnia funkcję klientowi wraz z informacją o typach i dokumentacji.

Zasób (resource) to dane tylko do odczytu. Definicja z dokumentacji MCP: zasoby są sposobem udostępniania danych modelom językowym, podobne do endpointów GET w API REST - dostarczają danych, ale nie powinny wykonywać istotnych obliczeń ani mieć skutków ubocznych. Praktyczny przykład z tutoriala: schemat bazy danych w zasobie schema://database i konfiguracja aplikacji w config://app-settings.

@mcp.resource("config://app-settings")
def get_app_settings() -> str:
    """Return current application settings."""
    return json.dumps({
        "max_file_size": "10MB",
        "supported_formats": ["txt", "md", "csv", "json"],
        "rate_limit": 100
    }, indent=2)

Zasoby są wymieniane przy połączeniu i można je czytać w dowolnym momencie rozmowy. Prompt to trzeci typ: szablon tekstu, który klient może wstawić do rozmowy, gdy użytkownik go wywoła.

@mcp.prompt()
async def greeting_prompt(name: str) -> str:
    """A simple greeting prompt."""
    return f"Greet {name} kindly."

Listę możliwości serwera sprawdzisz programowo przez list_tools(), list_prompts() i list_resources().

Schemat wejścia powstaje z typów Pythona

Tu jest największa oszczędność pracy w całym temacie. Nie piszesz schematu JSON. Piszesz typy.

FastMCP używa adnotacji typów Pythona i docstringów, żeby wygenerować JSON Schema, którego wymaga klient MCP. Parametr text: str staje się wymaganym stringiem w schemacie, a include_spaces: bool = True - opcjonalnym booleanem z wartością domyślną. Docstring staje się opisem narzędzia, który AI czyta, żeby zrozumieć, kiedy i jak go użyć.

Dokumentacja SDK ujmuje to wprost: schematem jest a: int, b: int, bez parsowania requestu, bez serializacji, bez kodu walidacji, bez obsługi protokołu. Napisałeś dwie funkcje z adnotacjami typów i docstringami, resztę zrobił SDK. Inspector zbudował z tego formularz z dwoma wymaganymi polami liczbowymi. Zbuduje go też Claude i każdy inny host MCP.

Praktyczna konsekwencja: opis w docstringu pisz pod model, nie pod siebie. „Policz słowa w tekście" daje Claude mniej do kombinowania niż „Funkcja pomocnicza nr 3". Przy większych serwerach różnica w jakości wywołań jest widoczna od razu. To ta sama logika, którą opisuję w tekście o tym, dlaczego prompt engineering sam w sobie już nie wystarcza, bo opis narzędzenia robi więcej niż dopracowana komenda.

Uruchomienie i sprawdzenie, zanim cokolwiek podłączysz

Nie podłączaj serwera do Claude Code, dopóki sam go nie zobaczyłeś. SDK ma do tego wbudowane narzędzie:

uv run mcp dev server.py

To startuje serwer i otwiera MCP Inspectora - interaktywny interfejs do dłubania w nim. Otwierasz adres, który wypisze, przechodzisz do zakładki Tools i wołasz add z a=1 i b=2. Dostajesz trójkę. Inspector to aplikacja Node.js, więc mcp dev potrzebuje npx w PATH. Wariant spotkasz też jako npx @modelcontextprotocol/inspector uv run src/server.py - to ta sama rzecz, tylko wywołana inaczej.

Jeśli nie chcesz zależeć od Node.js, napisz test klientem SDK. To dwadzieścia linii, które dają ci gotowy smoke test do CI, gdzie nie ma interfejsu:

# test_server.py
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def test():
    server_params = StdioServerParameters(
        command="python",
        args=["server.py"]
    )
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print(f"Available tools: {[t.name for t in tools.tools]}")
            result = await session.call_tool("count_words", {"text": "Hello world test"})
            print(f"Result: {result.content[0].text}")

asyncio.run(test())

W obu wariantach serwer startuje lokalnie jako proces stdio. Ten transport używa standardowego wejścia i wyjścia do komunikacji między procesami na jednej maszynie.

Podłączenie do Claude Code

Tutaj najczęściej kończy się artykuły o MCP, bo przechodzą do Claude Desktop, a to inny klient z innym plikiem konfiguracyjnym. Claude Desktop czyta claude_desktop_config.json i po edycji wymaga restartu aplikacji. Claude Code konfiguruje się komendą, którą możesz wkleić w terminal.

claude mcp add --transport stdio myserver -- python server.py

Separator -- jest tu obowiązkowy i to nie jest detal. Claude Code używa go, żeby oddzielić własne opcje, takie jak --transport, --env i --scope, od komendy uruchamiającej serwer. Wszystko po -- przechodzi do serwera nietknięte. Bez tego Claude Code spróbuje zinterpretować flagi serwera, na przykład --port, jako własne opcje.

Sekwencja weryfikacji jest krótka:

claude mcp list
claude mcp get myserver

claude mcp list pokazuje status zdrowia przy każdym serwerze: ✔ Connected, ! Needs authentication albo ✘ Failed to connect. W trakcie sesji status sprawdzisz komendą /mcp. Jeśli dopiero zaczynasz pracę z Claude Code, trzy tygodnie pracy z Claude Code i siedem rzeczy, które naprawdę przyspieszają pokażą, gdzie MCP wypada w codziennej pracy. Usunięcie: claude mcp remove myserver.

Do zespołu ten sam serwer oddasz przez --scope project albo wpis w .mcp.json w katalogu projektu, który idzie do repozytorium:

{
  "mcpServers": {
    "mcpServers-example": {
      "command": "npx",
      "args": ["-y", "@example/mcp-server"]
    }
  }
}

Trzy zakresy rozróżniają, gdzie żyje definicja. local to domyślny zakres, serwer widzisz tylko ty i tylko w bieżącym projekcie. project zapisuje do .mcp.json, więc zespół dziedziczy go przez wersjonowanie. user czyni serwer dostępnym we wszystkich Twoich projektach.

Jeśli serwer potrzebuje sekretu, podajesz go przez --env. Jest tu haczyk w kolejności flag: --env przyjmuje wiele par KEY=value, a gdy nazwa serwera stoi bezpośrednio po --env, CLI wczyta nazwę jako kolejną parę i odrzuci polecenie. Wstaw między nimi choćby --transport stdio. Przykład z dokumentacji wygląda tak:

claude mcp add --env KEY=value --transport stdio myserver -- python server.py --port 8080

Uruchamia to python server.py --port 8080 ze zmienną KEY w środowisku. Przy testowaniu serwerów, które czytają pliki projektu, przydatna jest zmienna CLAUDE_PROJECT_DIR - Claude Code ustawia ją w środowisku uruchomionego serwera na katalog główny projektu, więc ścieżki względne nie zależą od katalogu roboczego. Czytasz ją w Pythonie przez os.environ["CLAUDE_PROJECT_DIR"].

Kiedy stdio nie wystarczy

Transport stdio jest właściwy, dopóki serwer żyje na maszynie klienta. Gdy ma być dostępny dla zespołu albo działać jako usługa, przechodzisz na HTTP.

Oficjalna dokumentacja Claude Code mówi wprost, że transport SSE jest wycofany i należy używać serwerów HTTP. Powód widać w historii protokołu: przy SSE klient musiał trzymać dwa różne endpointy - jeden do utrzymania strumienia zdarzeń, drugi do wysyłania żądań. Streamable HTTP scala to w jeden endpoint, pozwala serwerowi wysyłać powiadomienia tą samą ścieżką i startuje połączenie jako zwykły request HTTP, który może dynamicznie przejść w SSE przy długich zadaniach.

Serwer HTTP z FastMCP uruchamiasz jedną komendą:

uv run mcp run server.py --transport streamable-http

W Claude Code wpisujesz go jako claude mcp add --transport http myserver http://localhost:8000/mcp. W polu type w JSON-ie streamable-http jest aliasem http, więc konfiguracje kopiowane z dokumentacji serwerów działają bez poprawek.

Zanim jednak coś postawisz na własnym serwerze, samodzielny hosting AI i prywatność w erze chmury opisuje, kiedy własny hosting ma sens, a kiedy to tylko dodatkowa infrastruktura do utrzymania. Do serwera HTTP podchodzą też standardowe narzędzia aplikacji. Klasyczny wariant to FastAPI, w którym aplikacja MCP montujesz pod roota, a endpoint /mcp dostajesz z ustawień domyślnych SDK, plus osobny /health do probe'ów - endpointy MCP przyjmują POST w formacie JSON-RPC i nie nadają się na kontrole stanu:

app = FastAPI(lifespan=lifespan)
app.mount("/", mcp.streamable_http_app())

@app.get("/health")
async def health():
    return JSONResponse({"status": "healthy"})

Jeśli klient nie ma jeszcze natywnego wsparcia dla zdalnych serwerów, ratuje Cię mcp-remote - adapter, który pozwala klientom obsługującym wyłącznie połączenia lokalne pracować ze zdalnymi serwerami MCP.

Pułapki, które kosztują najwięcej czasu

Spojrzałem na to, co wraca w pytaniach i komentarzach pod materiałami o MCP najczęściej. Krótka lista rzeczy, które psuły działanie:

Wpis JSON z polem url, ale bez type, to błąd konfiguracji - Claude Code czyta wpis bez typu jako serwer stdio. Do tego wpis deklarujący type: "sdk" zostanie pominięty z raportem, że taki typ potrafi zarejestrować tylko aplikacja hostująca SDK. Nazwy serwerów wbudowanych są zarezerwowane, w tym workspace, claude-in-chrome i computer-use. Ukryta spacja na początku lub końcu wartości w konfiguracji daje ostrzeżenie, ale nie jest obcinana, więc wklejony token z końcowym znakiem nowej linii nie zadziała.

Osobno stoi bezpieczeństwo. Serwery pobierające treści z zewnątrz wystawiają cię na prompt injection, więc traktujesz zescrapowane strony i komentarze w repozytoriach jako dane, nie jako instrukcje. Szerzej o tym, gdzie kończy się zaufanie do treści z zewnętrznych źródeł, w artykule o cyberbezpieczeństwie napędzanym przez AI. Druga sprawa jest bliższa twojemu kodowi: narzędzia MCP nie powinny rzucać nieobsłużonych wyjątków, bo protokół nie definiuje, jak klient ma wyświetlić surowy traceback. Zamiast tego zwróć błąd jako tekst strukturalny. Waliduj wejście wcześnie, łap oczekiwane wyjątki i przy zależności od usługi zewnętrznej obsłuż timeout oraz błędy uwierzytelniania. Przy narzędziach z operacjami nieodwracalnymi - zapis pliku, wykonanie zapytania, wysłanie wiadomości - daj mechanizm potwierdzenia, na przykład parametr confirm: bool albo podgląd przed wykonaniem.

Na koniec dwie rzeczy o wynikach. Claude Code ostrzega, gdy wyjście narzędzia MCP przekracza 10 000 tokenów, i domyślnie obcina je na 25 000; limit podnosi zmienna MAX_MCP_OUTPUT_TOKENS. Nie ma przy tym sztywnego limitu liczby narzędzi na serwer - realne ograniczenie to budżet kontekstu. Serwer duplikujący narzędzie wbudowane w Claude Code jest więc stratą: obsługa plików powtarza Read, Write, Edit, Grep i Glob.

Następny krok: serwer, który czyta Twoje pliki

Znasz już protokół, umiesz napisać narzędzie i podłączyć je do klienta. Najprostszy sensowny use case to serwer, który czyta pliki z projektu - wtedy agent nie zgaduje struktury katalogu, tylko pyta o nią. Klasa MCPClient ze standardowej biblioteki i stdio_client dają Ci do tego wygodną podstawę. Jeśli myślisz o tym szerzej, nie tylko dla siebie, ale jako narzędzie w procesie firmowym, jak zbudować własnego asystenta łączącego model językowy z resztą systemu pokazuje ten sam mechanizm od strony integracji.

Zanim zaczniesz pisać, ustal jedną rzecz: czy to narzędzie ma działać tylko u Ciebie na maszynie, czy być dostępne dla zespołu. Od tego zależy stdio albo HTTP, a decyzja jest nieodwracalna na poziomie hostowania i autoryzacji. Kolejne decyzje - własne narzędzie zamiast gotowego serwera, zakres wdrożenia, kto to utrzymuje - są w szybkim audycie procesów, od czego zacząć automatyzację w firmie.

Robisz coś podobnego u siebie?Napisz do mnie

Masz proces, który zjada tydzień pracy? Odzyskajmy ten tydzień.

Napisz do mnie