Serwer MCP: Python, uv i FastMCP

Tłumaczenie automatyczne

Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.

W tym tutorialu zbudujemy mały serwer MCP dla feature store z FastMCP, uruchomimy go przez uv i połączymy z Claude Desktop. Jest dla programistów Pythona, którzy chcą udostępnić hostowi MCP jeden lokalny workflow z bazą danych. Na końcu będziesz mieć środowisko z przypiętym SDK v1, serwer z trzema narzędziami i jednym zasobem oraz lokalny test do uruchomienia przed połączeniem z Claude Desktop.


Czym jest serwer MCP?

Model Context Protocol (MCP) to otwarty protokół do łączenia aplikacji AI z systemami zewnętrznymi. Zastępuje osobną, własną integrację dla każdego narzędzia jednym zestawem konwencji.

W tym tutorialu zbudujemy serwer MCP FeatureStoreLite. Działa między LLM a feature store, czyli bazą danych z wcześniej obliczonymi cechami ML. Serwer udostępnia narzędzia do odczytu i zapisu wektorów cech, kluczowanych według użytkownika, produktu lub dokumentu.

Po co to budować?

Debugowanie pipeline’u cech zwykle oznacza przejście do SQL albo napisanie jednorazowego skryptu, żeby sprawdzić wartość. Gdy serwer działa, pytasz po prostu Claude: “Jaki jest wektor cech dla user_123?” albo “Pokaż metadane produktu product_abc.”

Dlaczego uv?

Użyjemy uv do instalacji pakietów, rozwiązywania zależności i zarządzania wirtualnym środowiskiem. Konfiguracja Claude Desktop uruchomi ten projekt z jego własnego katalogu z flagą --locked, więc użyje zależności mcp[cli] zadeklarowanej tutaj i wersji zapisanych w uv.lock.

Narzędzia, zasoby i elementy typu prompt

Serwer MCP może oferować trzy rodzaje elementów. Specyfikacja MCP rozróżnia je według tego, kto decyduje o ich użyciu:

PrymitywKto go uruchamiaW tym serwerze
NarzędzieModel decyduje o wywołaniuget_feature, store_feature, list_features
ZasóbHost decyduje, jak dołączyć go do kontekstuschema://main
PromptUżytkownik wybiera go, jak zapisany szablon wiadomościNieużywany tutaj

Narzędzia pozwalają modelowi zażądać wykonania działania. Zasoby dostarczają kontekst za pośrednictwem hosta; sekcja 4.4 pokazuje, dlaczego udostępnienie zasobu nie gwarantuje, że odpowiedź go wykorzysta.

Przegląd architektury

Diagram pokazuje, które części działają na Twojej maszynie i jakie dane trafiają do modelu:

Architektura FeatureStoreLite: model Claude u dostawcy modelu otrzymuje Twoje pytanie, listę narzędzi i wyniki narzędzi, czyli wiersze z features.db; na Twojej maszynie Claude Desktop i jego klient MCP komunikują się przez stdin i stdout z procesem serwera FeatureStoreLite, który udostępnia trzy narzędzia i zasób schema://main oraz odczytuje i zapisuje tabelę SQLite z wierszami user_123 i product_abcArchitektura FeatureStoreLite: model Claude u dostawcy modelu otrzymuje Twoje pytanie, listę narzędzi i wyniki narzędzi, czyli wiersze z features.db; na Twojej maszynie Claude Desktop i jego klient MCP komunikują się przez stdin i stdout z procesem serwera FeatureStoreLite, który udostępnia trzy narzędzia i zasób schema://main oraz odczytuje i zapisuje tabelę SQLite z wierszami user_123 i product_abc

  1. Użytkownik zadaje pytanie w języku naturalnym.
  2. Claude Desktop jest hostem MCP. Wysyła rozmowę do modelu Claude, tworzy jednego klienta MCP dla tego serwera i zarządza połączeniem.
  3. Nasz serwer FastMCP udostępnia trzy narzędzia (get_feature, store_feature, list_features) i jeden zasób (schema://main).
  4. SQLite jest magazynem wektorów cech.

Host przekazuje listę narzędzi do Claude. Claude może zażądać wywołania narzędzia, ale komunikaty protokołu wysyła klient MCP przypisany do serwera. Serwer i SQLite działają lokalnie, jednak wyniki narzędzi trafiają do modelu jako część rozmowy. Udostępniaj tylko dane, które wkleiłbyś też do czatu.


1. Konfiguracja i instalacja

1.1. Zainstaluj uv

Jeśli nie masz jeszcze uv, zainstaluj go. Dalsza część tutoriala zakłada, że jest w Twoim PATH.

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

# Or via Homebrew
brew install uv

1.2. Zainicjalizuj projekt

Utwórz nowy katalog i zainicjalizuj projekt Pythona. uv init tworzy dla Ciebie 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"

Ten tutorial używa API MCP Python SDK v1. Oficjalna dokumentacja SDK v1 zaleca użytkownikom v1 przypięcie mcp>=1.28,<2, więc zostaw górną granicę <2, dopóki nie zmigrujesz kodu. Po tym poleceniu uv.lock zapisuje całe środowisko z ustalonymi wersjami zależności.

Kod używa FastMCP dostarczanego z oficjalnym SDK (mcp.server.fastmcp). Samodzielny pakiet fastmcp to osobny projekt z innym API, więc nie mieszaj jego dokumentacji z tym kodem.


2. Budowa serwera

Dwa pliki, podzielone według odpowiedzialności:

  1. database.py obsługuje operacje SQLite.
  2. featurestore_server.py definiuje serwer MCP.

2.1. Warstwa bazy danych (database.py)

Ten moduł zarządza połączeniem SQLite i kilkoma funkcjami pomocniczymi. Wypełniamy go dwoma przykładowymi wierszami, żeby serwer miał co zwrócić przy pierwszym zapytaniu.

Utwórz 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!")

Zainicjalizuj bazę danych:

uv run python database.py

2.2. Serwer MCP (featurestore_server.py)

FastMCP robi większość pracy. Udekoruj zwykłą funkcję Pythona, a zostanie zarejestrowana jako narzędzie lub zasób MCP. FastMCP buduje schemat wejściowy każdego narzędzia z adnotacji typów i używa docstringa jako opisu narzędzia. Model widzi jedno i drugie, więc pisz docstring dla modelu: powiedz, co narzędzie robi i jak wygląda każdy argument.

Utwórz 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()

Cztery szczegóły tego kodu decydują o tym, co model wysyła i co dostaje z powrotem:

  • Typowany vector. Schemat narzędzia wymaga niepustej tablicy liczb. W SDK 1.30 Pydantic przekształca też wartości logiczne i ciągi liczbowe w liczby zmiennoprzecinkowe: [true, false] staje się [1.0, 0.0], a ["1.5", "2"] staje się [1.5, 2.0]. Następnie przed uruchomieniem funkcji sprawdza, czy lista jest niepusta i każda wartość jest skończona. Jeśli model wyśle wektor jako ciąg JSON, na przykład "[1, NaN]", FastMCP najpierw parsuje ten ciąg. allow_inf_nan=False odrzuca potem NaN i nieskończoności, które parser JSON w Pythonie domyślnie akceptuje.
  • Typowane metadata. Schemat akceptuje obiekt JSON, null lub pominiętą wartość. allow_nan=False nie pozwala też zapisać NaN w metadanych.
  • Zgłaszane błędy. get_feature zgłasza ValueError dla brakującego klucza. Zgłoszony wyjątek trafia do klienta jako błąd narzędzia z isError: true; zwrócony ciąg wygląda jak udany wynik.
  • Nadpisywanie. store_feature używa INSERT OR REPLACE, więc zastępuje istniejący klucz. Docstring o tym mówi, dzięki czemu model wie to przed wywołaniem narzędzia.

3. Testowanie w MCP Inspector

Zanim podłączysz serwer do Claude, sprawdź go w MCP Inspector. To mały interfejs webowy, który łączy się z serwerem tak jak host. Możesz w nim przeglądać zasoby serwera, wywoływać każde narzędzie z własnymi argumentami i widzieć każde żądanie oraz odpowiedź.

SDK uruchamia Inspector przez npx, więc ten krok wymaga również Node.js i npm z npx w Twoim PATH. Inspector 2.10.1 wymaga Node.js 22.19.0 lub nowszego. Sprawdź zainstalowane polecenia, a następnie uruchom Inspector:

node --version
npx --version
uv run mcp dev featurestore_server.py

Polecenie uruchamia serwer pod MCP Inspector przez stdio. Użyj adresu URL przeglądarki, który wypisze polecenie. Port interfejsu Inspector zależy od implementacji, więc nie zakładaj stałego portu.

Wywołaj get_feature z key="user_123". Jeśli zwróci JSON z wiersza startowego, serwer działa. Potem wywołaj go z kluczem, który nie istnieje: wynikiem jest błąd narzędzia z isError: true. To sprawdza lokalną ścieżkę protokołu, rejestrację narzędzi, wyszukiwanie w bazie i ścieżkę błędu. Nie sprawdza, jak Claude wybiera narzędzia; robi to sekcja 4.

Inspector odczytuje schemat SQL z zasobu schema://main:

MCP Inspector pokazujący zasób get_schema i zwróconą instrukcję CREATE TABLE z SQLite


4. Połączenie z Claude Desktop

Gdy Inspector potwierdzi, że serwer działa, zarejestruj go w Claude Desktop.

4.1. Skonfiguruj Claude

Edytuj plik konfiguracji Claude Desktop:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%/Claude/claude_desktop_config.json

Dodaj serwer do obiektu mcpServers:

{
    "mcpServers": {
        "featurestore": {
            "command": "uv",
            "args": [
                "run",
                "--directory",
                "/ABSOLUTE/PATH/TO/mcp-featurestore",
                "--locked",
                "mcp",
                "run",
                "/ABSOLUTE/PATH/TO/mcp-featurestore/featurestore_server.py"
            ]
        }
    }
}

Ważne: użyj ścieżek bezwzględnych zarówno dla katalogu projektu, jak i featurestore_server.py. Claude Desktop uruchamia serwer jako osobny proces. uv run wykrywa projekt na podstawie katalogu roboczego dla polecenia takiego jak mcp, więc --directory wybiera pyproject.toml i uv.lock tego projektu; --locked kończy się wtedy błędem zamiast zmieniać ten plik blokady. Polecenie uv add z kroku 1.2 już zadeklarowało mcp[cli] w projekcie.

SDK ma też skrót: uv run mcp install featurestore_server.py zapisuje za Ciebie wpis Claude Desktop. W mcp 1.30 ten wpis uruchamia uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. Nie ma --directory, więc nie używa uv.lock tego projektu, a --with mcp[cli] nie ma ograniczenia <2. Użyj go do szybkiej próby; powyższa ręczna konfiguracja jest powtarzalna.

4.2. Jak działa interakcja

Oto, co działa od początku do końca, gdy Claude potrzebuje odczytu cechy. Kroki 1 i 2 wykonują się raz, gdy Claude Desktop uruchamia serwer. Kroki od 3 do 6 wykonują się przy każdym pytaniu wymagającym danych cech.

Diagram sekwencji jednego wyszukania user_123 między modelem Claude, Claude Desktop, serwerem FeatureStoreLite i features.db: tools/list i opisy narzędzi przy starcie, a potem dla każdego pytania żądanie get_feature od modelu, komunikat tools/call, SELECT w SQLite, wynik JSON i końcowa odpowiedź, ponumerowane od 1 do 6 jak na poniższej liścieDiagram sekwencji jednego wyszukania user_123 między modelem Claude, Claude Desktop, serwerem FeatureStoreLite i features.db: tools/list i opisy narzędzi przy starcie, a potem dla każdego pytania żądanie get_feature od modelu, komunikat tools/call, SELECT w SQLite, wynik JSON i końcowa odpowiedź, ponumerowane od 1 do 6 jak na poniższej liście

  1. Claude Desktop uruchamia dedykowanego klienta MCP dla serwera i wykrywa jego narzędzia.
  2. Host udostępnia opisy narzędzi modelowi Claude.
  3. Claude może zażądać wywołania narzędzia, gdy pytanie wymaga danych z feature store.
  4. Klient MCP wysyła to żądanie do serwera.
  5. Serwer uruchamia funkcję Pythona i zwraca wynik do hosta.
  6. Host może przekazać wynik modelowi Claude do przygotowania końcowej odpowiedzi.

Ten podział na hosta, klienta i serwer wynika ze specyfikacji architektury MCP.

4.3. Przykładowe zapytania

Uruchom ponownie Claude Desktop i wypróbuj kilka wiadomości typu prompt:

  1. “Wyświetl wszystkie dostępne cechy.” Deterministyczny wynik zawiera dwa klucze startowe: user_123 i product_abc.

  2. “Pobierz wektor cech dla user_123.” Odpowiedź zawiera wektor i metadane premium z database.py.

  3. “Zapisz nową cechę dla new_item z wektorem [0.5, 0.5] i metadanymi {"type": "test"}. Następnie pobierz new_item i pokaż zapisany wektor oraz metadane.” Po konwersji zwalidowany wektor musi być niepusty i zawierać wyłącznie skończone liczby zmiennoprzecinkowe. Metadane mogą być obiektem JSON, null lub zostać pominięte. Kolejny odczyt sprawdza pełny cykl zapisu i odczytu.

  4. “Pobierz wektor cech dla user_999.” get_feature zgłasza błąd dla brakującego klucza. Claude powinien powiedzieć, że klucz nie istnieje, i może wywołać list_features, aby pokazać poprawne klucze.

4.4. Jak czytać odpowiedzi Claude

Serwer zwraca dane narzędzi i zasobów. Claude wybiera narzędzie i pisze wyjaśnienie wokół wyniku.

Ten zrzut ekranu przedstawia uruchomienie z czerwca 2025 z wcześniejszą wersją przykładu. Poprosiłem Claude o pokazanie schematu bazy danych:

Claude Desktop podaje błędny schemat pojęciowy FeatureStoreLite podczas uruchomienia z czerwca 2025

Claude nazwał system magazynem NoSQL lub dokumentów i podał schemat pojęciowy, ale database.py tworzy relacyjną tabelę SQLite. Pokazana odpowiedź nie jest instrukcją CREATE TABLE zwracaną przez schema://main. Zrzut ekranu nie pokazuje, czy Claude Desktop odczytał ten zasób ani jaki kontekst otrzymał Claude, więc nie dowodzi przyczyny błędu.

Zasobami steruje aplikacja: host decyduje, jak dołączyć je do kontekstu modelu, przez wybór użytkownika lub automatycznie. MCP nie wymaga, aby host przekazywał modelowi każdy zasób. Jeśli model ma żądać schematu przez wywołanie narzędzia, udostępnij go także jako narzędzie.

Wiarygodnie brzmiące wyjaśnienie modelu nie zawsze zgadza się z tym, co zwróciło narzędzie. Gdy szczegół ma znaczenie, sprawdź wynik narzędzia lub kod źródłowy.

4.5. Inne hosty MCP

Serwer nie zależy od Claude Desktop. Każdy host, który uruchamia serwery stdio na tej samej maszynie, może użyć tego samego polecenia uv. Claude Desktop używa JSON z sekcji 4.1; pozostałe dwa to:

  • 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. Wszystko po -- to polecenie uruchamiające serwer.
  • Cursor czyta ten sam format JSON mcpServers z ~/.cursor/mcp.json lub .cursor/mcp.json w projekcie.

Host na innej maszynie nie może uruchomić procesu stdio na Twojej. Dla serwera współdzielonego przez kilka osób uruchom go przez Streamable HTTP z mcp.run(transport="streamable-http") i dodaj przed nim uwierzytelnianie.


5. Rozwiązywanie problemów

Ustal, która część zawiodła, zanim zmienisz kod: błędy startu są w logach, błędy narzędzi powtarzają się w Inspector bez modelu, a błędna odpowiedź wymaga porównania wyniku narzędzia z tekstem Claude.

  • “Server connection failed”:

    • Sprawdź logi w ~/Library/Logs/Claude/ na macOS lub %APPDATA%\Claude\logs na Windows. mcp.log zapisuje zdarzenia połączenia. mcp-server-featurestore.log w tym samym folderze zawiera własny stderr serwera, więc tam widać traceback z uruchamiania (logi Claude Desktop).
    • Upewnij się, że konfiguracja używa ścieżki bezwzględnej, a nie względnej.
    • Upewnij się, że uv jest w PATH Claude Desktop. Jeśli nie jest, wskaż pełną ścieżkę do pliku binarnego (which uv pokaże, gdzie się znajduje).
    • Na stdio stdout przenosi komunikaty protokołu. Loguj do stderr; print() w serwerze psuje ten strumień i może przerwać połączenie.
  • “Tool execution error”:

    • Odtwórz go w Inspector poleceniem uv run mcp dev featurestore_server.py. Inspector pokazuje komunikat błędu i argumenty, które go wywołały.
    • Sprawdź, czy features.db powstaje obok database.py. Ścieżka pochodzi z get_db_path(), która wyznacza ją względem skryptu, więc zmiana katalogu roboczego nie powinna przesunąć pliku.
  • Wywołanie się udaje, ale odpowiedź jest błędna:

    • Otwórz wynik narzędzia na czacie i porównaj go z tekstem Claude. Sekcja 4.4 pokazuje odpowiedź modelu, która błędnie opisała bazę danych.

6. Zanim agent użyje tego naprawdę

  • store_feature nadpisuje każdy klucz, także wiersze startowe. Zostaw w hoście włączone zatwierdzanie każdego wywołania narzędzi zapisu albo przenieś je na osobny serwer, który włączasz tylko wtedy, gdy go potrzebujesz.
  • Wyniki narzędzi trafiają do dostawcy modelu jako część rozmowy. Nie zwracaj danych, które muszą zostać na maszynie.
  • Serwer współdzielony przez kilka osób potrzebuje Streamable HTTP i uwierzytelniania zamiast stdio (sekcja 4.5).

7. Podsumowanie

To wszystko: serwer FastMCP, magazyn SQLite i konfiguracja Claude Desktop wskazująca polecenie uv run. Ten sam układ sprawdza się przy większości rzeczy, które można opakować w funkcję Pythona. Zamień wywołania SQLite na prawdziwy feature store, wewnętrzne API lub rejestr modeli, a serwer pozostanie mały.

Źródła