Ga naar inhoud

Automatische vertaling

Dit artikel is automatisch vertaald vanuit de oorspronkelijke Engelse versie.

MCP Server-tutorial met uv en FastMCP: bouw een FeatureStoreLite-server

In deze MCP-servertutorial bouwen we een kleine feature-store-server met FastMCP, draaien we die met uv, en koppelen we hem aan Claude Desktop.


Wat is een MCP-server?

Het Model Context Protocol (MCP) is een open standaard om AI-assistenten zoals Claude te verbinden met externe data en tools. Eén protocol, één set conventies, in plaats van een nieuwe integratie voor elke tool die je wilt ontsluiten.

In deze tutorial bouwen we een FeatureStoreLite MCP-server. Die zit tussen een LLM en een feature store (een database met vooraf berekende ML-features), en stelt tools beschikbaar om featurevectoren op te vragen en weg te schrijven, gesleuteld op user, product of document.

Waarom dit bouwen?

Je bent een ML engineer die een pipeline debugt. In plaats van SQL in te duiken of snel een script te schrijven om featurewaarden te controleren, vraag je Claude direct: "Wat is de featurevector voor user_123?" of "Laat me de metadata zien voor product_abc.". De MCP-server maakt dat mogelijk.

Waarom uv gebruiken?

We gebruiken uv, een Python package installer en projectmanager die veel sneller is dan pip en dependency resolution en virtualenvs in één tool afhandelt. De andere reden: uv run --with mcp[cli] ... laat ons later dependencies inline declareren in de Claude Desktop-config, zodat er geen aparte venv is die je synchroon moet houden.

Architectuuroverzicht

De vier onderdelen en hoe ze in elkaar passen:

MCP-architectuur

  1. De gebruiker stelt een vraag in natuurlijke taal.
  2. Claude Desktop is de MCP-client. Die kiest op basis van de vraag een tool en roept die aan.
  3. Onze FastMCP-server stelt get_feature en store_feature beschikbaar als MCP-tools.
  4. SQLite is de achterliggende opslag voor de featurevectoren.

2. Setup en installatie

2.1. Installeer uv

Als je uv nog niet hebt, installeer het dan. De rest van deze tutorial gaat ervan uit dat het op je PATH staat.

# macOS/Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Or via Homebrew
brew install uv

2.2. Initialiseer het project

Maak een nieuwe directory aan 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]"

3. De server bouwen

Twee bestanden, gescheiden per verantwoordelijkheid:

  1. database.py handelt SQLite-bewerkingen af.
  2. featurestore_server.py definieert de MCP-server.

3.1. De databaselaag (database.py)

Deze module beheert de SQLite-verbinding en een paar helpers. We vullen die 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

3.2. De MCP-server (featurestore_server.py)

FastMCP doet het meeste werk. Voorzie een gewone Python-functie van een decorator en die wordt geregistreerd als MCP-tool of resource. De docstring wordt de beschrijving die de LLM ziet, dus schrijf die voor een LLM, niet alleen voor mensen.

Maak featurestore_server.py aan:

# featurestore_server.py
import json
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()


@mcp.resource("schema://main")
def get_schema() -> str:
    """
    Resource: Provide the database schema.
    Resources are passive data that LLMs can read like files.
    """
    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.
    """
    conn = get_db_connection()
    try:
        # Validate that vector is valid JSON
        json.loads(vector)

        conn.execute(
            "INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
            (key, vector, metadata),
        )
        conn.commit()
        return f"Successfully stored feature '{key}'"
    except json.JSONDecodeError:
        return "Error: Vector must be a valid JSON array string (e.g., '[0.1, 0.2]')"
    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()

4. Testen met MCP Inspector

Voordat je dit aan Claude koppelt, controleer je de server eerst met de MCP Inspector. Dat is een kleine web-UI om tools aan te roepen en resources direct uit te lezen.

uv run mcp dev featurestore_server.py

Het commando start de server en opent de Inspector in je browser (meestal op http://localhost:5173).

Inspector

Roep get_feature aan met key="user_123". Als die de JSON voor de seed-rij teruggeeft, werkt de server.


5. Verbinden met Claude Desktop

Zodra de Inspector bevestigt dat de server werkt, registreer je hem in Claude Desktop.

5.1. Claude configureren

Bewerk je Claude Desktop-configuratiebestand:

  • 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",
                "--with",
                "mcp[cli]",
                "mcp",
                "run",
                "/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
            ]
        }
    }
}

Belangrijk: het pad naar featurestore_server.py moet absoluut zijn. Een relatief pad faalt stilzwijgend omdat Claude Desktop vanuit een andere working directory draait.

5.2. Hoe de interactie werkt

Dit draait end-to-end wanneer Claude een feature lookup nodig heeft:

MCP-workflow

  1. Claude ziet de beschikbare tools (get_feature, list_features, enzovoort).
  2. Het beslist dat de vraag data uit de feature store nodig heeft.
  3. Het bouwt een tool call op en stuurt die naar je server.
  4. De server voert de Python-functie uit en retourneert het resultaat.
  5. Claude gebruikt dat resultaat om het definitieve antwoord te schrijven.

5.3. Voorbeeldqueries

Herstart Claude Desktop en probeer een paar prompts:

  1. "Lijst alle beschikbare features op." Vraag 2

  2. "Haal de featurevector op voor user_123." Vraag 3

  3. "Sla een nieuwe feature op voor 'new_item' met vector [0.5, 0.5] en metadata {'type': 'test'}."


6. Problemen oplossen

Een paar foutmodi die handig zijn om te kennen:

  • "Server connection failed":

    • Controleer de logs op ~/Library/Logs/Claude/mcp.log op macOS.
    • Bevestig dat de config een absoluut pad gebruikt, geen relatief pad.
    • Bevestig dat uv op de PATH van Claude Desktop staat. Zo niet, verwijs dan naar het volledige binaire pad (which uv vertelt je waar het staat).
  • "Tool execution error":

    • Reproduceer het in de Inspector met uv run mcp dev featurestore_server.py. De Inspector toont de ruwe fout, die Claude Desktop meestal inslikt.
    • Controleer dat features.db naast database.py wordt aangemaakt. Als je working directory verschuift, is het script-relatieve pad in get_db_path() wat je redt.

7. Conclusie

Dat is het hele verhaal: een FastMCP-server, een SQLite-opslaglaag, en een Claude Desktop-config die verwijst naar een uv run-commando. Dezelfde vorm werkt voor alles wat je in een Python-functie kunt wrappen. Vervang de SQLite-calls door een echte feature store, een interne API of een model registry, en de server blijft klein.

Referenties