Stwórz swój pierwszy serwer MCP w Pythonie: narzędzia, zasoby, polecenia i inspektor
Stwórz serwer MCP w Pythonie za pomocą FastMCP: narzędzie pomocnicze z funkcjami, zasobami i szablonami zapytań, opcją pracy przez stdio lub HTTP, możliwością debugowania za pomocą inspektora oraz jedną konfiguracją dla 3 klientów.
Wyobraź sobie trzy niemal identyczne pliki w języku Python, nazwane Claude, Cursor i ChatGPT, z których każdy korzysta z API Slack, analizuje odpowiedź i przekształca ją pod kątem konkretnego klienta AI. Teraz wyobraź sobie zastąpienie tych trzech pojedynczym plikiem, z którego każdy z tych klientów może bezpośrednio czytać dane. Gdy Slack zmieni jakieś pole, wystarczy je poprawić tylko raz. Ten pojedynczy plik to serwer MCP, a jego stworzenie zajmuje około popołudnia.
MCP w kilku zdaniach
Model Context Protocol jest na tyle prosty, że można go opisać w trzech wiadomościach. Klient łączy się z serwerem i pyta, co może robić. Serwer odpowiada, jakie narzędzia może używać model, jakie dane może pobrać host oraz jakie prompty może wybrać użytkownik. Następnie klient wywołuje dane narzędzie z argumentami, serwer je uruchamia i odpowiada. To wszystko, co odbywa się w transmisji danych.
To, co sprawia, że MCP jest wartościowe do używania, to nie format wiadomości, lecz wspólna umowa. Claude Code, Cursor, Copilot w VS Code i inne narzędzia realizują ten sam mechanizm współpracy, dzięki czemu plik w języku Python napisany obecnie może zostać później dołączony do dowolnego z nich.
Server może oferować jedną z trzech rodzajów możliwości:
- Narzędzia to funkcje, które można wywołać – model wybiera je po nazwie, na przykład taką, która wyszukuje dane w bazie danych, wysyła gałąź lub skraca logi.
- Zasoby to materiały dostępne wyłącznie do odczytu, takie jak plik konfiguracyjny, schemat lub najnowsze komity, które host pobiera, gdy potrzebuje dodatkowych informacji w tle.
- Wzory zapytań to szablony, które można wielokrotnie używać – użytkownik je wybiera; pojawiają się one jako polecenie z kreską w Claude Code lub jako pozycja w menu w Claude Desktop.
W praktyce większość serwerów dostarcza narzędzia, a zasoby i instrukcje są ważne w drugiej kolejności. Jeśli masz nauczyć się tylko jednej z tych trzech rzeczy, to koniecznie narzędzi. To, jak przekazywane są wiadomości i jak działa autoryzacja, ma znaczenie później. Na razie pamiętaj o tym: jeden serwer, wielu klientów – trzy rzeczy do udostępnienia.
Dlaczego integracje kiedyś się mnożyły
Przed MCP połączenie narzędzia AI z backendem oznaczało pisanie adaptera, który dostosowywałby strukturę backendu do formatu wywoływania narzędzi wymaganego przez danego klienta. Jeden klient oczekiwał schematu narzędzi Anthropic, inny używał własnego formatu, a trzeci wymagał struktury wywoływania funkcji OpenAI. Funkcja wyszukiwania w Slacku pozostawała niezmienna, podczas gdy wokół niej rozwijały się trzy warstwy otaczające.
Matematyka szybko staje się skomplikowana. Trzy klienty i dwa backendy oznaczają sześć adapterów do utrzymania. Czwarty klient daje osiem, a trzeci backend – dwanaście, przy czym każdy z nich ma własną autoryzację, mechanizmy ponawiania prób i specyficzne cechy. MCP rozdziela ten układ – klienci i backendy już nie komunikują się ze sobą. Oba komunikują się z protokołem, więc wdrożenie nowego klienta AI wymaga jedynie dodania wpisu do konfiguracji, a nie nowej integracji.
Konstrukcja jest celowo nierównowaga. Klient decyduje o wszystkim: o tym, które serwery załadować, kiedy wywołać narzędzia i jak wyświetlić wyniki. Obowiązki serwera są znacznie mniejsze – on jedynie deklaruje swoje narzędzia i odpowiada po ich wywołaniu. Ta asymetria sprawia, że protokół pozostaje wystarczająco zwięzły, aby każdy duży klient mógł go szybko zaimplementować.
Pomocnik do standupu w jednym pliku
Pomocnik do standupu to klasyczne narzędzie wewnętrzne, które inżynierowie ciągle przepisują. Czyta lokalny log git i podsumowuje, co programista robił wczoraj. Tutaj zostanie przedstawiony jako narzędzie, zasób oraz prompt w jednym pliku Pythona.
Potrzebny jest pakiet FastMCP, warstwa Pythona wysokiego poziomu, na której opierają się wiele serwerów MCP.
pip install fastmcp
Stwórz plik standup_server.py. Zaczyna się od imporowania biblioteki i zdefiniowania narzędzia.
# standup_server.py
import subprocess
from typing import TypedDict
from fastmcp import FastMCP
mcp = FastMCP("standup-helper")
class StandupSummary(TypedDict):
branch: str
since: str
commit_count: int
commits: list[str]
@mcp.tool()
def summarize_standup(
branch: str = "main",
since: str = "yesterday",
) -> StandupSummary:
"""Summarize recent git activity for a standup.
Reads the local git log on the given branch since the
given time window. Returns commit count and one-line
subjects for each commit. Used by AI clients via MCP.
"""
try:
result = subprocess.run(
[
"git", "log",
f"--since={since}",
"--pretty=format:%h %s",
branch,
],
capture_output=True,
text=True,
timeout=5,
check=True,
)
except (subprocess.CalledProcessError,
subprocess.TimeoutExpired) as exc:
return {
"branch": branch,
"since": since,
"commit_count": 0,
"commits": [f"git error: {exc}"],
}
lines = [
line for line in result.stdout.splitlines() if line
]
return {
"branch": branch,
"since": since,
"commit_count": len(lines),
"commits": lines,
}
# resources and prompts come next
Za kulisami dekorator pełni cztery funkcje. Czyta wskazówki typowe, aby stworzyć schemat JSON, wykorzystuje dokumentację jako opis pokazywany modelowi, wymienia narzędzie wśród możliwości serwera podczas nawiązywania połączenia oraz kieruje przychodzące żądania JSON-RPC do odpowiedniej funkcji.
Zwróć uwagę na wywołanie podprocesu z ściśle określonym limitem czasowym wynoszącym pięć sekund. Jeśli polecenie git utknie, aplikacja hostująca, która czeka na odpowiedź narzędzia, również się zatrzyma, dlatego każde wywołanie zewnętrzne musi mieć limit czasu. Gałąź z błędem zwraca również taką samą strukturę słownika jak gałąź z powodzeniem, dzięki czemu model zawsze otrzymuje przewidywalną strukturę danych. Bardziej zaawansowane narzędzie może również wywołać GitHub CLI w celu sprawdzenia otwartych pull requestów, dodając kolejne wywołanie podprocesu i dołączając jego wynik do słownika.
Następnie przychodzą zasoby oraz prompt, które uzupełniają plik.
# standup_server.py (continued)
@mcp.resource("recent_commits://main")
def recent_commits_main() -> str:
"""Last 10 commits on the main branch, plain text.
Resources are pulled by the host opportunistically.
They are not invoked by the model the way tools are.
"""
result = subprocess.run(
[
"git", "log",
"-n", "10",
"--pretty=format:%h %ad %s",
"--date=short",
"main",
],
capture_output=True,
text=True,
timeout=5,
)
return result.stdout or "(no commits found)"
@mcp.prompt("standup_template")
def standup_template(focus: str = "shipping work") -> str:
"""Reusable standup question exposed as a prompt
template. Surfaces as a slash command in clients that
expose prompts (e.g. /standup_template in Claude Code).
"""
return (
f"Summarize what I worked on yesterday, focusing on "
f"{focus}. Use the summarize_standup tool to get the "
f"git log, then write a one-paragraph standup note."
)
if __name__ == "__main__":
mcp.run()
URI zasobu to po prostu ciąg znaków. Jego strukturę nie reguluje żadne ścisłe schemat, chociaż konwencją jest schemat następujący po ścieżce, a FastMCP obsługuje szablony URI, gdy potrzebne są parametry. Prompt to po prostu funkcja zwracająca sformatowany tekst. Klienci prezentują go jako szablon, który użytkownik wywołuje, a Claude Code udostępnia go jako polecenie z użyciem słowa „/”. Prompty nigdy nie są wstrzykiwane automatycznie, więc użytkownik zawsze je wybiera.
Ostatni wiersz uruchamia serwer. Nie ma żadnego portu, żadnego frameworku webowego ani pętli zdarzeń do zarządzania, ponieważ FastMCP zajmuje się obsługą standardowych operacji wejścia/wyjścia.
Transport i autoryzacja
Transport decyduje o tym, w jaki sposób wiadomości JSON-RPC przemieszczają się pomiędzy klientem a serwerem.
Użyj stdio dla serwerów lokalnych. Host uruchamia twój skrypt jako proces potomny. Stdio omija całą warstwę sieciową: nie ma portu, nie ma TLS ani autoryzacji, ponieważ proces potomny dziedziczy granice zaufania hosta.
Użyj Streamable HTTP, gdy serwer musi przechowywać stan pomiędzy żądaniami lub być dostępny z kilku maszyn. Serwer zdalny jest skierowany do sieci, więc wymaga ścisłej autoryzacji.
# bottom of standup_server.py
if __name__ == "__main__":
# Default transport is stdio. The host (Claude Code,
# Cursor, Claude Desktop, etc.) launches this script
# as a subprocess and talks to it over stdin/stdout.
# No port, no TLS, no auth. The trust boundary is
# whoever launched the host.
mcp.run()
# To expose the same server over the network instead,
# use Streamable HTTP. SSE was deprecated in the
# March 2025 spec update. Do not use it for new code.
#
# Production HTTP also needs an auth layer in front.
# OAuth 2.1 with Dynamic Client Registration is the
# current pattern. See Week 22 for the full flow.
#
# mcp.run(
# transport="streamable-http",
# host="0.0.0.0",
# port=8000,
# )
Wywołanie funkcji run bez argumentów oznacza użycie stdio, a komentowany blok pokazuje, jak wygląda proces aktualizacji HTTP. Ta wersja pozostaje przy stdio, co jest właściwym wyborem dla większości narzędzi deweloperskich używanych wewnętrznie. Narzędzie, które czyta jedynie pliki lokalne lub lokalny repozytorium, w ogóle nie powinno być dostępne przez interfejs sieciowy.
Pętla rozwoju lokalnego
Nie testuj nowego serwera, podłączając go bezpośrednio do klienta AI. Klienci są projektowani tak, by być odpornymi na awarie – w tle ignorują niezgodności schematów oraz brakujące funkcje, aby rozmowa mogła trwać, a gdy narzędzie zawiedzie, model po prostu przeprosi.
Zamiast tego użyj oficjalnego MCP Inspector, interfejsu deweloperskiego opartego na przeglądarce, który komunikuje się z twoim serwerem za pośrednictwem stdio.
npx @modelcontextprotocol/inspector python standup_server.py
Polecenie uruchamia Twój skrypt jako podproces i otwiera lokalną panel kontrolny. Możesz przeczytać surowy ruch komunikacyjny, zobaczyć dokładnie, jakie funkcje reklamował Twój serwer, oraz uruchamiać narzędzia i zasoby w czasie rzeczywistym.
W tym cyklu zazwyczaj pojawiają się trzy problemy:
- Niespójność schematu. Wartość zwracana przez funkcję nie zgadza się z jej typem, a inspektor natychmiast to wskazuje.
- Brakująca funkcja. Wywołujesz coś, czego serwer nigdy nie reklamował, a panel kontrolny pokazuje to, co faktycznie zostało zarejestrowane podczas negocjacji.
- Zanieczyszczenie wyjścia. Każdy nieplanowany wywołanie
print()uszkadza strumień JSON-RPC, ponieważ to stdio przenosi wiadomości. Kieruj logi do standardowego błędu za pomocą modułu logging w Pythonie, który protokół przeznaczył na czytelne diagnozy.
FastMCP oferuje również tryb rozwojowy, który restartuje serwer po zmianie pliku, co ułatwia pracę podczas iteracji w Inspektorze.
Jeden serwer, kilka klientów
Teraz projekt się opłaca – istnieje jeden plik w Pythonie, który jest weryfikowany w Inspektorze, i można go przypiąć do różnych klientów AI bez konieczności pisania kodu specyficznego dla każdego klienta. Każdy klient odczytuje plik konfiguracyjny z nazwami lokalnych serwerów do uruchomienia, a struktura JSON jest identyczna we wszystkich aplikacjach: w Claude Code, Cursor i Claude Desktop – jest to obiekt mcpServers zawierający polecenie do wykonania oraz jego argumenty.
{
"mcpServers": {
"standup-helper": {
"command": "python",
"args": ["/Users/you/code/standup_server.py"]
}
}
}
{
"mcpServers": {
"standup-helper": {
"command": "python",
"args": ["/Users/you/code/standup_server.py"]
}
}
}
{
"mcpServers": {
"standup-helper": {
"command": "python",
"args": ["/Users/you/code/standup_server.py"]
}
}
}
Klucz, polecenie oraz argumenty są identyczne we wszystkich trzech przypadkach. Tylko lokalizacja pliku konfiguracyjnego różni się w zależności od klienta.
Otwórz którykolwiek z nich i poproś o spotkanie typu standup. Klient uruchamia proces Pythona, negocjuje możliwości przez stdio, pokazuje narzędzie podsumowujące modelowi i kieruje jego wywołanie z powrotem do twojego skryptu. Git działa, słownik zwraca dane, a model zapisuje informacje z spotkania standup, zawsze korzystając z tego samego ścieżki kodu.
ChatGPT nie znajduje się na tej liście, ponieważ jego łączniki pracują z serwerami zdalnymi przez Streamable HTTP. Włączenie tego narzędzia pomocniczego wymagałoby publicznego adresu URL oraz warstwy autoryzacji, co stanowiłoby zupełnie inne profile operacyjne w porównaniu z tą ściśle lokalną implementacją.
Kiedy nie należy korzystać z MCP
Protokół ten nie jest uniwersalnym rozwiązaniem. W niektórych sytuacjach sprawdza się słabo:
- Ścieżki bardzo wrażliwe na opóźnienia. Każde wezwanie narzędzia powoduje dwukierunkową transmisję danych w formacie JSON-RPC, dlatego narzędzia znajdujące się w ścisłych pętlach wewnętrznych powinny korzystać z bezpośredniej integracji z SDK.
- Spersonalizowana interaktywna wyjściowa informacja. Jeśli potrzebujesz określonego wykresu lub spersonalizowanego formularza, musisz stworzyć integrację z hostem, ponieważ protokół oddziela dane od ich prezentacji.
- Ustawienia, które masz w pełni pod kontrolą. Jeśli jedna zespołowa grupa buduje zarówno klienta AI, jak i backend, połączenie ich bezpośrednio może być łatwiejsze do utrzymania niż wprowadzanie między nimi mechanizmu MCP.
Niewielki protokół, duży przełom
Cały protokół składa się z trzech elementów podstawowych, dwóch mechanizmów transportu oraz jednego ruchu handshakingowego JSON-RPC. To małe rozmiary są jego zaletą: oddzielają one osoby tworzące inteligencję od tych, które dostarczają kontekst. Napisanie pierwszego serwera zajmuje popołudnie i zawsze się opłaca, gdy pojawia się kolejne narzędzie AI.