Tutorial tworzenia serwera MCP: Python, uv i FastMCP
Tłumaczenie automatyczne Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.
Programiści Pythona mogą przekształcić lokalny workflow feature store w przypięty do konkretnych wersji serwer FastMCP dla Claude Desktop, używając uv do utworzenia środowiska, udostępnienia narzędzi i zasobu oraz lokalnego zweryfikowania serwera przed nawiązaniem połączenia z aplikacją desktopową.
Czym jest serwer MCP?
Model Context Protocol (MCP) to otwarty protokół do łączenia aplikacji AI z systemami zewnętrznymi. Zastępuje osobną niestandardową integrację dla każdego narzędzia jednym zestawem konwencji.
W tym tutorialu zbudujemy serwer MCP FeatureStoreLite. Znajduje się on między LLM a feature store, czyli bazą danych zawierającą wstępnie obliczone cechy ML. Serwer udostępnia narzędzia do odczytywania i zapisywania wektorów cech indeksowanych przez użytkownika, produkt lub dokument.
Po co go budować?
Debugowanie pipeline’u cech zwykle oznacza przejście do SQL albo napisanie tymczasowego skryptu w celu sprawdzenia wartości. Gdy serwer działa, możesz zamiast tego zapytać Claude: „Jaki jest wektor cech dla user_123?” albo „Pokaż mi metadane produktu product_abc”.
Dlaczego używać uv?
Użyjemy uv do instalowania pakietów, rozwiązywania zależności i zarządzania środowiskiem wirtualnym. Konfiguracja Claude Desktop uruchomi ten projekt z jego własnego katalogu za pomocą --locked, dzięki czemu użyje zależności mcp[cli] zadeklarowanej tutaj oraz wersji zapisanych w uv.lock.
Przegląd architektury
Cztery elementy i sposób ich współdziałania:
- Użytkownik zadaje pytanie w języku naturalnym.
- Claude Desktop jest hostem MCP. Tworzy jednego klienta MCP dla tego serwera i zarządza połączeniem.
- Nasz serwer
FastMCPudostępniaget_featureistore_featurejako narzędzia MCP. - SQLite jest magazynem przechowującym wektory cech.
Host może udostępnić Claude wykryte narzędzia. Claude może zażądać wywołania narzędzia, ale klient MCP dla danego serwera wysyła komunikaty protokołu.
1. Konfiguracja i instalacja
1.1. Zainstaluj uv
Jeśli nie masz jeszcze uv, zainstaluj je. W dalszej części tutorialu zakładamy, że znajduje się ono na 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 w Pythonie. uv init utworzy 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 korzysta z API MCP Python SDK v1. Oficjalna dokumentacja SDK v1 zaleca użytkownikom v1 przypięcie mcp>=1.28,<2, dlatego zachowaj górne ograniczenie <2 do czasu migracji kodu. uv.lock zapisuje kompletne rozwiązane środowisko po wykonaniu tego polecenia.
Kod inline w tym artykule jest kanoniczny. Powiązane repozytorium wskazane w odnośnikach zawiera wersję historyczną. Nie odtwarza bieżącego przypięcia zależności ani aktualnej walidacji wektora.
2. Budowa serwera
Dwa pliki, rozdzielone 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 z SQLite i kilkoma funkcjami pomocniczymi. Inicjalizujemy go dwoma przykładowymi wierszami, aby 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 wykonuje większość pracy. Ozdobienie zwykłej funkcji Pythona powoduje jej zarejestrowanie jako narzędzia lub zasobu MCP. Docstring staje się opisem widocznym dla LLM. Napisz go z myślą o tym odbiorcy.
Utwórz featurestore_server.py:
# featurestore_server.py
import json
import math
from mcp.server.fastmcp import FastMCP
from database import get_db_connection, init_db
# Initialize the MCP Server
mcp = FastMCP("FeatureStoreLite")
# Ensure DB is ready when server starts
init_db()
def reject_non_finite_json_number(value: str) -> None:
"""Reject NaN and infinities, which are not valid JSON numbers."""
raise ValueError(f"Non-finite JSON number: {value}")
@mcp.resource("schema://main")
def get_schema() -> str:
"""
Resource: Provide the database schema.
Resources provide context to the host application.
The host decides whether to pass that context to the model.
"""
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: str, metadata: str | None = None) -> str:
"""
Tool: Store a feature vector.
Tools are executable functions that LLMs can call to perform actions.
"""
try:
parsed_vector = json.loads(
vector, parse_constant=reject_non_finite_json_number
)
except (json.JSONDecodeError, ValueError):
return "Error: Vector must be valid JSON with finite numbers"
try:
valid_vector = (
isinstance(parsed_vector, list)
and bool(parsed_vector)
and all(
isinstance(value, (int, float))
and not isinstance(value, bool)
and math.isfinite(value)
for value in parsed_vector
)
)
except OverflowError:
valid_vector = False
if not valid_vector:
return "Error: Vector must be a non-empty JSON array of finite numbers (e.g., '[0.1, 0.2]')"
metadata_json = None
if metadata is not None:
try:
parsed_metadata = json.loads(
metadata, parse_constant=reject_non_finite_json_number
)
except (json.JSONDecodeError, ValueError):
return "Error: Metadata must be valid JSON with finite numbers"
if not isinstance(parsed_metadata, dict):
return "Error: Metadata must be a JSON object (e.g., '{\"type\": \"test\"}')"
metadata_json = json.dumps(parsed_metadata, allow_nan=False)
conn = get_db_connection()
try:
conn.execute(
"INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
(key, json.dumps(parsed_vector, allow_nan=False), metadata_json),
)
conn.commit()
return f"Successfully stored feature '{key}'"
except Exception as e:
return f"Error: {str(e)}"
finally:
conn.close()
@mcp.tool()
def get_feature(key: str) -> str:
"""
Tool: Retrieve a feature vector by key.
"""
conn = get_db_connection()
try:
row = conn.execute(
"SELECT vector, metadata FROM features WHERE key = ?", (key,)
).fetchone()
if row:
return json.dumps(
{
"key": key,
"vector": json.loads(row[0]),
"metadata": json.loads(row[1]) if row[1] else None,
},
indent=2,
)
return f"Feature '{key}' not found."
finally:
conn.close()
@mcp.tool()
def list_features() -> str:
"""
Tool: List all available feature keys.
"""
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()
Parser JSON w Pythonie domyślnie akceptuje NaN oraz nieskończoności, mimo że znajdują się one poza gramatyką liczb JSON. Callback parse_constant odrzuca takie zapisy, a math.isfinite sprawdza każdy element wektora, zanim wiersz trafi do SQLite. Metadane podlegają jednemu kontraktowi: muszą być tekstem zawierającym obiekt JSON, a serwer parsuje je i normalizuje przed wstawieniem. Zobacz uwagi Pythona dotyczące interoperacyjności JSON.
3. Testowanie za pomocą MCP Inspector
Przed podłączeniem serwera do Claude sprawdź go wstępnie za pomocą MCP Inspector. To niewielki interfejs webowy umożliwiający bezpośrednie wywoływanie narzędzi i odczytywanie zasobów.
uv run mcp dev featurestore_server.py
Polecenie uruchamia serwer pod kontrolą MCP Inspector przez stdio. Użyj adresu URL przeglądarki wyświetlonego przez polecenie. Port interfejsu Inspector zależy od implementacji, więc nie zakładaj stałego portu.
Tak wyglądało moje pierwotne uruchomienie z czerwca 2025 roku w MCP Inspector v0.14.0. Zrzut ekranu przedstawia konfigurację stdio Inspectora i pomyślne połączenie. Jego nieprzypięty argument --with mcp ma charakter historyczny; przy odtwarzaniu tutorialu użyj teraz przypiętego polecenia v1 z tego artykułu.

Wywołaj get_feature z key="user_123". Jeśli zwróci JSON dla wiersza inicjalizacyjnego, serwer działa.
Sprawdza to lokalną ścieżkę protokołu, rejestrację narzędzia, wyszukiwanie w bazie danych i dane wyjściowe JSON. Nie sprawdza jakości wyszukiwania wektorowego. W środowisku produkcyjnym dodaj testy z reprezentatywnymi zapytaniami, oczekiwanymi wynikami wyszukiwania najbliższych sąsiadów, progami odległości, filtrami metadanych oraz zbiorem ewaluacyjnym odpowiadającym Twojemu obciążeniu.
To samo uruchomienie ujawniło wszystkie trzy narzędzia i zasób schema://main. Te dwa zrzuty ekranu są przydatne do sprawdzenia procesu wykrywania: Inspector znalazł funkcje i odczytał schemat SQL zwrócony przez serwer.


Zrzut narzędzia zawiera cztery wiersze: user_123, product_abc, doc_guide_001 oraz recommendation_engine. Była to moja bogatsza lokalna baza danych z 10 czerwca 2025 roku. Jej implementacja list_features zwracała obiekty z polami key i created_at. Bieżąca funkcja zwraca tylko tablicę JSON z kluczami i inicjalizuje wyłącznie dwa pierwsze wiersze. Nie porównuj historycznego wyniku wiersz po wierszu z bieżącym fixture.
4. Połączenie z Claude Desktop
Gdy Inspector potwierdzi, że serwer działa, zarejestruj go w Claude Desktop.
4.1. Konfiguracja Claude
Edytuj plik konfiguracyjny 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 absolutnych zarówno dla katalogu projektu, jak i
featurestore_server.py. Claude Desktop uruchamia serwer jako osobny proces.uv runwyszukuje projekt na podstawie bieżącego katalogu roboczego dla polecenia takiego jakmcp, więc--directorywybierapyproject.tomliuv.locktego projektu;--lockedkończy się wtedy błędem zamiast zmienić ten plik lock. Polecenieuv addz kroku 1.2 zadeklarowało jużmcp[cli]w projekcie.
4.2. Jak działa interakcja
Oto przebieg całej ścieżki, gdy Claude potrzebuje wyszukać cechę:
- Claude Desktop uruchamia dedykowanego klienta MCP dla serwera i wykrywa jego narzędzia.
- Host udostępnia Claude opisy narzędzi.
- 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 Claude, aby ten wygenerował końcową odpowiedź.
Ten podział na hosta, klienta i serwer jest zgodny z specyfikacją architektury MCP. Zasoby dostarczają również kontekstu aplikacji hosta. MCP nie wymaga, aby host przekazywał modelowi każdy zasób.
4.3. Przykładowe zapytania
Uruchom ponownie Claude Desktop i wypróbuj kilka promptów:
-
„Wyświetl wszystkie dostępne cechy”. Deterministyczny wynik zawiera dwa zainicjalizowane klucze:
user_123orazproduct_abc. -
„Pobierz wektor cech dla user_123”. Odpowiedź zawiera wektor oraz metadane
premiumzdatabase.py. -
„Zapisz nową cechę dla
new_itemz ciągiem JSON wektora[0.5, 0.5]i ciągiem JSON metadanych{"type": "test"}. Następnie pobierznew_itemi pokaż zapisany wektor oraz metadane”. Oba argumenty muszą zawierać poprawny JSON. Zapis powiedzie się dopiero po zweryfikowaniu przez serwer wektora i metadanych. Następujący po nim odczyt sprawdza pełny cykl zapisu i odczytu.
4.4. Co pokazało moje uruchomienie Claude Desktop
Poniższe zrzuty ekranu pochodzą z tego samego uruchomienia z czerwca 2025 roku, zanim ograniczyłem fixture artykułu do dwóch wierszy. Pokazują, co Claude Desktop wyświetlił po połączeniu z moim serwerem. Są obserwacjami z tamtego uruchomienia, a nie gwarantowanymi danymi wyjściowymi MCP. Serwer zwraca dane narzędzi i zasobów; Claude wybiera narzędzie i tworzy objaśnienie wokół wyniku.
Najpierw poprosiłem Claude o wyświetlenie schematu bazy danych:

Claude pomylił istotny szczegół. Określił system jako magazyn NoSQL lub dokumentowy, mimo że database.py używa SQLite i tworzy tabelę relacyjną. Kolumna metadata zawiera tekst JSON, ale nie zmienia to silnika bazy danych. Zrzut ekranu dobrze przypomina, że wiarygodne wyjaśnienie modelu nie jest kontraktem narzędzia. Gdy to rozróżnienie ma znaczenie, sprawdź zwróconą instrukcję CREATE TABLE albo kod źródłowy.
Poprosiłem również Claude o wyświetlenie dostępnych cech:

Cztery klucze odpowiadają bogatszej lokalnej bazie danych pokazanej w Inspector. Etykiety takie jak „user embedding” i „model embedding” są interpretacją nazw i metadanych dokonaną przez Claude. Samo list_features gwarantuje wyłącznie wiersze zwracane przez jego implementację.
Na koniec pobrałem product_abc:

W tym przypadku wektor i metadane pochodziły z wyniku narzędzia. Tekst o podobieństwie, rekomendacjach i klastrowaniu został wygenerowany przez Claude. Są to możliwe zastosowania embeddingu, ale serwer z tego tutorialu jedynie przechowuje i pobiera wektory. Nie implementuje wyszukiwania najbliższych sąsiadów.
5. Rozwiązywanie problemów
Warto znać kilka typowych trybów awarii:
-
„Połączenie z serwerem nie powiodło się”:
- Sprawdź logi w
~/Library/Logs/Claude/mcp.logna macOS. - Upewnij się, że konfiguracja używa ścieżki absolutnej, a nie względnej.
- Upewnij się, że
uvznajduje się wPATHClaude Desktop. Jeśli nie, wskaż pełną ścieżkę do pliku binarnego (which uvpokaże, gdzie się znajduje).
- Sprawdź logi w
-
„Błąd wykonania narzędzia”:
- Odtwórz problem w Inspector za pomocą
uv run mcp dev featurestore_server.py. Inspector pokazuje surowy błąd, który Claude Desktop zwykle ukrywa. - Sprawdź, czy
features.dbjest tworzony obokdatabase.py. Ścieżka pochodzi zget_db_path(), które rozwiązuje ją względem skryptu, więc zmieniający się katalog roboczy nie powinien przenosić pliku.
- Odtwórz problem w Inspector za pomocą
6. Podsumowanie
To wszystko: serwer FastMCP, magazyn SQLite oraz konfiguracja Claude Desktop wskazująca na polecenie uv run. Ten sam schemat działa dla większości rzeczy, które można opakować w funkcję Pythona. Zastąp wywołania SQLite prawdziwym feature store, wewnętrznym API albo rejestrem modeli, a serwer pozostanie niewielki.
Najważniejsze wnioski
- MCP wyznacza granicę narzędzia, a nie uzasadnienie dla udostępniania każdej wewnętrznej funkcji.
- Utrzymuj narzędzia serwera małe, typowane i łatwe do testowania bez LLM.
- Używaj uv, aby środowisko tutorialu można było odtworzyć od zera.
- Traktuj serwer MCP jak kod produkcyjny, gdy tylko agent może go wywoływać.
Odnośniki
- Historyczne repozytorium towarzyszące (nie odtwarza bieżącego kodu inline)
- Wprowadzenie do MCP
- MCP Python SDK
- Claude Desktop
- uv