MCP-server met Python, uv en FastMCP
Automatische vertaling
Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.
Deze MCP-servertutorial bouwt met FastMCP een kleine feature-storeserver, draait hem met uv en verbindt hem met Claude Desktop. Hij is bedoeld voor Python-ontwikkelaars die één lokale databaseworkflow aan een MCP-host willen aanbieden. Aan het eind heb je een omgeving met vastgepinde v1 SDK, een server met drie tools en een resource, en een lokale controle die je uitvoert voordat je Claude Desktop verbindt.
Wat is een MCP-server?
Het Model Context Protocol (MCP) is een open protocol om AI-applicaties met externe systemen te verbinden. Het vervangt een aparte, eigen integratie per tool door één set afspraken.
In deze tutorial bouwen we een FeatureStoreLite MCP-server. Hij staat tussen een LLM en een feature store, oftewel een database met vooraf berekende ML-features. De server biedt tools om feature-vectoren op te vragen en te schrijven, met als sleutel een gebruiker, product of document.
Waarom dit bouwen?
Een feature-pipeline debuggen betekent meestal naar SQL overstappen of een wegwerpscript schrijven om een waarde te controleren. Met de draaiende server vraag je het in plaats daarvan aan Claude: “Wat is de feature-vector voor user_123?” of “Laat de metadata van product_abc zien.”
Waarom uv?
We gebruiken uv om pakketten te installeren, dependencies op te lossen en de virtuele omgeving te beheren. De Claude Desktop-configuratie start dit project vanuit zijn eigen map met --locked, zodat het de hier gedeclareerde mcp[cli]-dependency gebruikt en de versies uit uv.lock.
Tools, resources en prompts
Een MCP-server kan drie soorten dingen aanbieden. De MCP-specificatie scheidt ze op basis van wie beslist om ze te gebruiken:
| Primitive | Wie activeert het | In deze server |
|---|---|---|
| Tool | Het model besluit hem aan te roepen | get_feature, store_feature, list_features |
| Resource | De host bepaalt hoe de resource in de context komt | schema://main |
| Prompt | De gebruiker kiest hem, als een opgeslagen berichtsjabloon | Hier niet gebruikt |
Tools laten het model om een actie vragen. Resources leveren context via de host; paragraaf 4.4 laat zien waarom het aanbieden van een resource niet garandeert dat een antwoord die gebruikt.
Architectuuroverzicht
Het diagram toont welke onderdelen op je machine draaien en welke data naar het model gaat:
- De gebruiker stelt een vraag in natuurlijke taal.
- Claude Desktop is de MCP-host. Hij stuurt het gesprek naar het Claude-model, maakt één MCP-client voor deze server en beheert de verbinding.
- Onze
FastMCP-server biedt drie tools (get_feature,store_feature,list_features) en één resource (schema://main). - SQLite is de opslag voor de feature-vectoren.
De host geeft de toollijst door aan Claude. Claude kan om een tool call vragen, maar de MCP-client per server verstuurt de protocolberichten. De server en SQLite draaien lokaal, maar tool results gaan als onderdeel van het gesprek naar het model. Stel alleen data beschikbaar die je ook in de chat zou plakken.
1. Setup en installatie
1.1. uv installeren
Als je uv nog niet hebt, installeer het dan. De rest van de tutorial gaat ervan uit dat het in je PATH staat.
# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
# Or via Homebrew
brew install uv
1.2. Het project initialiseren
Maak een nieuwe map en initialiseer een Python-project. uv init maakt een pyproject.toml voor je aan.
# 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"
Deze tutorial gebruikt de v1-API van de MCP Python SDK. De officiële v1 SDK-documentatie schrijft v1-gebruikers voor mcp>=1.28,<2 vast te pinnen. Houd dus de bovengrens <2 aan totdat je de code migreert. uv.lock legt na dit commando de volledige opgeloste omgeving vast.
De code gebruikt de FastMCP die met de officiële SDK meekomt (mcp.server.fastmcp). Het losse fastmcp-pakket is een apart project met een andere API, dus meng de documentatie daarvan niet met deze code.
2. De server bouwen
Twee bestanden, gescheiden per verantwoordelijkheid:
database.pyverzorgt de SQLite-bewerkingen.featurestore_server.pydefinieert de MCP-server.
2.1. De databaselaag (database.py)
Deze module beheert de SQLite-verbinding en een paar helpers. We vullen hem met twee voorbeeldrijen, zodat de server bij de eerste query iets kan teruggeven.
Maak database.py aan:
# 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!")
Initialiseer de database:
uv run python database.py
2.2. De MCP-server (featurestore_server.py)
FastMCP doet het meeste werk. Decoreer een gewone Python-functie en ze wordt geregistreerd als MCP-tool of -resource. FastMCP bouwt het invoerschema van elke tool op uit de type hints en gebruikt de docstring als toolbeschrijving. Het model ziet beide, dus schrijf de docstring voor het model: zeg wat de tool doet en hoe elk argument eruitziet.
Maak featurestore_server.py aan:
# 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 deze code bepalen wat het model stuurt en wat het terugkrijgt:
- Getypeerde
vector. Het tool schema vraagt om een niet-lege array van getallen. Met SDK 1.30 zet Pydantic ook booleans en numerieke strings om in floats:[true, false]wordt[1.0, 0.0], en["1.5", "2"]wordt[1.5, 2.0]. Daarna controleert het vóór de functie draait dat de lijst niet leeg is en elke waarde eindig is. Stuurt een model de vector als JSON-string, zoals"[1, NaN]", dan parseert FastMCP eerst de string.allow_inf_nan=Falseweigert daarnaNaNen oneindige waarden, die de JSON-parser van Python standaard accepteert. - Getypeerde
metadata. Het schema accepteert een JSON-object,nullof een weggelaten waarde.allow_nan=FalsehoudtNaNook uit de opgeslagen metadata. - Opgeworpen fouten.
get_featurewerpt eenValueErrorop voor een ontbrekende sleutel. Een opgeworpen exception bereikt de client als toolfout metisError: true; een teruggegeven string ziet eruit als een geslaagd resultaat. - Overschrijven.
store_featuregebruiktINSERT OR REPLACEen vervangt dus een bestaande sleutel. De docstring zegt dit, zodat het model het weet voordat het de tool aanroept.
3. Testen met MCP Inspector
Controleer de server met de MCP Inspector voordat je hem in Claude aansluit. Het is een kleine webinterface die op dezelfde manier met de server verbindt als een host. Daarin kun je de resources van de server bekijken, elke tool met je eigen argumenten aanroepen en elk verzoek en elk antwoord zien.
De SDK start de Inspector via npx, dus deze stap vereist ook Node.js en npm met npx in je PATH. Inspector 2.10.1 vereist Node.js 22.19.0 of nieuwer. Controleer de geïnstalleerde commando’s en start daarna de Inspector:
node --version
npx --version
uv run mcp dev featurestore_server.py
Het commando start de server onder de MCP Inspector via stdio. Gebruik de browser-URL die het commando afdrukt. De poort van de Inspector-UI hangt van de implementatie af, dus ga niet uit van een vaste poort.
Roep get_feature aan met key="user_123". Als het de JSON van de seed-rij teruggeeft, werkt de server. Roep het daarna aan met een sleutel die niet bestaat: het resultaat is een toolfout met isError: true. Dit controleert het lokale protocolpad, de toolregistratie, de databaselookup en het foutpad. Het test niet hoe Claude tools kiest; dat controleert paragraaf 4.
De Inspector leest het SQL-schema uit de resource schema://main:
4. Verbinden met Claude Desktop
Zodra de Inspector bevestigt dat de server werkt, registreer je hem bij Claude Desktop.
4.1. Claude configureren
Bewerk het configuratiebestand van Claude Desktop:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%/Claude/claude_desktop_config.json
Voeg je server toe aan het object mcpServers:
{
"mcpServers": {
"featurestore": {
"command": "uv",
"args": [
"run",
"--directory",
"/ABSOLUTE/PATH/TO/mcp-featurestore",
"--locked",
"mcp",
"run",
"/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
]
}
}
}
Belangrijk: gebruik absolute paden voor zowel de projectmap als featurestore_server.py. Claude Desktop start de server als apart proces. uv run zoekt bij een commando als mcp een project vanuit de werkmap, dus --directory selecteert de pyproject.toml en uv.lock van dit project; --locked faalt daarna in plaats van het lockfile te wijzigen. Het commando uv add uit stap 1.2 heeft mcp[cli] al in het project gedeclareerd.
De SDK heeft ook een snelkoppeling: uv run mcp install featurestore_server.py schrijft de Claude Desktop-vermelding voor je. In mcp 1.30 draait die vermelding uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. Er staat geen --directory in, dus de uv.lock van dit project wordt niet gebruikt, en --with mcp[cli] heeft geen bovengrens <2. Gebruik het voor een snelle proef; de handmatige configuratie hierboven is de reproduceerbare.
4.2. Hoe de interactie werkt
Dit draait er van begin tot eind wanneer Claude een feature moet opzoeken. Stap 1 en 2 draaien één keer, wanneer Claude Desktop de server start. Stap 3 tot en met 6 draaien bij elke vraag waarvoor feature-data nodig is.
- Claude Desktop start een eigen MCP-client voor de server en ontdekt de tools ervan.
- De host stelt de toolbeschrijvingen beschikbaar aan Claude.
- Claude kan om een tool call vragen wanneer de vraag feature-store-data nodig heeft.
- De MCP-client stuurt dat verzoek naar de server.
- De server voert de Python-functie uit en geeft het resultaat terug aan de host.
- De host kan het resultaat aan Claude geven voor het uiteindelijke antwoord.
Deze scheiding tussen host, client en server volgt de MCP-architectuurspecificatie.
4.3. Voorbeeldvragen
Herstart Claude Desktop en probeer een paar prompts:
-
“Toon alle beschikbare features.” Het deterministische resultaat bevat de twee geseede sleutels:
user_123enproduct_abc. -
“Haal de feature-vector voor user_123 op.” Het antwoord bevat de vector en de metadata
premiumuitdatabase.py. -
“Sla een nieuwe feature voor
new_itemop met de vector[0.5, 0.5]en de metadata{"type": "test"}. Haal daarnanew_itemop en toon de opgeslagen vector en metadata.” De gevalideerde vector moet na de conversie niet leeg zijn en alleen eindige floats bevatten. Metadata mag een JSON-object ofnullzijn, of worden weggelaten. De volgende leesactie controleert de volledige schrijf-leesronde. -
“Haal de feature-vector voor user_999 op.”
get_featurewerpt een fout op voor de ontbrekende sleutel. Claude hoort te zeggen dat de sleutel niet bestaat en kanlist_featuresaanroepen om de geldige sleutels te tonen.
4.4. Claudes antwoorden lezen
De server geeft tool- en resourcedata terug. Claude kiest de tool en schrijft de uitleg rond het resultaat.
Deze screenshot toont een uitvoering uit juni 2025 met een eerdere versie van het voorbeeld. Ik vroeg Claude het databaseschema te tonen:
Claude noemde het systeem een NoSQL- of documentopslag en gaf een conceptueel schema, maar database.py maakt een relationele SQLite-tabel. Het getoonde antwoord is niet het CREATE TABLE-statement dat schema://main teruggeeft. De screenshot legt niet vast of Claude Desktop die resource las of welke context Claude kreeg, dus het toont de oorzaak van de fout niet aan.
Resources zijn applicatiegestuurd: de host bepaalt hoe ze via gebruikersselectie of automatische opname in de modelcontext komen. MCP vereist niet dat de host elke resource aan het model doorgeeft. Als het model het schema via een tool call moet aanvragen, stel het dan ook als tool beschikbaar.
Een aannemelijke uitleg van het model is niet hetzelfde als wat de tool teruggaf. Controleer het tool result of de bron wanneer het detail ertoe doet.
4.5. Andere MCP-hosts
De server hangt niet af van Claude Desktop. Elke host die stdio-servers op dezelfde machine kan starten, kan hetzelfde uv-commando draaien. Claude Desktop gebruikt de JSON uit paragraaf 4.1; de andere twee zijn:
- 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 na--is het commando dat de server start. - Cursor leest dezelfde JSON-vorm
mcpServersuit~/.cursor/mcp.json, of.cursor/mcp.jsonin een project.
Een host op een andere machine kan geen stdio-proces op de jouwe starten. Draai een server die meerdere mensen delen via Streamable HTTP met mcp.run(transport="streamable-http") en zet er authenticatie voor.
5. Problemen oplossen
Zoek eerst uit welk onderdeel faalt voordat je de code wijzigt: opstartfouten staan in de logs, toolfouten herhaal je zonder model in de Inspector, en bij een fout antwoord vergelijk je het tool result met Claudes tekst.
-
“Server connection failed”:
- Bekijk de logs in
~/Library/Logs/Claude/op macOS of%APPDATA%\Claude\logsop Windows.mcp.logregistreert verbindingsgebeurtenissen.mcp-server-featurestore.login dezelfde map bevat de eigen stderr van de server, dus een startup-traceback staat daar (Claude Desktop-logs). - Controleer of de configuratie een absoluut pad gebruikt, geen relatief pad.
- Controleer of
uvin hetPATHvan Claude Desktop staat. Zo niet, verwijs dan naar het volledige pad van de binary (which uvvertelt je waar die staat). - Bij stdio draagt stdout de protocolberichten. Log naar stderr; een
print()in de server beschadigt die stream en kan de verbinding verbreken.
- Bekijk de logs in
-
“Tool execution error”:
- Reproduceer hem in de Inspector met
uv run mcp dev featurestore_server.py. De Inspector toont de foutmelding en de argumenten die haar veroorzaakten. - Controleer of
features.dbnaastdatabase.pywordt aangemaakt. Het pad komt uitget_db_path(), dat het ten opzichte van het script bepaalt, dus een wisselende werkmap hoort het bestand niet te verplaatsen.
- Reproduceer hem in de Inspector met
-
De aanroep slaagt, maar het antwoord is fout:
- Open het tool result in de chat en vergelijk het met Claudes tekst. Paragraaf 4.4 toont een modelantwoord dat de database verkeerd beschreef.
6. Voordat een agent het echt gebruikt
store_featureoverschrijft elke sleutel, ook de seed-rijen. Laat goedkeuring per aanroep voor schrijftools aan in de host, of verplaats ze naar een aparte server die je alleen inschakelt wanneer je ze nodig hebt.- Tool results bereiken de modelaanbieder als onderdeel van het gesprek. Geef geen data terug die op de machine moet blijven.
- Een server die meerdere mensen delen, heeft Streamable HTTP en authenticatie nodig in plaats van stdio (paragraaf 4.5).
7. Conclusie
Dat is alles: een FastMCP-server, een SQLite-opslag en een Claude Desktop-configuratie die naar een uv run-commando wijst. Dezelfde opzet werkt voor de meeste dingen die je in een Python-functie kunt verpakken. Vervang de SQLite-aanroepen door een echte feature store, een interne API of een modelregister, en de server blijft klein.

