MCP-сервер на Python, uv и FastMCP
Автоматический перевод
Эта статья была автоматически переведена с оригинальной английской версии.
В этом туториале по MCP-серверу мы соберём небольшой сервер фичестора на FastMCP, запустим его через uv и подключим к Claude Desktop. Он рассчитан на Python-разработчиков, которые хотят открыть MCP-хосту один локальный рабочий процесс с базой данных. К концу у вас будет окружение с закреплённой версией SDK v1, сервер с тремя инструментами и одним ресурсом и локальная проверка, которую стоит выполнить до подключения Claude Desktop.
Что такое 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.
Инструменты, ресурсы и промпты
MCP-сервер может предлагать три вида сущностей. Спецификация MCP разделяет их по тому, кто решает их использовать:
| Примитив | Кто запускает | В этом сервере |
|---|---|---|
| Инструмент | Модель решает вызвать его | get_feature, store_feature, list_features |
| Ресурс | Хост решает, как включить его в контекст | schema://main |
| Промпт | Пользователь выбирает его, как сохранённый шаблон текста | Здесь не используется |
Инструменты позволяют модели запросить действие. Ресурсы предоставляют контекст через хост; раздел 4.4 показывает, почему доступность ресурса не гарантирует, что ответ его использует.
Обзор архитектуры
Диаграмма показывает, какие части работают на вашей машине и какие данные уходят к модели:
- Пользователь задаёт вопрос на естественном языке.
- Claude Desktop — это MCP-хост. Он отправляет диалог модели Claude, создаёт один MCP-клиент для этого сервера и управляет соединением.
- Наш сервер
FastMCPпредоставляет три инструмента (get_feature,store_feature,list_features) и один ресурс (schema://main). - SQLite служит хранилищем для векторов фичей.
Хост передаёт список инструментов Claude. Claude может запросить tool call, но протокольные сообщения отправляет MCP-клиент для конкретного сервера. Сервер и SQLite работают локально, однако результаты инструментов попадают к модели как часть диалога. Открывайте только те данные, которые вы бы вставили в чат.
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 фиксирует всё окружение с выбранными версиями зависимостей.
Код использует FastMCP, который входит в официальный SDK (mcp.server.fastmcp). Отдельный пакет fastmcp — это другой проект с другим API, поэтому не смешивайте его документацию с этим кодом.
2. Создание сервера
Два файла, разделённые по задачам:
database.pyотвечает за операции с SQLite.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-инструмент или ресурс. FastMCP строит входную схему каждого инструмента из аннотаций типов, а docstring использует как описание инструмента. Модель видит и то и другое, поэтому пишите docstring для модели: опишите, что делает инструмент и как выглядит каждый аргумент.
Создайте 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()
Четыре детали в этом коде определяют, что модель отправляет и что получает обратно:
- Типизированный
vector. Схема инструмента требует непустой массив чисел. В SDK 1.30 Pydantic также преобразует логические значения и числовые строки в числа с плавающей точкой:[true, false]превращается в[1.0, 0.0], а["1.5", "2"]— в[1.5, 2.0]. Затем до запуска функции он проверяет, что список непустой, а все значения конечны. Если модель присылает вектор как JSON-строку вроде"[1, NaN]", FastMCP сначала разбирает строку. Затемallow_inf_nan=FalseотклоняетNaNи бесконечности, которые JSON-парсер Python принимает по умолчанию. - Типизированные
metadata. Схема принимает JSON-объект,nullили отсутствие значения.allow_nan=Falseтакже запрещает сохранятьNaNв метаданных. - Выбрасываемые ошибки.
get_featureвыбрасываетValueError, если ключа нет. Выброшенное исключение доходит до клиента как ошибка инструмента сisError: true, а возвращённая строка выглядит как успешный результат. - Перезапись.
store_featureиспользуетINSERT OR REPLACE, поэтому заменяет существующий ключ. Docstring говорит об этом, и модель знает об этом до вызова инструмента.
3. Тестирование в MCP Inspector
Прежде чем подключать сервер к Claude, проверьте его в MCP Inspector. Это небольшой веб-интерфейс, который подключается к серверу так же, как хост. В нём можно просмотреть ресурсы сервера, вызвать каждый инструмент со своими аргументами и увидеть каждый запрос и ответ.
SDK запускает Inspector через npx, поэтому на этом шаге также нужны Node.js и npm с npx в вашем PATH. Inspector 2.10.1 требует Node.js 22.19.0 или новее. Проверьте установленные команды, затем запустите Inspector:
node --version
npx --version
uv run mcp dev featurestore_server.py
Команда запускает сервер под MCP Inspector по stdio. Откройте URL в браузере, который выведет команда. Порт интерфейса Inspector зависит от реализации, поэтому не рассчитывайте на фиксированный порт.
Вызовите get_feature с key="user_123". Если он вернёт JSON начальной строки, сервер работает. Затем вызовите его с несуществующим ключом: результатом будет ошибка инструмента с isError: true. Так проверяются локальный путь протокола, регистрация инструментов, поиск в базе и путь ошибки. Выбор инструментов Claude это не проверяет; его проверяет раздел 4.
Inspector читает SQL-схему из ресурса schema://main:
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 запускает сервер как отдельный процесс. Для команды вроде mcp uv run находит проект по рабочей директории, поэтому --directory выбирает pyproject.toml и uv.lock этого проекта; --locked затем завершается ошибкой, а не меняет этот lock-файл. Команда uv add из шага 1.2 уже объявила mcp[cli] в проекте.
У SDK есть и быстрый путь: uv run mcp install featurestore_server.py сам создаст запись для Claude Desktop. В mcp 1.30 эта запись запускает uv run --frozen --with mcp[cli] mcp run /ABSOLUTE/PATH/featurestore_server.py. В ней нет --directory, поэтому она не использует uv.lock этого проекта, а у --with mcp[cli] нет границы <2. Подходит для быстрой пробы; воспроизводимой остаётся ручная конфигурация выше.
4.2. Как проходит взаимодействие
Вот что выполняется от начала до конца, когда Claude нужен поиск фичи. Шаги 1 и 2 выполняются один раз, когда Claude Desktop запускает сервер. Шаги с 3 по 6 выполняются для каждого вопроса, которому нужны данные фичей.
- Claude Desktop запускает для сервера отдельный MCP-клиент и обнаруживает его инструменты.
- Хост делает описания инструментов доступными для Claude.
- Claude может запросить tool call, когда вопросу нужны данные из фичестора.
- MCP-клиент отправляет этот запрос серверу.
- Сервер выполняет Python-функцию и возвращает результат хосту.
- Хост может передать результат Claude для итогового ответа.
Такое разделение на хост, клиент и сервер соответствует спецификации архитектуры MCP.
4.3. Примеры запросов
Перезапустите Claude Desktop и попробуйте несколько промптов:
-
“Покажи все доступные фичи.” Детерминированный результат содержит два начальных ключа:
user_123иproduct_abc. -
“Получи вектор фичей для user_123.” Ответ содержит вектор и метаданные
premiumизdatabase.py. -
“Сохрани новую фичу для
new_itemс вектором[0.5, 0.5]и метаданными{"type": "test"}. Затем получиnew_itemи покажи сохранённые вектор и метаданные.” После преобразования проверенный вектор должен быть непустым и содержать только конечные числа с плавающей точкой. Метаданные могут быть JSON-объектом,nullили отсутствовать. Последующее чтение проверяет весь цикл записи и чтения. -
“Получи вектор фичей для user_999.”
get_featureвыбрасывает ошибку для отсутствующего ключа. Claude должен сказать, что такого ключа нет, и может вызватьlist_features, чтобы показать допустимые ключи.
4.4. Как читать ответы Claude
Сервер возвращает данные инструментов и ресурсов. Claude выбирает инструмент и пишет пояснение вокруг результата.
Этот скриншот показывает запуск в июне 2025 года с более ранней версией примера. Я попросил Claude показать схему базы данных:
Claude назвал систему NoSQL- или документным хранилищем и показал концептуальную схему, но database.py создаёт реляционную таблицу SQLite. Показанный ответ — не команда CREATE TABLE, которую возвращает schema://main. Скриншот не показывает, читал ли Claude Desktop этот ресурс и какой контекст получил Claude, поэтому не устанавливает причину ошибки.
Ресурсами управляет приложение: хост решает, как включить их в контекст модели — через выбор пользователя или автоматически. MCP не требует от хоста передавать модели каждый ресурс. Если модель должна запросить схему через tool call, предоставьте её также как инструмент.
Правдоподобное объяснение модели — не то же самое, что вернул инструмент. Когда деталь важна, проверьте результат инструмента или исходный код.
4.5. Другие MCP-хосты
Сервер не зависит от Claude Desktop. Любой хост, который запускает stdio-серверы на той же машине, может выполнить ту же команду uv. Claude Desktop использует JSON из раздела 4.1; остальные два:
- 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. Всё после--— это команда, которая запускает сервер. - Cursor читает тот же формат JSON
mcpServersиз~/.cursor/mcp.jsonили.cursor/mcp.jsonв проекте.
Хост на другой машине не может запустить stdio-процесс на вашей. Если сервером пользуются несколько человек, запустите его по Streamable HTTP через mcp.run(transport="streamable-http") и добавьте перед ним аутентификацию.
5. Устранение неполадок
Сначала выясните, какая часть сломалась, и только потом меняйте код: ошибки запуска видны в логах, ошибки инструмента повторяются в Inspector без модели, а неверный ответ требует сравнить результат инструмента с текстом Claude.
-
«Server connection failed»:
- Проверьте логи в
~/Library/Logs/Claude/на macOS или%APPDATA%\Claude\logsна Windows.mcp.logзаписывает события соединения.mcp-server-featurestore.logв той же папке содержит собственный stderr сервера, поэтому трейсбек при запуске оказывается там (логи Claude Desktop). - Убедитесь, что в конфигурации указан абсолютный путь, а не относительный.
- Убедитесь, что
uvесть вPATHClaude Desktop. Если нет, укажите полный путь к бинарнику (где он лежит, покажетwhich uv). - В stdio по stdout идут сообщения протокола. Пишите логи в stderr:
print()в сервере портит этот поток и может оборвать соединение.
- Проверьте логи в
-
«Tool execution error»:
- Воспроизведите ошибку в Inspector командой
uv run mcp dev featurestore_server.py. Inspector показывает сообщение об ошибке и аргументы, которые её вызвали. - Проверьте, что
features.dbсоздаётся рядом сdatabase.py. Путь берётся изget_db_path(), который вычисляет его относительно скрипта, поэтому смена рабочей директории не должна сдвигать файл.
- Воспроизведите ошибку в Inspector командой
-
Вызов успешен, но ответ неверный:
- Откройте результат инструмента в чате и сравните его с текстом Claude. Раздел 4.4 показывает ответ модели, в котором база данных описана неверно.
6. Перед тем как агент начнёт использовать это всерьёз
store_featureперезаписывает любой ключ, включая начальные строки. Оставьте в хосте подтверждение каждого вызова для инструментов записи или вынесите их на отдельный сервер, который вы включаете только при необходимости.- Результаты инструментов попадают к провайдеру модели как часть диалога. Не возвращайте данные, которые должны остаться на машине.
- Серверу, которым пользуются несколько человек, нужны Streamable HTTP и аутентификация вместо stdio (раздел 4.5).
7. Заключение
Вот и всё: сервер FastMCP, хранилище SQLite и конфигурация Claude Desktop, которая указывает на команду uv run. Та же схема подходит для большинства задач, которые можно обернуть в Python-функцию. Замените вызовы SQLite на настоящий фичестор, внутренний API или реестр моделей, и сервер останется небольшим.

