Tutorial tworzenia serwera MCP: Python, uv i FastMCP

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

Programiści Pythona mogą przekształcić lokalny workflow feature store w przypięty do konkretnych wersji serwer FastMCP dla Claude Desktop, używając uv do utworzenia środowiska, udostępnienia narzędzi i zasobu oraz lokalnego zweryfikowania serwera przed nawiązaniem połączenia z aplikacją desktopową.


Czym jest serwer MCP?

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

W tym tutorialu zbudujemy serwer MCP FeatureStoreLite. Znajduje się on między LLM a feature store, czyli bazą danych zawierającą wstępnie obliczone cechy ML. Serwer udostępnia narzędzia do odczytywania i zapisywania wektorów cech indeksowanych przez użytkownika, produkt lub dokument.

Po co go budować?

Debugowanie pipeline’u cech zwykle oznacza przejście do SQL albo napisanie tymczasowego skryptu w celu sprawdzenia wartości. Gdy serwer działa, możesz zamiast tego zapytać Claude: „Jaki jest wektor cech dla user_123?” albo „Pokaż mi metadane produktu product_abc”.

Dlaczego używać uv?

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

Przegląd architektury

Cztery elementy i sposób ich współdziałania:

Architektura FeatureStoreLite: od żądania użytkownika przez hosta MCP w Claude Desktop i klienta dla serwera do serwera FastMCP oraz magazynu SQLiteArchitektura FeatureStoreLite: od żądania użytkownika przez hosta MCP w Claude Desktop i klienta dla serwera do serwera FastMCP oraz magazynu SQLite

  1. Użytkownik zadaje pytanie w języku naturalnym.
  2. Claude Desktop jest hostem MCP. Tworzy jednego klienta MCP dla tego serwera i zarządza połączeniem.
  3. Nasz serwer FastMCP udostępnia get_feature i store_feature jako narzędzia MCP.
  4. SQLite jest magazynem przechowującym wektory cech.

Host może udostępnić Claude wykryte narzędzia. Claude może zażądać wywołania narzędzia, ale klient MCP dla danego serwera wysyła komunikaty protokołu.


1. Konfiguracja i instalacja

1.1. Zainstaluj uv

Jeśli nie masz jeszcze uv, zainstaluj je. W dalszej części tutorialu zakładamy, że znajduje się ono na 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 w Pythonie. uv init utworzy 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 korzysta z API MCP Python SDK v1. Oficjalna dokumentacja SDK v1 zaleca użytkownikom v1 przypięcie mcp>=1.28,<2, dlatego zachowaj górne ograniczenie <2 do czasu migracji kodu. uv.lock zapisuje kompletne rozwiązane środowisko po wykonaniu tego polecenia.

Kod inline w tym artykule jest kanoniczny. Powiązane repozytorium wskazane w odnośnikach zawiera wersję historyczną. Nie odtwarza bieżącego przypięcia zależności ani aktualnej walidacji wektora.


2. Budowa serwera

Dwa pliki, rozdzielone 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 z SQLite i kilkoma funkcjami pomocniczymi. Inicjalizujemy go dwoma przykładowymi wierszami, aby 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 wykonuje większość pracy. Ozdobienie zwykłej funkcji Pythona powoduje jej zarejestrowanie jako narzędzia lub zasobu MCP. Docstring staje się opisem widocznym dla LLM. Napisz go z myślą o tym odbiorcy.

Utwórz featurestore_server.py:

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

def reject_non_finite_json_number(value: str) -> None:
    """Reject NaN and infinities, which are not valid JSON numbers."""
    raise ValueError(f"Non-finite JSON number: {value}")

@mcp.resource("schema://main")
def get_schema() -> str:
    """
    Resource: Provide the database schema.
    Resources provide context to the host application.
    The host decides whether to pass that context to the model.
    """
    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.
    """
    try:
        parsed_vector = json.loads(
            vector, parse_constant=reject_non_finite_json_number
        )
    except (json.JSONDecodeError, ValueError):
        return "Error: Vector must be valid JSON with finite numbers"

    try:
        valid_vector = (
            isinstance(parsed_vector, list)
            and bool(parsed_vector)
            and all(
                isinstance(value, (int, float))
                and not isinstance(value, bool)
                and math.isfinite(value)
                for value in parsed_vector
            )
        )
    except OverflowError:
        valid_vector = False

    if not valid_vector:
        return "Error: Vector must be a non-empty JSON array of finite numbers (e.g., '[0.1, 0.2]')"

    metadata_json = None
    if metadata is not None:
        try:
            parsed_metadata = json.loads(
                metadata, parse_constant=reject_non_finite_json_number
            )
        except (json.JSONDecodeError, ValueError):
            return "Error: Metadata must be valid JSON with finite numbers"
        if not isinstance(parsed_metadata, dict):
            return "Error: Metadata must be a JSON object (e.g., '{\"type\": \"test\"}')"
        metadata_json = json.dumps(parsed_metadata, allow_nan=False)

    conn = get_db_connection()
    try:
        conn.execute(
            "INSERT OR REPLACE INTO features (key, vector, metadata) VALUES (?, ?, ?)",
            (key, json.dumps(parsed_vector, allow_nan=False), metadata_json),
        )
        conn.commit()
        return f"Successfully stored feature '{key}'"
    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()

Parser JSON w Pythonie domyślnie akceptuje NaN oraz nieskończoności, mimo że znajdują się one poza gramatyką liczb JSON. Callback parse_constant odrzuca takie zapisy, a math.isfinite sprawdza każdy element wektora, zanim wiersz trafi do SQLite. Metadane podlegają jednemu kontraktowi: muszą być tekstem zawierającym obiekt JSON, a serwer parsuje je i normalizuje przed wstawieniem. Zobacz uwagi Pythona dotyczące interoperacyjności JSON.


3. Testowanie za pomocą MCP Inspector

Przed podłączeniem serwera do Claude sprawdź go wstępnie za pomocą MCP Inspector. To niewielki interfejs webowy umożliwiający bezpośrednie wywoływanie narzędzi i odczytywanie zasobów.

uv run mcp dev featurestore_server.py

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

Tak wyglądało moje pierwotne uruchomienie z czerwca 2025 roku w MCP Inspector v0.14.0. Zrzut ekranu przedstawia konfigurację stdio Inspectora i pomyślne połączenie. Jego nieprzypięty argument --with mcp ma charakter historyczny; przy odtwarzaniu tutorialu użyj teraz przypiętego polecenia v1 z tego artykułu.

MCP Inspector v0.14.0 połączony z FeatureStoreLite przez stdio za pomocą uv

Wywołaj get_feature z key="user_123". Jeśli zwróci JSON dla wiersza inicjalizacyjnego, serwer działa.

Sprawdza to lokalną ścieżkę protokołu, rejestrację narzędzia, wyszukiwanie w bazie danych i dane wyjściowe JSON. Nie sprawdza jakości wyszukiwania wektorowego. W środowisku produkcyjnym dodaj testy z reprezentatywnymi zapytaniami, oczekiwanymi wynikami wyszukiwania najbliższych sąsiadów, progami odległości, filtrami metadanych oraz zbiorem ewaluacyjnym odpowiadającym Twojemu obciążeniu.

To samo uruchomienie ujawniło wszystkie trzy narzędzia i zasób schema://main. Te dwa zrzuty ekranu są przydatne do sprawdzenia procesu wykrywania: Inspector znalazł funkcje i odczytał schemat SQL zwrócony przez serwer.

MCP Inspector pokazujący store_feature, get_feature, list_features oraz pomyślne wywołanie list_features

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

Zrzut narzędzia zawiera cztery wiersze: user_123, product_abc, doc_guide_001 oraz recommendation_engine. Była to moja bogatsza lokalna baza danych z 10 czerwca 2025 roku. Jej implementacja list_features zwracała obiekty z polami key i created_at. Bieżąca funkcja zwraca tylko tablicę JSON z kluczami i inicjalizuje wyłącznie dwa pierwsze wiersze. Nie porównuj historycznego wyniku wiersz po wierszu z bieżącym fixture.


4. Połączenie z Claude Desktop

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

4.1. Konfiguracja Claude

Edytuj plik konfiguracyjny 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 absolutnych zarówno dla katalogu projektu, jak i featurestore_server.py. Claude Desktop uruchamia serwer jako osobny proces. uv run wyszukuje projekt na podstawie bieżącego 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 zmienić ten plik lock. Polecenie uv add z kroku 1.2 zadeklarowało już mcp[cli] w projekcie.

4.2. Jak działa interakcja

Oto przebieg całej ścieżki, gdy Claude potrzebuje wyszukać cechę:

Sześcioetapowe wyszukiwanie w FeatureStoreLite: od wykrycia narzędzia MCP przez tools/call i dostęp do SQLite po końcową odpowiedź ClaudeSześcioetapowe wyszukiwanie w FeatureStoreLite: od wykrycia narzędzia MCP przez tools/call i dostęp do SQLite po końcową odpowiedź Claude

  1. Claude Desktop uruchamia dedykowanego klienta MCP dla serwera i wykrywa jego narzędzia.
  2. Host udostępnia Claude opisy narzędzi.
  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 Claude, aby ten wygenerował końcową odpowiedź.

Ten podział na hosta, klienta i serwer jest zgodny z specyfikacją architektury MCP. Zasoby dostarczają również kontekstu aplikacji hosta. MCP nie wymaga, aby host przekazywał modelowi każdy zasób.

4.3. Przykładowe zapytania

Uruchom ponownie Claude Desktop i wypróbuj kilka promptów:

  1. „Wyświetl wszystkie dostępne cechy”. Deterministyczny wynik zawiera dwa zainicjalizowane klucze: user_123 oraz product_abc.

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

  3. „Zapisz nową cechę dla new_item z ciągiem JSON wektora [0.5, 0.5] i ciągiem JSON metadanych {"type": "test"}. Następnie pobierz new_item i pokaż zapisany wektor oraz metadane”. Oba argumenty muszą zawierać poprawny JSON. Zapis powiedzie się dopiero po zweryfikowaniu przez serwer wektora i metadanych. Następujący po nim odczyt sprawdza pełny cykl zapisu i odczytu.

4.4. Co pokazało moje uruchomienie Claude Desktop

Poniższe zrzuty ekranu pochodzą z tego samego uruchomienia z czerwca 2025 roku, zanim ograniczyłem fixture artykułu do dwóch wierszy. Pokazują, co Claude Desktop wyświetlił po połączeniu z moim serwerem. Są obserwacjami z tamtego uruchomienia, a nie gwarantowanymi danymi wyjściowymi MCP. Serwer zwraca dane narzędzi i zasobów; Claude wybiera narzędzie i tworzy objaśnienie wokół wyniku.

Najpierw poprosiłem Claude o wyświetlenie schematu bazy danych:

Claude Desktop odpowiadający na prośbę o wyświetlenie schematu bazy danych FeatureStoreLite

Claude pomylił istotny szczegół. Określił system jako magazyn NoSQL lub dokumentowy, mimo że database.py używa SQLite i tworzy tabelę relacyjną. Kolumna metadata zawiera tekst JSON, ale nie zmienia to silnika bazy danych. Zrzut ekranu dobrze przypomina, że wiarygodne wyjaśnienie modelu nie jest kontraktem narzędzia. Gdy to rozróżnienie ma znaczenie, sprawdź zwróconą instrukcję CREATE TABLE albo kod źródłowy.

Poprosiłem również Claude o wyświetlenie dostępnych cech:

Claude Desktop wywołujący list_features i wyświetlający cztery wiersze z bazy danych autora z czerwca 2025 roku

Cztery klucze odpowiadają bogatszej lokalnej bazie danych pokazanej w Inspector. Etykiety takie jak „user embedding” i „model embedding” są interpretacją nazw i metadanych dokonaną przez Claude. Samo list_features gwarantuje wyłącznie wiersze zwracane przez jego implementację.

Na koniec pobrałem product_abc:

Claude Desktop wywołujący get_feature dla product_abc i wyświetlający jego wektor oraz metadane

W tym przypadku wektor i metadane pochodziły z wyniku narzędzia. Tekst o podobieństwie, rekomendacjach i klastrowaniu został wygenerowany przez Claude. Są to możliwe zastosowania embeddingu, ale serwer z tego tutorialu jedynie przechowuje i pobiera wektory. Nie implementuje wyszukiwania najbliższych sąsiadów.


5. Rozwiązywanie problemów

Warto znać kilka typowych trybów awarii:

  • „Połączenie z serwerem nie powiodło się”:

    • Sprawdź logi w ~/Library/Logs/Claude/mcp.log na macOS.
    • Upewnij się, że konfiguracja używa ścieżki absolutnej, a nie względnej.
    • Upewnij się, że uv znajduje się w PATH Claude Desktop. Jeśli nie, wskaż pełną ścieżkę do pliku binarnego (which uv pokaże, gdzie się znajduje).
  • „Błąd wykonania narzędzia”:

    • Odtwórz problem w Inspector za pomocą uv run mcp dev featurestore_server.py. Inspector pokazuje surowy błąd, który Claude Desktop zwykle ukrywa.
    • Sprawdź, czy features.db jest tworzony obok database.py. Ścieżka pochodzi z get_db_path(), które rozwiązuje ją względem skryptu, więc zmieniający się katalog roboczy nie powinien przenosić pliku.

6. Podsumowanie

To wszystko: serwer FastMCP, magazyn SQLite oraz konfiguracja Claude Desktop wskazująca na polecenie uv run. Ten sam schemat działa dla większości rzeczy, które można opakować w funkcję Pythona. Zastąp wywołania SQLite prawdziwym feature store, wewnętrznym API albo rejestrem modeli, a serwer pozostanie niewielki.

Najważniejsze wnioski

  1. MCP wyznacza granicę narzędzia, a nie uzasadnienie dla udostępniania każdej wewnętrznej funkcji.
  2. Utrzymuj narzędzia serwera małe, typowane i łatwe do testowania bez LLM.
  3. Używaj uv, aby środowisko tutorialu można było odtworzyć od zera.
  4. Traktuj serwer MCP jak kod produkcyjny, gdy tylko agent może go wywoływać.

Odnośniki