Туториал по MCP-серверу: сборка с Python, uv и FastMCP

Автоматический перевод Эта статья была автоматически переведена с оригинальной английской версии.

Python-разработчики могут превратить локальный workflow фичестора в FastMCP-сервер с зафиксированными версиями для Claude Desktop: использовать uv для создания окружения, предоставить инструменты и ресурс, а затем проверить сервер локально до подключения к десктопному приложению.

Коротко: небольшого FastMCP-сервера достаточно, чтобы предоставить агенту полезный локальный инструмент. Используйте uv для воспроизводимой настройки, держите границу сервера узкой, валидируйте размерность вектора на границе инструмента и протестируйте MCP-инструмент до подключения модели.


Что такое MCP-сервер?

Model Context Protocol (MCP) — это открытый протокол для подключения AI-приложений к внешним системам. Он заменяет отдельные кастомные интеграции для каждого инструмента единым набором соглашений.

В этом туториале мы создадим MCP-сервер FeatureStoreLite. Он располагается между LLM и фичестором — базой данных с предварительно вычисленными ML-фичами. Сервер предоставляет инструменты для чтения и записи векторов фичей, индексируемых по пользователю, продукту или документу.

Зачем это нужно?

Отладка пайплайна фичей обычно сводится к тому, чтобы открыть SQL или написать одноразовый скрипт для проверки значения. Когда сервер запущен, вместо этого можно спросить Claude: «Какой вектор фичей у user_123?» или «Покажи метаданные product_abc».

Зачем использовать uv?

Мы будем использовать uv для установки пакетов, разрешения зависимостей и управления виртуальным окружением. Конфигурация Claude Desktop будет запускать этот проект из его собственной директории с помощью --locked, поэтому будет использовать зависимость mcp[cli], объявленную здесь, и версии, записанные в uv.lock.

Обзор архитектуры

Четыре компонента и их взаимодействие:

Архитектура FeatureStoreLite: от запроса пользователя через MCP-хост Claude Desktop и клиент для конкретного сервера к FastMCP-серверу и SQLite-хранилищуАрхитектура FeatureStoreLite: от запроса пользователя через MCP-хост Claude Desktop и клиент для конкретного сервера к FastMCP-серверу и SQLite-хранилищу

  1. Пользователь задаёт вопрос на естественном языке.
  2. Claude Desktop выступает MCP-хостом. Он создаёт один MCP-клиент для этого сервера и управляет подключением.
  3. Наш сервер FastMCP предоставляет get_feature и store_feature в качестве MCP-инструментов.
  4. SQLite — бэкенд для хранения векторов фичей.

Хост может сделать обнаруженные инструменты доступными Claude. Claude может запросить tool call, но сообщения протокола отправляет MCP-клиент для конкретного сервера.


1. Настройка и установка

1.1. Установка uv

Если у вас ещё нет uv, установите его. Далее предполагается, что он уже находится в вашем PATH.

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

# Or via Homebrew
brew install uv

1.2. Инициализация проекта

Создайте новую директорию и инициализируйте Python-проект. uv init создаст для вас 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"

В этом туториале используется API MCP Python SDK v1. Официальная документация SDK v1 рекомендует пользователям v1 зафиксировать mcp>=1.28,<2, поэтому сохраняйте верхнюю границу <2, пока не перенесёте код на новую версию. uv.lock записывает полное разрешённое окружение после выполнения этой команды.

Inline code в этой статье является каноничным. Репозиторий-компаньон, ссылка на который приведена в разделе references, содержит историческую версию. В нём нет текущей фиксации зависимости и текущей валидации векторов.


2. Создание сервера

Два файла, разделённые по ответственности:

  1. database.py отвечает за операции с SQLite.
  2. featurestore_server.py определяет MCP-сервер.

2.1. Слой базы данных (database.py)

Этот модуль владеет подключением к SQLite и несколькими вспомогательными функциями. Мы добавим в базу две примерные строки, чтобы сервер мог что-то вернуть при первом запросе.

Создайте 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!")

Инициализируйте базу данных:

uv run python database.py

2.2. MCP-сервер (featurestore_server.py)

FastMCP делает почти всю работу. Декорируйте обычную Python-функцию — и она будет зарегистрирована как MCP-инструмент или ресурс. Докстринг становится описанием, которое видит LLM. Пишите его с учётом этого читателя.

Создайте 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()

JSON-парсер Python по умолчанию принимает NaN и бесконечности, хотя они не входят в грамматику чисел JSON. Колбэк parse_constant отклоняет такие записи, а math.isfinite проверяет каждый элемент вектора до того, как SQLite получит строку. Для метаданных используется единый контракт: они должны быть строкой, содержащей JSON-объект, а сервер разбирает и нормализует их перед вставкой. См. заметки о совместимости JSON в Python.


3. Тестирование с MCP Inspector

До подключения к Claude проверьте сервер с помощью MCP Inspector. Это небольшой веб-интерфейс для прямого вызова инструментов и чтения ресурсов.

uv run mcp dev featurestore_server.py

Команда запускает сервер под управлением MCP Inspector через stdio. Используйте URL браузера, который напечатает команда. Порт веб-интерфейса Inspector зависит от реализации, поэтому не рассчитывайте на фиксированный порт.

Именно так выглядел мой исходный запуск в июне 2025 года с MCP Inspector v0.14.0. На скриншоте показана конфигурация stdio Inspector и успешное подключение. Незафиксированный аргумент --with mcp относится к исторической версии; сейчас при воспроизведении туториала используйте зафиксированную v1-команду из этой статьи.

MCP Inspector v0.14.0, подключённый к FeatureStoreLite через stdio с использованием uv

Вызовите get_feature с key="user_123". Если он вернёт JSON для начальной строки, сервер работает.

Это проверяет локальный протокольный путь, регистрацию инструмента, поиск в базе данных и JSON-вывод. Проверка не оценивает качество vector search. Для production retrieval добавьте тесты с репрезентативными запросами, ожидаемыми результатами поиска ближайших соседей, порогами расстояния, фильтрами метаданных и evaluation-набором, соответствующим вашей нагрузке.

В том же запуске были обнаружены все три инструмента и ресурс schema://main. Эти два скриншота полезны для проверки discovery: Inspector нашёл функции и прочитал SQL-схему, которую вернул сервер.

MCP Inspector с инструментами store_feature, get_feature, list_features и успешным вызовом list_features

MCP Inspector с ресурсом get_schema и возвращённой инструкцией SQLite CREATE TABLE

На скриншоте инструмента показаны четыре строки: user_123, product_abc, doc_guide_001 и recommendation_engine. Это была моя более богатая локальная база данных от 10 июня 2025 года. Реализация list_features возвращала объекты с полями key и created_at. Текущая функция возвращает только JSON-массив строк-ключей и добавляет в базу только первые две строки. Не сравнивайте исторический вывод построчно с текущим fixture.


4. Подключение к Claude Desktop

После того как Inspector подтвердит работоспособность сервера, зарегистрируйте его в Claude Desktop.

4.1. Настройка Claude

Откройте файл конфигурации Claude Desktop:

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

Добавьте сервер в объект mcpServers:

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

Важно: используйте абсолютные пути и для директории проекта, и для featurestore_server.py. Claude Desktop запускает сервер как отдельный процесс. uv run ищет проект в рабочей директории для такой команды, как mcp, поэтому --directory выбирает pyproject.toml и uv.lock этого проекта; затем --locked завершается с ошибкой вместо изменения этого lockfile. Команда uv add из шага 1.2 уже объявила mcp[cli] в проекте.

4.2. Как работает взаимодействие

Вот что происходит от начала до конца, когда Claude требуется найти фичу:

Шесть шагов поиска в FeatureStoreLite: от обнаружения MCP-инструмента через tools/call и обращения к SQLite до финального ответа ClaudeШесть шагов поиска в FeatureStoreLite: от обнаружения MCP-инструмента через tools/call и обращения к SQLite до финального ответа Claude

  1. Claude Desktop запускает отдельный MCP-клиент для сервера и обнаруживает его инструменты.
  2. Хост делает описания инструментов доступными Claude.
  3. Claude может запросить tool call, если для ответа нужны данные фичестора.
  4. MCP-клиент отправляет этот запрос серверу.
  5. Сервер выполняет Python-функцию и возвращает результат хосту.
  6. Хост может передать результат Claude для формирования финального ответа.

Такое разделение хоста, клиента и сервера соответствует спецификации архитектуры MCP. Ресурсы также предоставляют контекст хост-приложению. MCP не требует, чтобы хост передавал модели каждый ресурс.

4.3. Примеры запросов

Перезапустите Claude Desktop и попробуйте несколько промптов:

  1. «Покажи все доступные фичи». Детерминированный результат содержит два добавленных при инициализации ключа: user_123 и product_abc.

  2. «Получи вектор фичей для user_123». Ответ содержит вектор и метаданные premium из database.py.

  3. «Сохрани новую фичу для new_item с JSON-строкой вектора [0.5, 0.5] и JSON-строкой метаданных {"type": "test"}. Затем получи new_item и покажи сохранённые вектор и метаданные». Оба аргумента должны содержать валидный JSON. Запись выполняется только после того, как сервер провалидирует вектор и метаданные. Последующее чтение проверяет полный round trip записи и чтения.

4.4. Что показал мой запуск Claude Desktop

Следующие скриншоты относятся к тому же запуску в июне 2025 года — до того, как я сократил fixture статьи до двух строк. На них показано, что Claude Desktop отображал после подключения к моему серверу. Это наблюдения конкретного запуска, а не гарантированный вывод MCP. Сервер возвращает данные инструментов и ресурсов; Claude выбирает инструмент и формирует пояснение вокруг результата.

Сначала я попросил Claude показать схему базы данных:

Claude Desktop отвечает на запрос показать схему базы данных FeatureStoreLite

Claude ошибся в важной детали. Он назвал систему NoSQL- или документным хранилищем, хотя database.py использует SQLite и создаёт реляционную таблицу. Колонка metadata содержит JSON-текст, но это не меняет движок базы данных. Скриншот хорошо напоминает: правдоподобное объяснение модели — не контракт инструмента. Если различие важно, проверьте возвращённую инструкцию CREATE TABLE или исходный код.

Я также попросил Claude перечислить доступные фичи:

Claude Desktop вызывает list_features и отображает четыре строки из базы данных автора за июнь 2025 года

Эти четыре ключа совпадают с ключами из более богатой локальной базы данных, показанной в Inspector. Такие подписи, как «эмбеддинг пользователя» и «эмбеддинг модели», являются интерпретацией Claude названий и метаданных. Сам list_features гарантирует только те строки, которые возвращает его реализация.

Наконец, я запросил product_abc:

Claude Desktop вызывает get_feature для product_abc и отображает его вектор и метаданные

Здесь вектор и метаданные взяты из результата инструмента. Текст о сходстве, рекомендациях и кластеризации написал Claude. Это возможные варианты использования эмбеддинга, но сервер из этого туториала только сохраняет и извлекает векторы. Поиск ближайших соседей в нём не реализован.


5. Устранение неполадок

Несколько типичных проблем, о которых стоит знать:

  • «Не удалось подключиться к серверу»:

    • Проверьте логи в ~/Library/Logs/Claude/mcp.log на macOS.
    • Убедитесь, что в конфигурации указан абсолютный, а не относительный путь.
    • Убедитесь, что uv находится в PATH Claude Desktop. Если это не так, укажите полный путь к бинарному файлу (команда which uv покажет, где он находится).
  • «Ошибка выполнения инструмента»:

    • Воспроизведите её в Inspector с помощью uv run mcp dev featurestore_server.py. Inspector показывает исходную ошибку, которую Claude Desktop обычно скрывает.
    • Проверьте, что features.db создаётся рядом с database.py. Путь берётся из get_db_path(), который разрешает его относительно скрипта, поэтому изменение рабочей директории не должно перемещать файл.

6. Заключение

Вот и всё: FastMCP-сервер, SQLite в качестве backing store и конфигурация Claude Desktop, указывающая на команду uv run. Такая же схема подходит для большинства вещей, которые можно обернуть в Python-функцию. Замените вызовы SQLite на настоящий фичестор, внутренний API или реестр моделей — и сервер останется небольшим.

Ключевые выводы

  1. MCP — это граница инструментов, а не причина предоставлять доступ к каждой внутренней функции.
  2. Делайте инструменты сервера небольшими, типизированными и удобными для тестирования без LLM.
  3. Используйте uv, чтобы окружение туториала можно было пересоздать с нуля.
  4. Относитесь к MCP-серверу как к production-коду, как только агент получает возможность его вызывать.

References