MCP-Server-Tutorial: Mit Python, uv und FastMCP entwickeln
Automatische Übersetzung Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.
Python-Entwickler können einen lokalen Feature-Store-Workflow in einen versionsgepinnten FastMCP-Server für Claude Desktop umwandeln. Dabei verwenden sie uv, um die Umgebung zu erstellen, Tools und eine Resource bereitzustellen und den Server lokal zu überprüfen, bevor die Desktop-Verbindung hergestellt wird.
Was ist ein MCP-Server?
Das Model Context Protocol (MCP) ist ein offenes Protokoll zur Verbindung von AI-Anwendungen mit externen Systemen. Es ersetzt separate individuelle Integrationen für jedes Tool durch einen gemeinsamen Satz von Konventionen.
In diesem Tutorial entwickeln wir einen FeatureStoreLite-MCP-Server. Er sitzt zwischen einem LLM und einem Feature Store, also einer Datenbank mit vorab berechneten ML-Features. Der Server stellt Tools zum Abfragen und Schreiben von Feature-Vektoren bereit, die über Benutzer, Produkte oder Dokumente adressiert werden.
Warum sollte man das entwickeln?
Das Debugging einer Feature-Pipeline bedeutet normalerweise, SQL auszuführen oder ein Wegwerf-Skript zu schreiben, um einen Wert zu überprüfen. Wenn der Server läuft, fragst du stattdessen Claude: „Wie lautet der Feature-Vektor für user_123?“ oder „Zeige mir die Metadaten für product_abc.“
Warum uv verwenden?
Wir verwenden uv zum Installieren von Packages, Auflösen von Dependencies und Verwalten der virtuellen Umgebung. Die Claude-Desktop-Konfiguration führt dieses Projekt aus seinem eigenen Verzeichnis mit --locked aus. Dadurch verwendet sie die hier deklarierte Dependency mcp[cli] sowie die in uv.lock aufgezeichneten Versionen.
Architekturüberblick
Die vier Komponenten und ihr Zusammenspiel:
- Der Benutzer stellt eine Frage in natürlicher Sprache.
- Claude Desktop ist der MCP-Host. Er erstellt für diesen Server einen MCP-Client und verwaltet die Verbindung.
- Unser
FastMCP-Server stelltget_featureundstore_featureals MCP-Tools bereit. - SQLite ist der zugrunde liegende Store für die Feature-Vektoren.
Der Host kann die erkannten Tools für Claude verfügbar machen. Claude kann einen Tool Call anfordern, aber der serverspezifische MCP-Client sendet die Protokollnachrichten.
1. Setup und Installation
1.1. uv installieren
Falls du uv noch nicht hast, installiere es. Der Rest des Tutorials setzt voraus, dass es in deinem PATH verfügbar ist.
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
1.2. Projekt initialisieren
Erstelle ein neues Verzeichnis und initialisiere ein Python-Projekt. uv init erstellt dabei automatisch eine pyproject.toml für dich.
# 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"
Dieses Tutorial verwendet die MCP Python SDK v1 API. Die offizielle Dokumentation für das v1 SDK weist v1-Benutzer an, mcp>=1.28,<2 zu pinnen. Behalte daher die obere Grenze <2 bei, bis du den Code migrierst. uv.lock zeichnet nach diesem Befehl die vollständig aufgelöste Umgebung auf.
Der Inline-Code in diesem Artikel ist maßgeblich. Das in den Referenzen verlinkte Begleit-Repository ist eine historische Version. Es enthält weder den aktuellen Dependency-Pin noch die aktuelle Vektorvalidierung.
2. Den Server entwickeln
Zwei Dateien, nach Verantwortlichkeit getrennt:
database.pyübernimmt die SQLite-Operationen.featurestore_server.pydefiniert den MCP-Server.
2.1. Die Datenbankschicht (database.py)
Dieses Modul verwaltet die SQLite-Verbindung und einige Hilfsfunktionen. Wir fügen zwei Beispielzeilen ein, damit der Server bei der ersten Abfrage etwas zurückgeben kann.
Erstelle 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!")
Initialisiere die Datenbank:
uv run python database.py
2.2. Der MCP-Server (featurestore_server.py)
FastMCP übernimmt den größten Teil der Arbeit. Dekoriere eine gewöhnliche Python-Funktion, und sie wird als MCP-Tool oder Resource registriert. Der Docstring wird zur Beschreibung, die das LLM sieht. Schreibe ihn für diesen Leser.
Erstelle 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()
Pythons JSON-Parser akzeptiert standardmäßig NaN und Unendlichkeiten, obwohl sie außerhalb der JSON-Zahlengrammatik liegen. Der Callback parse_constant weist diese Schreibweisen zurück, und math.isfinite überprüft jedes Vektorelement, bevor SQLite die Zeile erhält. Für Metadaten gilt ein einheitlicher Vertrag: Sie müssen ein JSON-Objekt als String enthalten, und der Server parst und normalisiert sie vor dem Einfügen. Siehe die Hinweise zur JSON-Interoperabilität in Python.
3. Testen mit dem MCP Inspector
Bevor du den Server mit Claude verbindest, überprüfe ihn mit dem MCP Inspector. Dabei handelt es sich um eine kleine Weboberfläche, mit der du Tools aufrufen und Resources direkt lesen kannst.
uv run mcp dev featurestore_server.py
Der Befehl startet den Server über stdio unter dem MCP Inspector. Verwende die vom Befehl ausgegebene Browser-URL. Der Port der Inspector-Oberfläche hängt von der Implementierung ab; gehe daher nicht von einem festen Port aus.
So sah mein ursprünglicher Lauf im Juni 2025 mit MCP Inspector v0.14.0 aus. Der Screenshot dokumentiert die stdio-Konfiguration des Inspectors und eine erfolgreiche Verbindung. Das ungepinnte Argument --with mcp ist historisch; verwende bei einer heutigen Wiederholung des Tutorials den gepinnten v1-Befehl aus diesem Artikel.

Rufe get_feature mit key="user_123" auf. Wenn das JSON für die Seed-Zeile zurückgegeben wird, funktioniert der Server.
Damit werden der lokale Protokollpfad, die Tool-Registrierung, die Datenbankabfrage und die JSON-Ausgabe überprüft. Die Qualität der Vector Search wird dadurch nicht validiert. Für Production Retrieval solltest du Tests mit repräsentativen Queries, erwarteten Nearest-Neighbor-Ergebnissen, Distanzschwellen, Metadatenfiltern und einem Eval-Dataset ergänzen, das zu deinem Workload passt.
Derselbe Lauf legte alle drei Tools und die Resource schema://main offen. Diese beiden Screenshots sind nützliche Prüfungen der Discovery: Der Inspector fand die Funktionen und las das vom Server zurückgegebene SQL-Schema.


Der Tool-Screenshot enthält vier Zeilen: user_123, product_abc, doc_guide_001 und recommendation_engine. Das war meine umfangreichere lokale Datenbank am 10. Juni 2025. Ihre Implementierung von list_features gab Objekte mit den Feldern key und created_at zurück. Die aktuelle Funktion gibt nur ein JSON-Array mit Key-Strings zurück und fügt nur die ersten beiden Zeilen als Seed ein. Vergleiche die historische Ausgabe daher nicht Zeile für Zeile mit dem aktuellen Fixture.
4. Mit Claude Desktop verbinden
Sobald der Inspector bestätigt, dass der Server funktioniert, registriere ihn bei Claude Desktop.
4.1. Claude konfigurieren
Bearbeite deine Claude-Desktop-Konfigurationsdatei:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%/Claude/claude_desktop_config.json
Füge deinen Server zum Objekt mcpServers hinzu:
{
"mcpServers": {
"featurestore": {
"command": "uv",
"args": [
"run",
"--directory",
"/ABSOLUTE/PATH/TO/mcp-featurestore",
"--locked",
"mcp",
"run",
"/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
]
}
}
}
Wichtig: Verwende sowohl für das Projektverzeichnis als auch für
featurestore_server.pyabsolute Pfade. Claude Desktop startet den Server als separaten Prozess.uv runerkennt ein Projekt anhand seines Arbeitsverzeichnisses für einen Befehl wiemcp. Dadurch wählt--directorydiepyproject.tomlunduv.lockdieses Projekts aus; anschließend schlägt--lockedfehl, statt diese Lockfile zu ändern. Der Befehluv addaus Schritt 1.2 hatmcp[cli]bereits im Projekt deklariert.
4.2. So funktioniert die Interaktion
Das läuft Ende zu Ende ab, wenn Claude einen Feature Lookup benötigt:
- Claude Desktop startet einen dedizierten MCP-Client für den Server und entdeckt dessen Tools.
- Der Host macht die Tool-Beschreibungen für Claude verfügbar.
- Claude kann einen Tool Call anfordern, wenn die Frage Feature-Store-Daten benötigt.
- Der MCP-Client sendet diese Anfrage an den Server.
- Der Server führt die Python-Funktion aus und gibt das Ergebnis an den Host zurück.
- Der Host kann das Ergebnis für die finale Antwort an Claude weitergeben.
Diese Trennung zwischen Host, Client und Server folgt der MCP-Architekturspezifikation. Resources stellen der Host-Anwendung ebenfalls Kontext bereit. MCP verlangt nicht, dass der Host jede Resource an das Model weitergibt.
4.3. Beispielabfragen
Starte Claude Desktop neu und probiere einige Prompts aus:
-
„Liste alle verfügbaren Features auf.“ Das deterministische Ergebnis enthält die beiden als Seed angelegten Keys:
user_123undproduct_abc. -
„Rufe den Feature-Vektor für user_123 ab.“ Die Antwort enthält den Vektor sowie die Metadaten
premiumausdatabase.py. -
„Speichere ein neues Feature für
new_itemmit dem Vector-JSON-String[0.5, 0.5]und dem Metadaten-JSON-String{"type": "test"}. Rufe anschließendnew_itemab und zeige den gespeicherten Vektor und die Metadaten.“ Beide Argumente müssen gültiges JSON enthalten. Der Schreibvorgang ist erst erfolgreich, nachdem der Server Vektor und Metadaten validiert hat. Der anschließende Read überprüft den vollständigen Write/Read-Roundtrip.
4.4. Was mein Claude-Desktop-Lauf gezeigt hat
Die folgenden Screenshots stammen aus demselben Lauf im Juni 2025, bevor ich das Fixture des Artikels auf zwei Zeilen reduziert habe. Sie zeigen, was Claude Desktop nach der Verbindung mit meinem Server angezeigt hat. Es handelt sich um Beobachtungen aus diesem Lauf, nicht um garantiertes MCP-Output. Der Server gibt Tool- und Resource-Daten zurück; Claude wählt ein Tool aus und formuliert die Erklärung rund um das Ergebnis.
Zuerst bat ich Claude, das Datenbankschema anzuzeigen:

Claude lag bei einem wichtigen Detail falsch. Es bezeichnete das System als NoSQL- oder Document Store, obwohl database.py SQLite verwendet und eine relationale Tabelle erstellt. Die Spalte metadata enthält JSON-Text, aber dadurch ändert sich die Database Engine nicht. Der Screenshot erinnert daran, dass eine plausible Model-Erklärung nicht der Tool-Vertrag ist. Prüfe das zurückgegebene CREATE TABLE-Statement oder den Quellcode, wenn diese Unterscheidung relevant ist.
Ich bat Claude außerdem, die verfügbaren Features aufzulisten:

Die vier Keys stimmen mit der umfangreicheren lokalen Datenbank aus dem Inspector überein. Bezeichnungen wie „user embedding“ und „model embedding“ sind Claudes Interpretation von Namen und Metadaten. list_features selbst garantiert nur die Zeilen, die seine Implementierung zurückgibt.
Zum Schluss rief ich product_abc ab:

Hier stammten Vektor und Metadaten aus dem Tool Result. Die Ausführungen zu Ähnlichkeit, Empfehlungen und Clustering stammten von Claude. Das sind mögliche Einsatzgebiete für ein Embedding, aber der Server dieses Tutorials speichert und ruft nur Vektoren ab. Er implementiert keine Nearest-Neighbor-Suche.
5. Fehlerbehebung
Einige Failure Modes, die du kennen solltest:
-
„Server connection failed“:
- Prüfe unter macOS die Logs in
~/Library/Logs/Claude/mcp.log. - Stelle sicher, dass die Konfiguration einen absoluten und keinen relativen Pfad verwendet.
- Stelle sicher, dass
uvimPATHvon Claude Desktop liegt. Falls nicht, gib den vollständigen Binary-Pfad an (which uvzeigt dir, wo sich die Datei befindet).
- Prüfe unter macOS die Logs in
-
„Tool execution error“:
- Reproduziere den Fehler im Inspector mit
uv run mcp dev featurestore_server.py. Der Inspector zeigt den Raw Error an, den Claude Desktop normalerweise verschluckt. - Prüfe, ob
features.dbnebendatabase.pyerstellt wird. Der Pfad stammt ausget_db_path(), das ihn relativ zum Script auflöst. Ein wechselndes Arbeitsverzeichnis sollte die Datei daher nicht verschieben.
- Reproduziere den Fehler im Inspector mit
6. Fazit
Das ist bereits alles: ein FastMCP-Server, ein SQLite-Store und eine Claude-Desktop-Konfiguration, die auf einen uv run-Befehl verweist. Dieselbe Struktur funktioniert für die meisten Dinge, die sich in eine Python-Funktion kapseln lassen. Ersetze die SQLite-Aufrufe durch einen echten Feature Store, eine interne API oder eine Model Registry, und der Server bleibt klein.
Die wichtigsten Erkenntnisse
- MCP ist eine Tool-Grenze und kein Grund, jede interne Funktion bereitzustellen.
- Halte Server-Tools klein, typisiert und ohne LLM einfach testbar.
- Verwende uv, damit sich die Tutorial-Umgebung von Grund auf neu erstellen lässt.
- Behandle den MCP-Server als Production Code, sobald ein Agent ihn aufrufen kann.
Referenzen
- Historisches Begleit-Repository (enthält den aktuellen Inline-Code nicht)
- Einführung in MCP
- MCP Python SDK
- Claude Desktop
- uv