MCP-Server mit Python, uv und FastMCP
Automatische Übersetzung
Dieser Artikel wurde automatisch aus der englischen Originalversion übersetzt.
Dieses MCP-Server-Tutorial baut mit FastMCP einen kleinen Feature-Store-Server, startet ihn mit uv und verbindet ihn mit Claude Desktop. Es richtet sich an Python-Entwickler, die einen lokalen Datenbank-Workflow für einen MCP-Host bereitstellen wollen. Am Ende hast du eine Umgebung mit festgelegtem v1-SDK, einen Server mit drei Tools und einer Resource sowie einen lokalen Test, den du ausführst, bevor du Claude Desktop verbindest.
Was ist ein MCP-Server?
Das Model Context Protocol (MCP) ist ein offenes Protokoll, das AI-Anwendungen mit externen Systemen verbindet. Es ersetzt eine eigene Integration für jedes Tool durch einen einheitlichen Satz von Konventionen.
In diesem Tutorial bauen wir einen FeatureStoreLite-MCP-Server. Er sitzt zwischen einem LLM und einem Feature Store, also einer Datenbank mit vorberechneten ML-Features. Der Server stellt Tools bereit, um Feature-Vektoren abzufragen und zu schreiben, die über Nutzer, Produkt oder Dokument adressiert werden.
Warum das Ganze?
Wer eine Feature-Pipeline debuggt, landet meist in SQL oder schreibt ein Wegwerfskript, um einen Wert zu prüfen. Läuft der Server, fragst du stattdessen Claude: “Wie lautet der Feature-Vektor für user_123?” oder “Zeig mir die Metadaten für product_abc.”
Warum uv?
Wir nutzen uv, um Pakete zu installieren, Abhängigkeiten aufzulösen und die virtuelle Umgebung zu verwalten. Die Claude-Desktop-Konfiguration startet dieses Projekt aus seinem eigenen Verzeichnis mit --locked. So nutzt sie die hier deklarierte Abhängigkeit mcp[cli] und die in uv.lock festgehaltenen Versionen.
Tools, Resources und Prompts
Ein MCP-Server kann drei Arten von Dingen anbieten. Die MCP-Spezifikation unterscheidet sie danach, wer entscheidet, sie zu verwenden:
| Primitive | Wer löst es aus | In diesem Server |
|---|---|---|
| Tool | Das Model entscheidet, es aufzurufen | get_feature, store_feature, list_features |
| Resource | Der Host entscheidet, wie sie in den Kontext aufgenommen wird | schema://main |
| Prompt | Der Nutzer wählt ihn aus, wie eine gespeicherte Vorlage | Hier nicht verwendet |
Tools lassen das Model eine Aktion anfordern. Resources liefern über den Host Kontext; Abschnitt 4.4 zeigt, warum eine bereitgestellte Resource nicht garantiert, dass eine Antwort sie nutzt.
Architekturüberblick
Das Diagramm zeigt, welche Teile auf deinem Rechner laufen und welche Daten an das Model gehen:
- Der Nutzer stellt eine Frage in natürlicher Sprache.
- Claude Desktop ist der MCP-Host. Es sendet die Konversation an das Claude-Model, erstellt einen MCP-Client für diesen Server und verwaltet die Verbindung.
- Unser
FastMCP-Server stellt drei Tools (get_feature,store_feature,list_features) und eine Resource (schema://main) bereit. - SQLite ist der Speicher für die Feature-Vektoren.
Der Host übergibt die Tool-Liste an Claude. Claude kann einen Tool Call anfordern, aber der MCP-Client pro Server sendet die Protokollnachrichten. Der Server und SQLite laufen lokal, doch Tool Results gehen als Teil der Konversation an das Model. Stelle nur Daten bereit, die du auch in den Chat einfügen würdest.
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 liegt.
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
1.2. Projekt initialisieren
Lege ein neues Verzeichnis an und initialisiere ein Python-Projekt. uv init erstellt dir eine 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"
Dieses Tutorial nutzt die API des MCP Python SDK v1. Die offizielle v1-SDK-Dokumentation weist v1-Nutzer an, mcp>=1.28,<2 festzulegen. Behalte die Obergrenze <2 also bei, bis du den Code migrierst. uv.lock hält nach diesem Befehl die vollständig aufgelöste Umgebung fest.
Der Code nutzt das FastMCP, das im offiziellen SDK enthalten ist (mcp.server.fastmcp). Das eigenständige Paket fastmcp ist ein separates Projekt mit anderer API. Mische deshalb dessen Dokumentation nicht mit diesem Code.
2. Den Server bauen
Zwei Dateien, getrennt nach Zuständigkeit:
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 ein paar Hilfsfunktionen. Wir füllen es mit zwei Beispielzeilen, 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 erledigt den größten Teil der Arbeit. Dekoriere eine gewöhnliche Python-Funktion, und sie wird als MCP-Tool oder -Resource registriert. FastMCP erzeugt das Eingabeschema jedes Tools aus den Type Hints und nutzt den Docstring als Tool-Beschreibung. Das Model sieht beides. Schreibe den Docstring deshalb für das Model: Sage, was das Tool tut und wie jedes Argument aussieht.
Erstelle 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()
Vier Details in diesem Code bestimmen, was das Model sendet und was es zurückbekommt:
- Typisierter
vector. Das Tool Schema verlangt ein nicht leeres Array von Zahlen. Mit SDK 1.30 wandelt Pydantic auch boolesche Werte und numerische Strings in Floats um:[true, false]wird zu[1.0, 0.0], und["1.5", "2"]wird zu[1.5, 2.0]. Danach prüft es vor dem Funktionsaufruf, dass die Liste nicht leer und jeder Wert endlich ist. Sendet ein Model den Vektor als JSON-String wie"[1, NaN]", parst FastMCP zuerst den String.allow_inf_nan=Falselehnt dannNaNund Unendlichkeitswerte ab, die Pythons JSON-Parser standardmäßig akzeptiert. - Typisierte
metadata. Das Schema akzeptiert ein JSON-Objekt,nulloder einen ausgelassenen Wert.allow_nan=FalsehältNaNauch aus den gespeicherten Metadaten heraus. - Ausgelöste Fehler.
get_featurelöst bei einem fehlenden Schlüssel einenValueErroraus. Eine ausgelöste Exception erreicht den Client als Tool-Fehler mitisError: true; ein zurückgegebener String sieht wie ein erfolgreiches Ergebnis aus. - Überschreiben.
store_featurenutztINSERT OR REPLACEund ersetzt damit einen vorhandenen Schlüssel. Der Docstring sagt das ausdrücklich, damit das Model es weiß, bevor es das Tool aufruft.
3. Testen mit dem MCP Inspector
Bevor du den Server in Claude einbindest, prüfe ihn mit dem MCP Inspector. Das ist eine kleine Web-Oberfläche, die sich wie ein Host mit dem Server verbindet. Dort kannst du die Resources des Servers durchsuchen, jedes Tool mit eigenen Argumenten aufrufen und jede Anfrage und Antwort sehen.
Das SDK startet den Inspector über npx. Dieser Schritt braucht deshalb auch Node.js und npm mit npx im PATH. Inspector 2.10.1 benötigt Node.js 22.19.0 oder neuer. Prüfe die installierten Befehle und starte dann den Inspector:
node --version
npx --version
uv run mcp dev featurestore_server.py
Der Befehl startet den Server über stdio unter dem MCP Inspector. Nutze die Browser-URL, die der Befehl ausgibt. Der Port der Inspector-Oberfläche hängt von der Implementierung ab. Gehe also nicht von einem festen Port aus.
Rufe get_feature mit key="user_123" auf. Gibt es das JSON der Beispielzeile zurück, funktioniert der Server. Rufe es dann mit einem Schlüssel auf, den es nicht gibt: Das Ergebnis ist ein Tool-Fehler mit isError: true. Das prüft den lokalen Protokollpfad, die Tool-Registrierung, die Datenbankabfrage und den Fehlerpfad. Es testet nicht, wie Claude Tools auswählt; das prüft Abschnitt 4.
Der Inspector liest das SQL-Schema aus der Resource schema://main:
4. Mit Claude Desktop verbinden
Sobald der Inspector bestätigt, dass der Server funktioniert, registriere ihn in Claude Desktop.
4.1. Claude konfigurieren
Bearbeite die Konfigurationsdatei von Claude Desktop:
- 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 absolute Pfade für das Projektverzeichnis und für featurestore_server.py. Claude Desktop startet den Server als eigenen Prozess. uv run findet bei einem Befehl wie mcp ein Projekt über sein Arbeitsverzeichnis. Deshalb wählt --directory die pyproject.toml und die uv.lock dieses Projekts aus; --locked schlägt dann fehl, statt die Lockfile zu ändern. Der Befehl uv add aus Schritt 1.2 hat mcp[cli] bereits im Projekt deklariert.
Das SDK hat auch eine Abkürzung: uv run mcp install featurestore_server.py schreibt den Eintrag für Claude Desktop für dich. In mcp 1.30 führt dieser Eintrag uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py aus. Er hat kein --directory und nutzt daher nicht die uv.lock dieses Projekts, und --with mcp[cli] hat keine Obergrenze <2. Nutze ihn für einen schnellen Versuch; die manuelle Konfiguration oben ist die reproduzierbare.
4.2. Wie die Interaktion abläuft
Das läuft von Anfang bis Ende ab, wenn Claude eine Feature-Abfrage braucht. Die Schritte 1 und 2 laufen einmal, wenn Claude Desktop den Server startet. Die Schritte 3 bis 6 laufen bei jeder Frage, die Feature-Daten braucht.
- Claude Desktop startet einen eigenen MCP-Client für den Server und erkennt dessen Tools.
- Der Host stellt Claude die Tool-Beschreibungen zur Verfügung.
- Claude kann einen Tool Call anfordern, wenn die Frage Daten aus dem Feature Store braucht.
- 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 Claude das Ergebnis für die endgültige Antwort bereitstellen.
Diese Trennung in Host, Client und Server folgt der MCP-Architekturspezifikation.
4.3. Beispielabfragen
Starte Claude Desktop neu und probiere ein paar Prompts aus:
-
“Liste alle verfügbaren Features auf.” Das deterministische Ergebnis enthält die beiden Beispielschlüssel:
user_123undproduct_abc. -
“Hole den Feature-Vektor für user_123.” Die Antwort enthält den Vektor und die Metadaten
premiumausdatabase.py. -
“Speichere ein neues Feature für
new_itemmit dem Vektor[0.5, 0.5]und den Metadaten{"type": "test"}. Hole anschließendnew_itemund zeige den gespeicherten Vektor und die Metadaten.” Der validierte Vektor muss nach der Umwandlung nicht leer sein und nur endliche Floats enthalten. Metadaten dürfen ein JSON-Objekt odernullsein oder fehlen. Das anschließende Lesen prüft den vollständigen Schreib-Lese-Zyklus. -
“Hole den Feature-Vektor für user_999.”
get_featurelöst für den fehlenden Schlüssel einen Fehler aus. Claude sollte sagen, dass der Schlüssel nicht existiert, und kannlist_featuresaufrufen, um die gültigen Schlüssel zu zeigen.
4.4. Claudes Antworten lesen
Der Server liefert Tool- und Resource-Daten. Claude wählt das Tool und schreibt die Erklärung um das Ergebnis herum.
Dieser Screenshot zeigt einen Durchlauf vom Juni 2025 mit einer früheren Version des Beispiels. Ich bat Claude, das Datenbankschema anzuzeigen:
Claude nannte das System einen NoSQL- oder Dokumentenspeicher und gab ein konzeptionelles Schema an, aber database.py erstellt eine relationale SQLite-Tabelle. Die gezeigte Antwort ist nicht die CREATE TABLE-Anweisung, die schema://main zurückgibt. Der Screenshot hält nicht fest, ob Claude Desktop diese Resource gelesen hat oder welchen Kontext Claude erhielt. Er belegt daher nicht die Ursache des Fehlers.
Resources sind anwendungsgesteuert: Der Host entscheidet, wie sie durch Nutzerauswahl oder automatische Aufnahme in den Model-Kontext gelangen. MCP verlangt nicht, dass der Host jede Resource an das Model weitergibt. Soll das Model das Schema durch einen Tool Call anfordern, stelle es zusätzlich als Tool bereit.
Eine plausible Erklärung des Models ist nicht dasselbe wie das, was das Tool zurückgegeben hat. Kommt es auf das Detail an, prüfe das Tool Result oder den Quellcode.
4.5. Andere MCP-Hosts
Der Server hängt nicht von Claude Desktop ab. Jeder Host, der stdio-Server auf demselben Rechner startet, kann denselben uv-Befehl ausführen. Claude Desktop nutzt das JSON aus Abschnitt 4.1; die anderen beiden sind:
- 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. Alles nach--ist der Befehl, der den Server startet. - Cursor liest dieselbe
mcpServers-JSON-Struktur aus~/.cursor/mcp.jsonoder.cursor/mcp.jsonin einem Projekt.
Ein Host auf einem anderen Rechner kann auf deinem Rechner keinen stdio-Prozess starten. Für einen Server, den mehrere Personen teilen, betreibe ihn über Streamable HTTP mit mcp.run(transport="streamable-http") und schalte eine Authentifizierung davor.
5. Fehlerbehebung
Finde heraus, welcher Teil fehlgeschlagen ist, bevor du den Code änderst: Startfehler stehen in den Logs, Tool-Fehler lassen sich im Inspector ohne Model wiederholen, und bei einer falschen Antwort vergleichst du das Tool Result mit Claudes Text.
-
“Server connection failed”:
- Prüfe die Logs in
~/Library/Logs/Claude/unter macOS oder%APPDATA%\Claude\logsunter Windows.mcp.logprotokolliert Verbindungsereignisse.mcp-server-featurestore.logim selben Ordner enthält den stderr des Servers, ein Traceback beim Start steht also dort (Claude-Desktop-Logs). - Prüfe, ob die Konfiguration einen absoluten Pfad nutzt, keinen relativen.
- Prüfe, ob
uvimPATHvon Claude Desktop liegt. Falls nicht, verweise auf den vollständigen Pfad der Binärdatei (which uvzeigt, wo sie liegt). - Bei stdio trägt stdout die Protokollnachrichten. Schreibe Logs nach stderr; ein
print()im Server stört diesen Stream und kann die Verbindung unterbrechen.
- Prüfe die Logs in
-
“Tool execution error”:
- Reproduziere den Fehler im Inspector mit
uv run mcp dev featurestore_server.py. Der Inspector zeigt die Fehlermeldung und die Argumente, die sie ausgelöst haben. - Prüfe, ob
features.dbnebendatabase.pyangelegt wird. Der Pfad kommt ausget_db_path(), das ihn relativ zum Skript auflöst. Ein wechselndes Arbeitsverzeichnis sollte die Datei also nicht verschieben.
- Reproduziere den Fehler im Inspector mit
-
Der Aufruf gelingt, aber die Antwort ist falsch:
- Öffne das Tool Result im Chat und vergleiche es mit Claudes Text. Abschnitt 4.4 zeigt eine Model-Antwort, die die Datenbank falsch beschrieb.
6. Bevor ein Agent den Server produktiv nutzt
store_featureüberschreibt jeden Schlüssel, auch die Beispielzeilen. Lass für Schreib-Tools im Host die Freigabe pro Aufruf eingeschaltet, oder verlege sie in einen separaten Server, den du nur bei Bedarf aktivierst.- Tool Results erreichen den Model-Anbieter als Teil der Konversation. Gib keine Daten zurück, die auf dem Rechner bleiben müssen.
- Ein Server, den mehrere Personen teilen, braucht statt stdio Streamable HTTP und Authentifizierung (Abschnitt 4.5).
7. Fazit
Das ist alles: ein FastMCP-Server, ein SQLite-Speicher und eine Claude-Desktop-Konfiguration, die auf einen uv run-Befehl zeigt. Dieselbe Form funktioniert für das meiste, was sich in eine Python-Funktion packen lässt. Ersetze die SQLite-Aufrufe durch einen echten Feature Store, eine interne API oder eine Model Registry, und der Server bleibt klein.

