MCP Quickstart: свой MCP server за вечер

За вечер соберёте на Python MCP-сервер: программу, через которую Claude Desktop, Cursor или ваш агент вызывают ваши функции и читают ваши данные. В финале у вас будет сервер базы знаний, который не отдаёт закрытые документы. Ломается обычно на мелочах. Сервер работает в терминале, но в приложении его нет. Лишний print в stdout вешает клиента, а ошибка, возвращённая строкой, уходит модели как ответ. Ставить ничего не нужно, ключи API тоже: лабы идут в песочнице на официальном Python SDK.

Чему вы научитесь

Сервер базы знаний, который не отдаст закрытый документ

В финале курса вы собираете MCP-сервер базы знаний учебной компании «Ортус». Модели внутри сервера нет: какой инструмент вызвать, решает модель в приложении, которое подключило сервер. В базе вперемешку лежат тарифы и документация API, зарплатные вилки и условия сделок. У каждого документа свой access_level: public, internal или restricted.

Просьба к модели «не показывай закрытое» защитой не считается. Граница стоит в коде сервера: search строит BM25 только по публичным документам, а get_document на закрытый и на несуществующий id отвечает одной и той же ошибкой.

Автопроверка лабы, настоящий MCP-клиент, ищет зарплатные вилки по грейдам, бонусный пул руководителей и аварийный доступ Vault break-glass. Ни одного непубличного документа в выдаче быть не должно. Prompt answer_with_sources требует ставить источник [doc_id] после каждого утверждения.

Для кого

Python-разработчикам, которые подключают свои данные и API к Claude Desktop, Cursor или собственному агенту.

Тем, у кого сервер работает в терминале, но в приложении не появляется или клиент на нём зависает. Здесь причину ищут по сообщениям протокола.

Инженерам, которым мало декораторов SDK: нужно видеть, какая строка ушла в stdin сервера и какая пришла из stdout.

Инженерам, которые открывают агенту внутреннюю базу документов и отвечают за то, что он оттуда достанет.

Что нужно знать

Python: функции, словари, исключения, запуск процесса через subprocess.

Умение читать асинхронный код: async with, await, asyncio.run. Клиент MCP в курсе асинхронный.

pydantic BaseModel и Field пригодятся: на них строится структурированный ответ инструмента.

JSON: объекты, массивы, строки.

Код в курсе на Python. Если вы пишете на Go, протокол и ошибки те же, но лабы сдаются на Python.

До первого вызова нужны три строки

Первым ломается рукопожатие. Клиент шлёт initialize, сервер отвечает serverInfo и capabilities, клиент отправляет notifications/initialized. Без последней строки сервер отклоняет tools/list. Это самая частая ошибка, когда работаете с протоколом руками: клиент ждёт result и падает с KeyError.

Поэтому первым вы пишете клиента без SDK: subprocess и строки JSON-RPC. Каждое сообщение пишется одной строкой JSON с \n в конце, после записи нужен flush. Без flush клиент висит на readline, а лишний print в stdout сервера ломает поток. Между запросом и ответом может прийти notifications/message, свой ответ находят по id.

Дальше эти строки спрячет SDK, но при сбое вы будете знать, что ушло в сервер и что вернулось.

Что вернёт сервер на limit=0?

Модель передаёт инструменту что угодно: limit=0, строку вместо числа, имя заметки, которой нет. Сервер должен ответить так, чтобы отказ было видно. Ошибка инструмента приходит с isError: true, ошибка ресурса или prompt приходит как McpError, а поиск без совпадений отдаёт пустой список без ошибки.

Опаснее всего безобидная на вид строка return "Ошибка: деление на ноль". Для протокола это успешный ответ с isError false, и модель примет его за результат. Автопроверка такую работу не засчитает: ошибку нужно бросить исключением.

С типами то же самое. Значение, которое к типу не приводится, например "много" вместо числа, FastMCP отсекает сам: функция не вызывается, клиент получает isError. А "3" для int он молча превратит в 3, так что на схему целиком не рассчитывайте. Без аннотаций параметр попадает в схему как string, клиент шлёт "2", и сложение превращается в склейку строк. Поэтому автопроверка вызывает ваш add с отрицательными и случайными числами, а search с limit вне диапазона от 1 до 10.

Ваш тест против пяти серверов

Зелёный тест ещё ничего не доказывает. Сломанный add, который складывает модули чисел, пройдёт add(2, 3) и упадёт только на add(-2, 5).

Вы пишете контрактный тест: путь к серверу передаётся аргументом, итог отдаётся кодом выхода 0 или 1, при нарушении одна строка FAIL. Автопроверка запускает его на пяти серверах. Два правильных, в трёх есть поломки: divide возвращает строку вместо isError, add теряет знак, ресурс notes:// отдаёт не тот текст.

Пропустите поломку и увидите: «тест не поймал сломанный сервер… Тест завершился кодом 0, а должен 1». Клиента и конфиг автопроверка тоже запускает по-настоящему. Клиент обязан смотреть result.isError до чтения content. В mcpServers команда python должна быть абсолютным путём: приложение запускается не из вашего терминала и не видит ваш PATH.

Что дальше

Продолжить тему на Mentorix можно в курсе «MCP Integrations». Там серверы работают с PostgreSQL и pgvector, опасные действия идут через подтверждение, доступ закрывается токенами со scopes, а агента защищают от prompt injection.

Вопросы по курсу можно задать поддержке: t.me/mentorix_support.

Программа курса

Модулей: 3, уроков: 12. Задачи с кодом проверяются автоматически.

  1. Что такое MCP и как он разговаривает

  2. Сервер целиком: tools, resources, prompts

  3. Клиент, проверка и подключение

Бесплатный курс

Открыть курс на Mentorix · Все курсы