MCP (Model Context Protocol) называют «USB-C для ИИ-агентов»: один протокол, и к модели подключаются десятки готовых интеграций без костылей. Звучит красиво, но настоящее понимание приходит только когда соберёшь сервер руками — где протокол реально экономит, а где придётся повозиться, видно лишь на практике. Я собрал свой за вечер и рассказываю по шагам.
Коннектор отдаёт Claude мою базу знаний — словарь из 90 ИИ-терминов и блок частых вопросов. Спрашиваешь в диалоге «что такое RAG» — и Claude достаёт определение из моей базы, со ссылкой на источник. Дальше — стек, код, деплой за nginx и три грабли, на которых я залип.
Зачем бизнесу свой MCP-сервер
Чтобы данные и инструменты жили прямо внутри ИИ-ассистента, там, где идёт работа, без выгрузки в отдельную систему. Что я вижу у клиентов:
-
внутренний справочник, доступный в Claude без копипасты;
-
доступ к CRM или таблицам «только на чтение» — аналитика прямо в диалоге;
-
каталог, прайс, документация — то, что сотрудник иначе ищет руками.
У меня случай простой — база знаний по ИИ. На ней и покажу.
Стек: FastMCP и Streamable HTTP
Сервер на Python, фреймворк — FastMCP (версия 2.14.7 на момент сборки). Главное решение тут — транспорт. Старый SSE в спецификации MCP уже депрекейтнут, живой вариант — Streamable HTTP. Хотите удалённый коннектор, который подключается по URL, — берите его. В FastMCP это одна строка:
mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)
Endpoint — /mcp, и именно без слэша. /mcp/ отдаёт 307-редирект, а голый GET ловит 406 (нужен заголовок Accept: text/event-stream). Эта мелочь ещё аукнется на healthcheck.
Инструменты: тонкие обёртки с аннотациями
Инструментов четыре, все — только на чтение. Вот регистрация одного:
@mcp.tool( annotations={ "readOnlyHint": True, "destructiveHint": False, "openWorldHint": False, })def search_glossary(query: str, limit: int = 8) -> str: """ Search the AI glossary (90 terms) by keyword. Returns matching terms with short definitions and source URL. """ ...
annotations тут работают на дело: они сообщают клиенту, что инструмент делает — читает данные или меняет, опасное ли действие, лезет ли наружу. По ним клиент решает, спрашивать ли у пользователя подтверждение. Совет из практики: проставляйте аннотации на каждом инструменте сразу. При подаче в каталог Anthropic их отсутствие — одна из самых частых причин отказа. Дешевле сделать заранее, чем переделывать после ревью.
И ещё мелочь, которая бережёт нервы: логику каждого инструмента я вынес в отдельную функцию, а @mcp.tool оставил тонкой обёрткой. Так тесты гоняет обычный pytest, без подъёма всего MCP-стека.
Данные: грузим один раз на старте
База — статичные JSON (90 терминов плюс 21 вопрос), выгрузил их из генераторов своего сайта. Файлы крошечные, поэтому читаю их в память один раз при старте и там же строю индекс поиска. Никаких обращений к диску на каждый вызов:
# Load data once on import (FastMCP calls tools after startup)load_data()
В каждом ответе — атрибуция и ссылка на источник. Так и пользователю честнее (видно, откуда определение), и мне полезно: ответы ведут на сайт.
Деплой: Docker за nginx и три грабли
Сервер крутится в Docker на обычном VPS, наружу торчит через nginx с TLS. Тут я собрал три неочевидные вещи.
Грабля 1. Docker: «all predefined address pools have been fully subnetted». На сервере с кучей compose-проектов у Docker кончился пул адресов под новые сети. Лечится без боли для соседних контейнеров — сажаем контейнер на дефолтный bridge вместо отдельной сети:
services: zs-mcp: network_mode: bridge ports: - "127.0.0.1:8096:8000" # наружу только через nginx
Грабля 2. Healthcheck, который вечно «unhealthy». Первый healthcheck дёргал GET /mcp/ через urllib. Но /mcp/ отдаёт 307, а /mcp без SSE-заголовка — 406. urllib видит ответ мимо 200 и считает контейнер больным. Решение — проверять только то, что порт слушает, мимо протокола:
healthcheck: test: ["CMD", "python", "-c", "import socket; socket.create_connection(('localhost', 8000), timeout=5).close()"]
Грабля 3. nginx буферизует SSE. Streamable HTTP отвечает потоком (event: message). nginx по умолчанию буферизует ответ апстрима — и поток залипает. В локации обязательно гасим буферизацию:
location / { proxy_pass http://127.0.0.1:8096; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 3600s; chunked_transfer_encoding on;}
TLS — обычный certbot на поддомен. После этого сервер живёт на https://mcp.zinin-shturbin.com/mcp.
Проверка: рукопожатие руками
Прежде чем тащить коннектор в Claude, я проверяю сервер вживую обычным curl. Сначала initialize, ловлю mcp-session-id из заголовка:
curl -sD - -X POST https://mcp.zinin-shturbin.com/mcp \ -H 'Content-Type: application/json' \ -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize", "params":{"protocolVersion":"2024-11-05","capabilities":{}, "clientInfo":{"name":"curl","version":"1"}}}'
В ответе — serverInfo с именем сервера, в заголовках — mcp-session-id. С этим session-id дальше дёргаешь tools/list и живой tools/call. Когда search_glossary по запросу «MCP» вернул определение со ссылкой, стало ясно: вся цепочка (TLS → nginx → контейнер → streamable-http) держит.
Подключение в Claude
В Claude (claude.ai или Claude Desktop): Settings → Connectors → Add custom connector → имя и URL https://mcp.zinin-shturbin.com/mcp. Авторизация тут лишняя (данные публичные, чтение) — подключается одним движением.
Что в итоге
За вечер — рабочий удалённый MCP-сервер: FastMCP плюс Streamable HTTP, четыре read-only инструмента с аннотациями, Docker за nginx с TLS. Грабли, которые держу в голове на будущее:
-
endpoint
/mcpбез слэша; -
аннотации на каждом инструменте сразу;
-
network_mode: bridge, когда у Docker кончились подсети; -
healthcheck через TCP-connect вместо HTTP-запроса к
/mcp; -
proxy_buffering offпод SSE-поток.
Начинайте с самого узкого полезного сервера — одна-две функции на чтение. Доводите цепочку до рабочего состояния и только потом расширяйте: так сразу видно, где протокол реально экономит, а где он лишний.
Сам коннектор отдаёт мой словарь ИИ-терминов — он открыт, заходите полистать. Такие штуки я собираю с фаундерами в своей практике обучения работе с ИИ — теми, кому ИИ нужен живым инструментом внутри команды. Ловили грабли, которых тут нет, — пишите в комментарии, добавлю.
ссылка на оригинал статьи https://habr.com/ru/articles/1042470/