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:

PrimitiveWie activeert hetIn deze server
ToolHet model besluit hem aan te roepenget_feature, store_feature, list_features
ResourceDe host bepaalt hoe de resource in de context komtschema://main
PromptDe gebruiker kiest hem, als een opgeslagen berichtsjabloonHier 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:

Architectuur van FeatureStoreLite: het Claude-model bij de modelprovider ontvangt je vraag, de lijst met tools en de tool results, rijen uit features.db; op je machine praten Claude Desktop en zijn MCP-client via stdin en stdout met het FeatureStoreLite-serverproces, dat drie tools en de resource schema://main biedt en de SQLite-tabel met de rijen user_123 en product_abc leest en schrijftArchitectuur van FeatureStoreLite: het Claude-model bij de modelprovider ontvangt je vraag, de lijst met tools en de tool results, rijen uit features.db; op je machine praten Claude Desktop en zijn MCP-client via stdin en stdout met het FeatureStoreLite-serverproces, dat drie tools en de resource schema://main biedt en de SQLite-tabel met de rijen user_123 en product_abc leest en schrijft

  1. De gebruiker stelt een vraag in natuurlijke taal.
  2. 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.
  3. Onze FastMCP-server biedt drie tools (get_feature, store_feature, list_features) en één resource (schema://main).
  4. 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:

  1. database.py verzorgt de SQLite-bewerkingen.
  2. featurestore_server.py definieert 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=False weigert daarna NaN en oneindige waarden, die de JSON-parser van Python standaard accepteert.
  • Getypeerde metadata. Het schema accepteert een JSON-object, null of een weggelaten waarde. allow_nan=False houdt NaN ook uit de opgeslagen metadata.
  • Opgeworpen fouten. get_feature werpt een ValueError op voor een ontbrekende sleutel. Een opgeworpen exception bereikt de client als toolfout met isError: true; een teruggegeven string ziet eruit als een geslaagd resultaat.
  • Overschrijven. store_feature gebruikt INSERT OR REPLACE en 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:

MCP Inspector met de resource get_schema en de teruggegeven SQLite CREATE TABLE-statement


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.

Sequentiediagram van één lookup van user_123 tussen het Claude-model, Claude Desktop, de FeatureStoreLite-server en features.db: tools/list en de tool descriptions bij het opstarten, en daarna per vraag het get_feature-verzoek van het model, een tools/call-bericht, de SQLite SELECT, het JSON-resultaat en het eindantwoord, genummerd van 1 tot en met 6 zoals in de lijst hieronderSequentiediagram van één lookup van user_123 tussen het Claude-model, Claude Desktop, de FeatureStoreLite-server en features.db: tools/list en de tool descriptions bij het opstarten, en daarna per vraag het get_feature-verzoek van het model, een tools/call-bericht, de SQLite SELECT, het JSON-resultaat en het eindantwoord, genummerd van 1 tot en met 6 zoals in de lijst hieronder

  1. Claude Desktop start een eigen MCP-client voor de server en ontdekt de tools ervan.
  2. De host stelt de toolbeschrijvingen beschikbaar aan Claude.
  3. Claude kan om een tool call vragen wanneer de vraag feature-store-data nodig heeft.
  4. De MCP-client stuurt dat verzoek naar de server.
  5. De server voert de Python-functie uit en geeft het resultaat terug aan de host.
  6. 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:

  1. “Toon alle beschikbare features.” Het deterministische resultaat bevat de twee geseede sleutels: user_123 en product_abc.

  2. “Haal de feature-vector voor user_123 op.” Het antwoord bevat de vector en de metadata premium uit database.py.

  3. “Sla een nieuwe feature voor new_item op met de vector [0.5, 0.5] en de metadata {"type": "test"}. Haal daarna new_item op 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 of null zijn, of worden weggelaten. De volgende leesactie controleert de volledige schrijf-leesronde.

  4. “Haal de feature-vector voor user_999 op.” get_feature werpt een fout op voor de ontbrekende sleutel. Claude hoort te zeggen dat de sleutel niet bestaat en kan list_features aanroepen 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 Desktop geeft een onjuist conceptueel schema voor FeatureStoreLite tijdens de uitvoering van juni 2025

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 mcpServers uit ~/.cursor/mcp.json, of .cursor/mcp.json in 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\logs op Windows. mcp.log registreert verbindingsgebeurtenissen. mcp-server-featurestore.log in 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 uv in het PATH van Claude Desktop staat. Zo niet, verwijs dan naar het volledige pad van de binary (which uv vertelt 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.
  • “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.db naast database.py wordt aangemaakt. Het pad komt uit get_db_path(), dat het ten opzichte van het script bepaalt, dus een wisselende werkmap hoort het bestand niet te verplaatsen.
  • 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_feature overschrijft 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.

Referenties