Serwer MCP: Python, uv i FastMCP
Tłumaczenie automatyczne
Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.
W tym tutorialu zbudujemy mały serwer MCP dla feature store z FastMCP, uruchomimy go przez uv i połączymy z Claude Desktop. Jest dla programistów Pythona, którzy chcą udostępnić hostowi MCP jeden lokalny workflow z bazą danych. Na końcu będziesz mieć środowisko z przypiętym SDK v1, serwer z trzema narzędziami i jednym zasobem oraz lokalny test do uruchomienia przed połączeniem z Claude Desktop.
Czym jest serwer MCP?
Model Context Protocol (MCP) to otwarty protokół do łączenia aplikacji AI z systemami zewnętrznymi. Zastępuje osobną, własną integrację dla każdego narzędzia jednym zestawem konwencji.
W tym tutorialu zbudujemy serwer MCP FeatureStoreLite. Działa między LLM a feature store, czyli bazą danych z wcześniej obliczonymi cechami ML. Serwer udostępnia narzędzia do odczytu i zapisu wektorów cech, kluczowanych według użytkownika, produktu lub dokumentu.
Po co to budować?
Debugowanie pipeline’u cech zwykle oznacza przejście do SQL albo napisanie jednorazowego skryptu, żeby sprawdzić wartość. Gdy serwer działa, pytasz po prostu Claude: “Jaki jest wektor cech dla user_123?” albo “Pokaż metadane produktu product_abc.”
Dlaczego uv?
Użyjemy uv do instalacji pakietów, rozwiązywania zależności i zarządzania wirtualnym środowiskiem. Konfiguracja Claude Desktop uruchomi ten projekt z jego własnego katalogu z flagą --locked, więc użyje zależności mcp[cli] zadeklarowanej tutaj i wersji zapisanych w uv.lock.
Narzędzia, zasoby i elementy typu prompt
Serwer MCP może oferować trzy rodzaje elementów. Specyfikacja MCP rozróżnia je według tego, kto decyduje o ich użyciu:
| Prymityw | Kto go uruchamia | W tym serwerze |
|---|---|---|
| Narzędzie | Model decyduje o wywołaniu | get_feature, store_feature, list_features |
| Zasób | Host decyduje, jak dołączyć go do kontekstu | schema://main |
| Prompt | Użytkownik wybiera go, jak zapisany szablon wiadomości | Nieużywany tutaj |
Narzędzia pozwalają modelowi zażądać wykonania działania. Zasoby dostarczają kontekst za pośrednictwem hosta; sekcja 4.4 pokazuje, dlaczego udostępnienie zasobu nie gwarantuje, że odpowiedź go wykorzysta.
Przegląd architektury
Diagram pokazuje, które części działają na Twojej maszynie i jakie dane trafiają do modelu:
- Użytkownik zadaje pytanie w języku naturalnym.
- Claude Desktop jest hostem MCP. Wysyła rozmowę do modelu Claude, tworzy jednego klienta MCP dla tego serwera i zarządza połączeniem.
- Nasz serwer
FastMCPudostępnia trzy narzędzia (get_feature,store_feature,list_features) i jeden zasób (schema://main). - SQLite jest magazynem wektorów cech.
Host przekazuje listę narzędzi do Claude. Claude może zażądać wywołania narzędzia, ale komunikaty protokołu wysyła klient MCP przypisany do serwera. Serwer i SQLite działają lokalnie, jednak wyniki narzędzi trafiają do modelu jako część rozmowy. Udostępniaj tylko dane, które wkleiłbyś też do czatu.
1. Konfiguracja i instalacja
1.1. Zainstaluj uv
Jeśli nie masz jeszcze uv, zainstaluj go. Dalsza część tutoriala zakłada, że jest w Twoim PATH.
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
1.2. Zainicjalizuj projekt
Utwórz nowy katalog i zainicjalizuj projekt Pythona. uv init tworzy dla Ciebie pyproject.toml.
# Create project directory
mkdir mcp-featurestore
cd mcp-featurestore
# Initialize Python project
uv init
# Add the MCP SDK with CLI tools
uv add "mcp[cli]>=1.28,<2"
Ten tutorial używa API MCP Python SDK v1. Oficjalna dokumentacja SDK v1 zaleca użytkownikom v1 przypięcie mcp>=1.28,<2, więc zostaw górną granicę <2, dopóki nie zmigrujesz kodu. Po tym poleceniu uv.lock zapisuje całe środowisko z ustalonymi wersjami zależności.
Kod używa FastMCP dostarczanego z oficjalnym SDK (mcp.server.fastmcp). Samodzielny pakiet fastmcp to osobny projekt z innym API, więc nie mieszaj jego dokumentacji z tym kodem.
2. Budowa serwera
Dwa pliki, podzielone według odpowiedzialności:
database.pyobsługuje operacje SQLite.featurestore_server.pydefiniuje serwer MCP.
2.1. Warstwa bazy danych (database.py)
Ten moduł zarządza połączeniem SQLite i kilkoma funkcjami pomocniczymi. Wypełniamy go dwoma przykładowymi wierszami, żeby serwer miał co zwrócić przy pierwszym zapytaniu.
Utwórz database.py:
# database.py
import json
import os
import sqlite3
def get_db_path() -> str:
"""Get the database path - always in the script's directory"""
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(script_dir, "features.db")
def init_db() -> None:
"""Initialize the feature store database with table and sample data"""
conn = sqlite3.connect(get_db_path())
conn.execute("""
CREATE TABLE IF NOT EXISTS features (
key TEXT PRIMARY KEY,
vector TEXT NOT NULL,
metadata TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Sample data for experimentation
example_features = [
(
"user_123",
"[0.1, 0.2, -0.5, 0.8, 0.3, -0.1, 0.9, -0.4]",
json.dumps({"type": "user", "id": 123, "segment": "premium"}),
),
(
"product_abc",
"[0.7, -0.3, 0.4, 0.1, -0.8, 0.6, 0.2, -0.5]",
json.dumps({"type": "product", "id": "abc", "category": "electronics"}),
),
]
# Insert if not exists
for key, vector, metadata in example_features:
try:
conn.execute(
"INSERT INTO features (key, vector, metadata) VALUES (?, ?, ?)",
(key, vector, metadata),
)
except sqlite3.IntegrityError:
pass # Already exists
conn.commit()
conn.close()
def get_db_connection() -> sqlite3.Connection:
"""Get a database connection"""
return sqlite3.connect(get_db_path())
if __name__ == "__main__":
init_db()
print("✅ Database initialized successfully!")
Zainicjalizuj bazę danych:
uv run python database.py
2.2. Serwer MCP (featurestore_server.py)
FastMCP robi większość pracy. Udekoruj zwykłą funkcję Pythona, a zostanie zarejestrowana jako narzędzie lub zasób MCP. FastMCP buduje schemat wejściowy każdego narzędzia z adnotacji typów i używa docstringa jako opisu narzędzia. Model widzi jedno i drugie, więc pisz docstring dla modelu: powiedz, co narzędzie robi i jak wygląda każdy argument.
Utwórz featurestore_server.py:
# featurestore_server.py
import json
from typing import Annotated, Any
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from database import get_db_connection, init_db
# Initialize the MCP Server
mcp = FastMCP("FeatureStoreLite")
# Ensure DB is ready when server starts
init_db()
# A float that rejects NaN and infinities
FiniteFloat = Annotated[float, Field(allow_inf_nan=False)]
@mcp.resource("schema://main")
def get_schema() -> str:
"""The CREATE TABLE statements of the feature store database."""
conn = get_db_connection()
try:
schema = conn.execute(
"SELECT sql FROM sqlite_master WHERE type='table'"
).fetchall()
return "\n".join(sql[0] for sql in schema if sql[0]) or "No tables found."
finally:
conn.close()
@mcp.tool()
def store_feature(
key: str,
vector: Annotated[list[FiniteFloat], Field(min_length=1)],
metadata: dict[str, Any] | None = None,
) -> str:
"""Store a feature vector under key. Overwrites an existing key.
vector: non-empty list of finite numbers, e.g. [0.1, 0.2].
metadata: optional JSON object, e.g. {"type": "user"}.
"""
metadata_json = None if metadata is None else json.dumps(metadata, allow_nan=False)
conn = get_db_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
(key, json.dumps(vector), metadata_json),
)
conn.commit()
finally:
conn.close()
return f"Stored feature '{key}'"
@mcp.tool()
def get_feature(key: str) -> str:
"""Return the vector and metadata stored under key, as JSON.
Call list_features first if you do not know the key.
"""
conn = get_db_connection()
try:
row = conn.execute(
"SELECT vector, metadata FROM features WHERE key = ?", (key,)
).fetchone()
finally:
conn.close()
if row is None:
raise ValueError(f"Feature '{key}' not found. Call list_features for valid keys.")
return json.dumps(
{
"key": key,
"vector": json.loads(row[0]),
"metadata": json.loads(row[1]) if row[1] else None,
},
indent=2,
)
@mcp.tool()
def list_features() -> str:
"""Return a JSON array of every stored feature key."""
conn = get_db_connection()
try:
rows = conn.execute("SELECT key FROM features").fetchall()
return json.dumps([row[0] for row in rows])
finally:
conn.close()
if __name__ == "__main__":
mcp.run()
Cztery szczegóły tego kodu decydują o tym, co model wysyła i co dostaje z powrotem:
- Typowany
vector. Schemat narzędzia wymaga niepustej tablicy liczb. W SDK 1.30 Pydantic przekształca też wartości logiczne i ciągi liczbowe w liczby zmiennoprzecinkowe:[true, false]staje się[1.0, 0.0], a["1.5", "2"]staje się[1.5, 2.0]. Następnie przed uruchomieniem funkcji sprawdza, czy lista jest niepusta i każda wartość jest skończona. Jeśli model wyśle wektor jako ciąg JSON, na przykład"[1, NaN]", FastMCP najpierw parsuje ten ciąg.allow_inf_nan=Falseodrzuca potemNaNi nieskończoności, które parser JSON w Pythonie domyślnie akceptuje. - Typowane
metadata. Schemat akceptuje obiekt JSON,nulllub pominiętą wartość.allow_nan=Falsenie pozwala też zapisaćNaNw metadanych. - Zgłaszane błędy.
get_featurezgłaszaValueErrordla brakującego klucza. Zgłoszony wyjątek trafia do klienta jako błąd narzędzia zisError: true; zwrócony ciąg wygląda jak udany wynik. - Nadpisywanie.
store_featureużywaINSERT OR REPLACE, więc zastępuje istniejący klucz. Docstring o tym mówi, dzięki czemu model wie to przed wywołaniem narzędzia.
3. Testowanie w MCP Inspector
Zanim podłączysz serwer do Claude, sprawdź go w MCP Inspector. To mały interfejs webowy, który łączy się z serwerem tak jak host. Możesz w nim przeglądać zasoby serwera, wywoływać każde narzędzie z własnymi argumentami i widzieć każde żądanie oraz odpowiedź.
SDK uruchamia Inspector przez npx, więc ten krok wymaga również Node.js i npm z npx w Twoim PATH. Inspector 2.10.1 wymaga Node.js 22.19.0 lub nowszego. Sprawdź zainstalowane polecenia, a następnie uruchom Inspector:
node --version
npx --version
uv run mcp dev featurestore_server.py
Polecenie uruchamia serwer pod MCP Inspector przez stdio. Użyj adresu URL przeglądarki, który wypisze polecenie. Port interfejsu Inspector zależy od implementacji, więc nie zakładaj stałego portu.
Wywołaj get_feature z key="user_123". Jeśli zwróci JSON z wiersza startowego, serwer działa. Potem wywołaj go z kluczem, który nie istnieje: wynikiem jest błąd narzędzia z isError: true. To sprawdza lokalną ścieżkę protokołu, rejestrację narzędzi, wyszukiwanie w bazie i ścieżkę błędu. Nie sprawdza, jak Claude wybiera narzędzia; robi to sekcja 4.
Inspector odczytuje schemat SQL z zasobu schema://main:
4. Połączenie z Claude Desktop
Gdy Inspector potwierdzi, że serwer działa, zarejestruj go w Claude Desktop.
4.1. Skonfiguruj Claude
Edytuj plik konfiguracji Claude Desktop:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%/Claude/claude_desktop_config.json
Dodaj serwer do obiektu mcpServers:
{
"mcpServers": {
"featurestore": {
"command": "uv",
"args": [
"run",
"--directory",
"/ABSOLUTE/PATH/TO/mcp-featurestore",
"--locked",
"mcp",
"run",
"/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
]
}
}
}
Ważne: użyj ścieżek bezwzględnych zarówno dla katalogu projektu, jak i featurestore_server.py. Claude Desktop uruchamia serwer jako osobny proces. uv run wykrywa projekt na podstawie katalogu roboczego dla polecenia takiego jak mcp, więc --directory wybiera pyproject.toml i uv.lock tego projektu; --locked kończy się wtedy błędem zamiast zmieniać ten plik blokady. Polecenie uv add z kroku 1.2 już zadeklarowało mcp[cli] w projekcie.
SDK ma też skrót: uv run mcp install featurestore_server.py zapisuje za Ciebie wpis Claude Desktop. W mcp 1.30 ten wpis uruchamia uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. Nie ma --directory, więc nie używa uv.lock tego projektu, a --with mcp[cli] nie ma ograniczenia <2. Użyj go do szybkiej próby; powyższa ręczna konfiguracja jest powtarzalna.
4.2. Jak działa interakcja
Oto, co działa od początku do końca, gdy Claude potrzebuje odczytu cechy. Kroki 1 i 2 wykonują się raz, gdy Claude Desktop uruchamia serwer. Kroki od 3 do 6 wykonują się przy każdym pytaniu wymagającym danych cech.
- Claude Desktop uruchamia dedykowanego klienta MCP dla serwera i wykrywa jego narzędzia.
- Host udostępnia opisy narzędzi modelowi Claude.
- Claude może zażądać wywołania narzędzia, gdy pytanie wymaga danych z feature store.
- Klient MCP wysyła to żądanie do serwera.
- Serwer uruchamia funkcję Pythona i zwraca wynik do hosta.
- Host może przekazać wynik modelowi Claude do przygotowania końcowej odpowiedzi.
Ten podział na hosta, klienta i serwer wynika ze specyfikacji architektury MCP.
4.3. Przykładowe zapytania
Uruchom ponownie Claude Desktop i wypróbuj kilka wiadomości typu prompt:
-
“Wyświetl wszystkie dostępne cechy.” Deterministyczny wynik zawiera dwa klucze startowe:
user_123iproduct_abc. -
“Pobierz wektor cech dla user_123.” Odpowiedź zawiera wektor i metadane
premiumzdatabase.py. -
“Zapisz nową cechę dla
new_itemz wektorem[0.5, 0.5]i metadanymi{"type": "test"}. Następnie pobierznew_itemi pokaż zapisany wektor oraz metadane.” Po konwersji zwalidowany wektor musi być niepusty i zawierać wyłącznie skończone liczby zmiennoprzecinkowe. Metadane mogą być obiektem JSON,nulllub zostać pominięte. Kolejny odczyt sprawdza pełny cykl zapisu i odczytu. -
“Pobierz wektor cech dla user_999.”
get_featurezgłasza błąd dla brakującego klucza. Claude powinien powiedzieć, że klucz nie istnieje, i może wywołaćlist_features, aby pokazać poprawne klucze.
4.4. Jak czytać odpowiedzi Claude
Serwer zwraca dane narzędzi i zasobów. Claude wybiera narzędzie i pisze wyjaśnienie wokół wyniku.
Ten zrzut ekranu przedstawia uruchomienie z czerwca 2025 z wcześniejszą wersją przykładu. Poprosiłem Claude o pokazanie schematu bazy danych:
Claude nazwał system magazynem NoSQL lub dokumentów i podał schemat pojęciowy, ale database.py tworzy relacyjną tabelę SQLite. Pokazana odpowiedź nie jest instrukcją CREATE TABLE zwracaną przez schema://main. Zrzut ekranu nie pokazuje, czy Claude Desktop odczytał ten zasób ani jaki kontekst otrzymał Claude, więc nie dowodzi przyczyny błędu.
Zasobami steruje aplikacja: host decyduje, jak dołączyć je do kontekstu modelu, przez wybór użytkownika lub automatycznie. MCP nie wymaga, aby host przekazywał modelowi każdy zasób. Jeśli model ma żądać schematu przez wywołanie narzędzia, udostępnij go także jako narzędzie.
Wiarygodnie brzmiące wyjaśnienie modelu nie zawsze zgadza się z tym, co zwróciło narzędzie. Gdy szczegół ma znaczenie, sprawdź wynik narzędzia lub kod źródłowy.
4.5. Inne hosty MCP
Serwer nie zależy od Claude Desktop. Każdy host, który uruchamia serwery stdio na tej samej maszynie, może użyć tego samego polecenia uv. Claude Desktop używa JSON z sekcji 4.1; pozostałe dwa to:
- Claude Code:
claude mcp add featurestore -- uv run --directory /ABSOLUTE/PATH/TO/mcp-featurestore --locked mcp run /ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py. Wszystko po--to polecenie uruchamiające serwer. - Cursor czyta ten sam format JSON
mcpServersz~/.cursor/mcp.jsonlub.cursor/mcp.jsonw projekcie.
Host na innej maszynie nie może uruchomić procesu stdio na Twojej. Dla serwera współdzielonego przez kilka osób uruchom go przez Streamable HTTP z mcp.run(transport="streamable-http") i dodaj przed nim uwierzytelnianie.
5. Rozwiązywanie problemów
Ustal, która część zawiodła, zanim zmienisz kod: błędy startu są w logach, błędy narzędzi powtarzają się w Inspector bez modelu, a błędna odpowiedź wymaga porównania wyniku narzędzia z tekstem Claude.
-
“Server connection failed”:
- Sprawdź logi w
~/Library/Logs/Claude/na macOS lub%APPDATA%\Claude\logsna Windows.mcp.logzapisuje zdarzenia połączenia.mcp-server-featurestore.logw tym samym folderze zawiera własny stderr serwera, więc tam widać traceback z uruchamiania (logi Claude Desktop). - Upewnij się, że konfiguracja używa ścieżki bezwzględnej, a nie względnej.
- Upewnij się, że
uvjest wPATHClaude Desktop. Jeśli nie jest, wskaż pełną ścieżkę do pliku binarnego (which uvpokaże, gdzie się znajduje). - Na stdio stdout przenosi komunikaty protokołu. Loguj do stderr;
print()w serwerze psuje ten strumień i może przerwać połączenie.
- Sprawdź logi w
-
“Tool execution error”:
- Odtwórz go w Inspector poleceniem
uv run mcp dev featurestore_server.py. Inspector pokazuje komunikat błędu i argumenty, które go wywołały. - Sprawdź, czy
features.dbpowstaje obokdatabase.py. Ścieżka pochodzi zget_db_path(), która wyznacza ją względem skryptu, więc zmiana katalogu roboczego nie powinna przesunąć pliku.
- Odtwórz go w Inspector poleceniem
-
Wywołanie się udaje, ale odpowiedź jest błędna:
- Otwórz wynik narzędzia na czacie i porównaj go z tekstem Claude. Sekcja 4.4 pokazuje odpowiedź modelu, która błędnie opisała bazę danych.
6. Zanim agent użyje tego naprawdę
store_featurenadpisuje każdy klucz, także wiersze startowe. Zostaw w hoście włączone zatwierdzanie każdego wywołania narzędzi zapisu albo przenieś je na osobny serwer, który włączasz tylko wtedy, gdy go potrzebujesz.- Wyniki narzędzi trafiają do dostawcy modelu jako część rozmowy. Nie zwracaj danych, które muszą zostać na maszynie.
- Serwer współdzielony przez kilka osób potrzebuje Streamable HTTP i uwierzytelniania zamiast stdio (sekcja 4.5).
7. Podsumowanie
To wszystko: serwer FastMCP, magazyn SQLite i konfiguracja Claude Desktop wskazująca polecenie uv run. Ten sam układ sprawdza się przy większości rzeczy, które można opakować w funkcję Pythona. Zamień wywołania SQLite na prawdziwy feature store, wewnętrzne API lub rejestr modeli, a serwer pozostanie mały.

