MCP-сервер своими руками на Python
Про MCP уже написано много объяснений «что это и зачем», а вот до конкретного «как написать свой сервер на Python» руки доходят реже — документация официального SDK разрослась, и непонятно, с какого конца заходить. На деле минимальный рабочий MCP-сервер — это меньше тридцати строк кода. Дальше разберём, из чего он состоит, напишем один настоящий инструмент и проверим, что всё работает, прежде чем тащить это на сервер или подключать к Claude Desktop.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Что понадобится и с чего начать
Нужен Python 3.10 или новее и официальный пакет mcp от Anthropic — он и есть SDK, вокруг которого построена вся статья. Ставить можно классическим pip, но для новых Python-проектов удобнее uv: он сам создаёт виртуальное окружение и решает версии зависимостей.
curl -LsSf https://astral.sh/uv/install.sh | sh
mkdir mcp-demo && cd mcp-demo
uv init
uv add "mcp[cli]"
Хвост [cli] важен: без него ставится только сама библиотека протокола, а вам ещё понадобится консольная утилита mcp — она пригодится для локальной проверки сервера без подключения к Claude Desktop. Если предпочитаете обычный pip и venv:
python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"
Для минимального примера хватит одного файла — server.py в корне проекта. Отдельный pyproject.toml можно не заводить, если через uv init он уже создался.
Из чего состоит MCP-сервер
Если отвлечься от конкретных названий функций, любой MCP-сервер на Python устроен из трёх частей, и это верно и для официального SDK, и для реализаций на других языках.
Объект сервера — точка, к которой цепляются все инструменты, ресурсы и промпты. У него есть имя, по которому сервер представляется клиенту при подключении.
Описание и обработчик инструмента — это то, что модель реально вызывает. Описание — имя инструмента, человекочитаемое пояснение, что он делает, и схема входных параметров. Обработчик — код, который выполняется, когда модель решает этим инструментом воспользоваться, и результат которого возвращается обратно.
Точка входа — код, который поднимает транспорт (то, как сервер общается с клиентом — через стандартный ввод-вывод или по сети) и запускает цикл обработки запросов.
Официальный Python SDK даёт два способа собрать эти три части. Низкоуровневый API — вы вручную регистрируете обработчик «дай список инструментов» и обработчик «вызови инструмент по имени», сами описываете JSON-схему параметров и сами поднимаете транспорт. Это гибко, но многословно. Для подавляющего большинства задач в SDK есть FastMCP — обёртка, где инструмент объявляется одним декоратором над обычной Python-функцией: имя параметра и его аннотация типа становятся схемой, а строка документации — описанием инструмента для модели. Ниже используем именно её — по объёму кода разница в разы, а результат по протоколу неотличим.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSПервый инструмент: читаем файл
Возьмём практичный пример: инструмент, который отдаёт модели содержимое текстового файла из заранее разрешённой папки — то есть у ИИ-ассистента появляется «рука», которой раньше не было.
# server.py
from pathlib import Path
from mcp.server.fastmcp import FastMCP
NOTES_DIR = Path(__file__).parent / "notes"
mcp = FastMCP("notes-demo")
@mcp.tool()
def read_note(filename: str) -> str:
"""Читает текстовый файл из папки notes и возвращает его содержимое.
filename: имя файла без пути, например "todo.txt".
"""
path = (NOTES_DIR / filename).resolve()
if NOTES_DIR.resolve() not in path.parents and path != NOTES_DIR.resolve():
raise ValueError("Доступ разрешён только к файлам внутри папки notes")
if not path.is_file():
raise FileNotFoundError(f"Файл {filename} не найден")
return path.read_text(encoding="utf-8")
if __name__ == "__main__":
mcp.run()
Три вещи здесь важнее самого кода. Во-первых, @mcp.tool() берёт имя функции, аннотации типов параметров и докстринг и на их основе сам собирает описание инструмента, которое видит модель — вам не нужно отдельно писать JSON-схему руками. Во-вторых, докстринг — не формальность: это единственное, по чему модель понимает, что делает инструмент и как его вызывать, поэтому пишите его так же внятно, как объясняли бы задачу коллеге. В-третьих, проверка path.resolve() внутри NOTES_DIR — не паранойя, а необходимость: без неё модель (или что-то, подсунувшее ей нужный промпт) может запросом вроде ../../etc/passwd прочитать файл далеко за пределами папки notes. Любой инструмент, работающий с файловой системой, обязан ограничивать доступ явно.
Исключения ValueError и FileNotFoundError SDK сам превращает в понятный модели ответ об ошибке — оборачивать их вручную в try/except не нужно, если вас устраивает стандартное сообщение.
Второй инструмент: обращаемся к своему API
Инструменты необязательно только читают локальные файлы — часто нужнее, чтобы сервер сходил во внешний или собственный API и вернул модели актуальные данные. FastMCP одинаково поддерживает и синхронные, и асинхронные функции, а для сетевых вызовов асинхронный вариант — стандартный выбор.
import httpx
@mcp.tool()
async def get_server_status(hostname: str) -> str:
"""Запрашивает статус сервера через внутренний API мониторинга.
hostname: доменное имя или адрес сервера, статус которого нужно узнать.
"""
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
"https://api.example.internal/status",
params={"host": hostname},
)
response.raise_for_status()
data = response.json()
return f"{hostname}: {data['state']}, аптайм {data['uptime_hours']} ч."
(В httpx нужна установка: uv add httpx.) Обратите внимание на timeout — без него зависший запрос к API подвесит и весь вызов инструмента, а вместе с ним — ответ модели пользователю. response.raise_for_status() превратит ошибку HTTP в исключение, которое SDK так же аккуратно доведёт до клиента, вместо того чтобы молча вернуть модели пустой или обрезанный ответ.
Если инструментов становится несколько, держите их в одном файле только для совсем маленького сервера — начиная с трёх-четырёх стоит выносить в отдельные модули и импортировать в server.py, чтобы декоратор @mcp.tool() всё равно применялся к объекту mcp из одного места.
Локальный запуск и проверка без подключения к клиенту
Прежде чем тянуть сервер в Claude Desktop, его стоит проверить в изоляции — это экономит время на отладке, потому что ошибки видно сразу, без слоя «а вдруг дело в настройках клиента». Для этого в пакете mcp[cli] есть команда dev, которая поднимает сервер и веб-интерфейс инспектора рядом с ним:
uv run mcp dev server.py
Команда откроет локальный адрес в браузере — там видно список объявленных инструментов ровно в том виде, в каком их увидит модель, можно вручную вызвать read_note с конкретным именем файла и посмотреть на сырой ответ или на текст ошибки, если что-то пошло не так. Это самый быстрый способ поймать опечатку в докстринге или неверную схему параметров, не дожидаясь, пока это заметит уже подключённый ИИ-ассистент.
Здесь же стоит один нюанс, который ломает сервер незаметно: при транспорте по умолчанию (stdio) стандартный вывод процесса — это канал протокола, а не место для логов. Случайный print() внутри инструмента разъезжает JSON-RPC-сообщение, и клиент падает с ошибкой парсинга, которую по тексту не связать с истинной причиной. Если нужна диагностика — пишите в stderr:
import sys
print("отладочное сообщение", file=sys.stderr)
или настройте штатный модуль logging — он по умолчанию тоже пишет в stderr, а не в stdout.
Подключение к Claude Desktop и что дальше
Сервер, который проходит проверку в инспекторе, готов к подключению — но сама процедура регистрации в конфиге Claude Desktop, с путями и форматом JSON, подробно разобрана отдельно, повторять её здесь смысла нет: смотрите статью как подключить MCP-сервер к Claude Desktop. Если в процессе что-то пойдёт не так — сервер не появляется в списке, падает при первом вызове инструмента, — есть отдельный разбор частых ошибок MCP-сервера с типичными причинами и решениями. А если пока не до конца ясна сама идея протокола — с чего лучше начать, что такое хост, клиент и сервер в этой связке, — короткое объяснение без жаргона есть в статье что такое MCP простыми словами.
Локальный stdio-сервер, который мы написали, отлично работает, пока он нужен только вам на своей машине. Как только требуется, чтобы к серверу подключался кто-то ещё — второй человек, веб-клиент, CI-пайплайн — потребуется уже сетевой транспорт вместо stdio и отдельная машина, на которой сервер будет постоянно работать, а не запускаться клиентом по требованию. Это отдельная задача с собственными нюансами — портом, прокси и авторизацией, — и она разобрана в статье как поднять MCP-сервер на VPS. Для локальной разработки и проверки идеи всё, что описано выше, — этого достаточно, дополнительный сервер не нужен.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать VPSОбсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Частые вопросы
Обязательно ли использовать FastMCP, или можно писать на низкоуровневом API?
Не обязательно — низкоуровневый API даёт больше контроля (например, свою логику формирования схемы параметров или нестандартную обработку списка инструментов), но для подавляющего большинства серверов с несколькими инструментами FastMCP закрывает задачу в разы меньшим объёмом кода. Начинать стоит с неё, а к низкоуровневому API переходить только если упёрлись в конкретное ограничение.
Нужно ли самому валидировать типы входных параметров?
Базовая проверка типов (что параметр — строка, число, булево значение и так далее) делается автоматически на основе аннотаций типов в сигнатуре функции. А вот содержательная проверка — как в примере с чтением файла, где нужно убедиться, что путь не выходит за пределы разрешённой папки, — остаётся на вас; SDK не знает бизнес-логику вашего инструмента.
Как вернуть модели не текст, а структурированные данные?
Функция-инструмент может вернуть не только строку — SDK поддерживает и другие типы содержимого ответа. Для большинства практических случаев, впрочем, достаточно вернуть аккуратно отформатированный текст или JSON-строку: модель разбирает и то, и другое одинаково хорошо.
Что делать, если инструмент должен выполняться долго?
Пишите его как асинхронную функцию и не блокируйте цикл событий синхронными операциями внутри — синхронный requests.get() вместо httpx.AsyncClient в асинхронном инструменте застопорит обработку остальных запросов, пока не выполнится текущий.
Можно ли в одном сервере совмещать инструменты, ресурсы и промпты?
Да, это обычная практика: @mcp.tool() для действий, @mcp.resource() для данных на чтение, @mcp.prompt() для готовых шаблонов запросов — все три вида регистрируются декораторами на одном и том же объекте FastMCP и живут в одном файле или пакете.
Нужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.