Что такое MCP сервер и как его создать. Часть 2
|
Когда я впервые попытался подключить свой MCP сервер к Claude Desktop, думал что это будет как с любым другим API - указал эндпоинт, авторизовался, готово. Но нет. MCP требует конфигурирования на стороне клиента, и каждое приложение делает это по-своему. Причём документация местами отсутствует или устарела, приходилось разбираться методом проб и ошибок. Что такое MCP сервер и как его создать. Часть 1 Клиентские приложения для MCP можно разделить на две категории. Первая - полноценные AI-ассистенты типа Claude Desktop или продукты от Anthropic. Они запускают серверы как дочерние процессы через stdio, управляют lifecycle, показывают доступные инструменты в интерфейсе. Вторая - кастомные клиенты, написанные разработчиками под конкретные нужды. Они могут использовать HTTP-транспорт, работать через proxy, добавлять свою логику авторизации. Claude Desktop конфигурируется через JSON-файл в системной папке настроек. На Mac это ~/Library/Application Support/Claude/claude_desktop_config.json, на Windows - %APPDATA%/Claude/claude_desktop_config.json. Файл содержит объект mcpServers где ключ - имя сервера, значение - параметры запуска. Для stdio-сервера указываешь command и args - путь к исполняемому файлу и аргументы командной строки. Я долго не мог понять почему мой сервер не запускается пока не включил логирование. Оказалось - Claude Desktop запускает процесс в своей рабочей директории, относительные пути не работают. Пришлось указывать абсолютный путь к Node.js и к скрипту сервера. Ещё один момент - переменные окружения. Если сервер использует env vars для конфигурации, их нужно явно прокидывать через параметр env в конфиге.
HTTP-серверы подключаются иначе. Вместо command указываешь url и transport: "http". Но тут начинаются нюансы с CORS, аутентификацией, сертификатами если используешь HTTPS. Claude Desktop не показывает подробных ошибок соединения - просто "server failed to connect". Приходится смотреть логи сервера и дебажить через curl. Другие AI-клиенты реализуют MCP по-своему. Cursor и некоторые другие IDE имеют встроенную поддержку, но конфигурируются через свои settings.json. Есть проекты типа mcp-client - библиотеки для написания собственных клиентов на TypeScript или Python. Они дают больше контроля но требуют кода вместо конфигурационных файлов. Тестирование интеграции - отдельная боль. Нельзя просто написать unit-тест и проверить что сервер работает. Нужно запустить реального клиента, выполнить действия через интерфейс, проверить результат. Я автоматизировал часть проверок через скрипты которые шлют JSON-RPC напрямую в stdin сервера, но это не покрывает реальные сценарии использования. Версионирование capabilities становится критичным когда обновляешь сервер. Старые клиенты могут не поддерживать новые фичи, новые клиенты могут требовать capabilities которых нет у старых серверов. Протокол предусматривает согласование возможностей через handshake, но это не спасает от проблем совместимости если клиент некорректно обрабатывает отсутствие ожидаемых функций. Подключение к Claude DesktopПервое подключение моего MCP сервера к Claude Desktop заняло три часа вместо обещанных пяти минут. Официальная документация показывала радужную картинку - добавь пару строк в конфиг и всё заработает. На практике я встретил с десяток неочевидных проблем, каждая из которых требовала гугления и чтения issue на GitHub. Начать нужно с поиска конфигурационного файла. На macOS он живёт по адресу ~/Library/Application Support/Claude/claude_desktop_config.json. Если файла нет - создайте вручную, Claude не сделает это за вас. На Windows путь будет %APPDATA%\Claude\claude_desktop_config.j son, на Linux обычно ~/.config/Claude/claude_desktop_config.json, хотя я встречал вариации в зависимости от дистрибутива. Структура файла элементарная - корневой объект с ключом mcpServers, внутри которого описания серверов. Каждый сервер имеет уникальное имя-ключ, а значение содержит параметры запуска. Для stdio-транспорта обязательны два поля: command - полный путь к исполняемому файлу, и args - массив аргументов командной строки.
Рабочая директория процесса тоже оказалась сюрпризом. Сервер запускается не из той папки где лежит его код, а из домашней директории пользователя. Если в коде есть относительные пути к конфигам или ресурсам - они сломаются. Я хранил настройки в ./config/settings.json рядом со скриптом, и сервер их не находил. Переписал на абсолютные пути через __dirname или process.env.HOME. Переменные окружения передаются через опциональное поле env. Это объект где ключи - имена переменных, значения - их значения. Если сервер использует DATABASE_URL или API_KEY, прописывайте их здесь. Переменные из вашего shell-окружения автоматически не наследуются, что логично с точки зрения безопасности но неудобно при разработке.
Отладка проблем подключения требует терпения. Claude Desktop не показывает детальных ошибок в интерфейсе - максимум невнятное "Failed to connect to server". Смотреть нужно в логи приложения и логи самого сервера если вы их настроили. На Mac логи Claude лежат в ~/Library/Logs/Claude/mcp*.log, там видно попытки запуска серверов, ошибки stdio, JSON-RPC сообщения. Я добавил в свой сервер логирование в отдельный файл:
Тестирование нового сервера начинается с простого промпта в Claude Desktop: "Покажи доступные инструменты". Если сервер подключен правильно, модель выведет список зарегистрированных tools с описаниями. Дальше можно пробовать реальные запросы типа "Создай заметку с заголовком Test" и смотреть что происходит. Windows добавляет свои грабли. Пути с обратными слешами нужно экранировать или использовать прямые. PowerShell и CMD имеют разную семантику кавычек в аргументах. Я видел конфиги где args содержали строки типа C:\Users\Name\project\script.js - работало, но выглядело ужасно. Проще перейти на forward slashes даже на Windows, Node.js их понимает. Управление несколькими серверами одновременно - обычная практика. У меня в конфиге пять разных MCP серверов: для работы с заметками, для мониторинга серверов, для управления задачами, для поиска по документации, для интеграции с почтой. Claude Desktop запускает их все при старте, каждый живёт в своём процессе. Главное чтобы имена не пересекались и инструменты имели понятные уникальные названия. Проверка работоспособности после обновления кода требует полного цикла: изменить код, собрать (npm run build), убить Claude Desktop, запустить заново, проверить в чате. Автоматизация помогает - я написал bash-скрипт который делает всё одной командой. Экономит минуты на каждой итерации, а их бывает десятки в день. Если существует такое число A, что после приведения его в порядок, получается B, то выведите любое такое число Одни говорят что класс это объект, другие, что класс-это шаблон объекта, дак что такое объект? Как в select option добавить часть текста: value = 'часть текста'? Как взять часть ссылки средствами JS и вставить эту часть в другую ссылку? Работа с другими AI-ассистентамиClaude Desktop - не единственный клиент для MCP, хотя документация создаёт именно такое впечатление. Я потратил неделю на интеграцию с Cursor IDE и обнаружил что каждый клиент понимает протокол по-своему, добавляя собственные особенности и ограничения. Cursor поддерживает MCP нативно через свой конфигурационный файл. Находится он в .cursor/config.json внутри проекта или в глобальных настройках редактора. Структура похожа на Claude Desktop, но есть нюансы. Cursor запускает серверы в контексте проекта, поэтому относительные пути работают корректно - удобнее чем абсолютные. Другое отличие - серверы стартуют только когда открываешь проект, а не вместе с самим редактором.
Собственные клиенты на основе MCP SDK дают максимальную гибкость. Я написал простой CLI-клиент для тестирования серверов локально, без GUI и лишних зависимостей. Запускаешь сервер через child_process, шлёшь JSON-RPC команды, получаешь ответы. Идеально для CI/CD пайплайнов и автоматического тестирования.
Я столкнулся с багом когда один клиент отправлял параметры инструмента как строку даже если схема требовала число. Сервер валидировал через Zod и падал с ошибкой валидации. Пришлось добавить принудительное преобразование типов перед валидацией - грязный хак но другого решения не нашел. Версионирование capabilities становится критичным при работе с разными клиентами. Один поддерживает только базовые инструменты, другой умеет работать с промптами и ресурсами, третий добавил собственные расширения протокола. Сервер должен деградировать gracefully, предоставляя минимальную функциональность если клиент не поддерживает продвинутые фичи. Я проверяю capabilities клиента при инициализации и отключаю несовместимые возможности. Тестирование на разных клиентах обязательно если планируете публичный релиз сервера. У меня стоит набор автотестов которые гоняют сервер через разные клиенты и проверяют что базовая функциональность работает везде одинаково. Специфичные фичи тестируются отдельно для каждого клиента. Отладка и мониторинг взаимодействияОтладка MCP серверов - это отдельный круг ада для разработчиков. Первый раз когда мой сервер молча падал без объяснений, я потратил два часа на поиск проблемы. Console.log не работает - stdout занят протоколом, любой вывод туда ломает JSON-RPC. Debugger подключить нельзя - процесс запускается клиентом, не тобой. Остаётся только stderr и файлы, причём логирование нужно настраивать заранее, post-factum ничего не узнаешь. Я сразу добавляю в каждый новый сервер модуль логирования который пишет в файл. Простая функция с timestamp'ом и уровнями debug/info/error спасает кучу времени. В разработке ставлю DEBUG, в продакшене только ERROR и выше. Формат JSON удобен для автоматического парсинга, plain text читается человеком проще.
Хитрый приём - перехватывать process.stdin и process.stdout через прокси-стримы. Читаешь из реального stdin, пишешь копию в лог, передаёшь дальше. То же самое с stdout в обратную сторону. Получается полная запись сессии которую можно replay для воспроизведения бага.
Метрики производительности собираю через простой wrapper вокруг обработчиков. Замеряю время выполнения каждого инструмента, считаю количество вызовов, отслеживаю ошибки. Раз в час сбрасываю статистику в файл, потом анализирую что тормозит и где узкие места. Типичная проблема - race condition при конкурентных запросах. Клиент может отправить несколько вызовов инструментов одновременно, они начинают выполняться параллельно. Если инструменты модифицируют общее состояние без синхронизации, получается каша. Я добавил queue для критичных операций - только одна выполняется в момент времени, остальные ждут. Производительность упала но стабильность выросла, а это важнее. Расширенные возможности и паттерныБазовый CRUD-сервер работает, но это только начало. Когда я начал строить реальные интеграции, быстро понял что нужны более хитрые паттерны. Middleware для общей логики, композиция серверов для модульности, graceful degradation когда что-то падает. Дальше покажу приёмы которые сэкономили мне недели отладки. Middleware-паттерн пришёл из Express.js но прекрасно работает и в MCP. Идея проста - обернуть обработчики инструментов в цепочку функций которые выполняются до и после основной логики. Логирование, валидация, проверка прав, кэширование - всё это middleware. Я написал общий wrapper который применяется ко всем инструментам автоматически:
Rate limiting защищает от перегрузки когда модель спамит запросами. Простейшая реализация через счётчик вызовов за окно времени:
Graceful degradation означает что сервер продолжает работать даже если часть функций недоступна. База данных упала - возвращаем кэшированные данные. API не отвечает - используем fallback-источник. Я добавил это в сервер новостей - если RSS фид недоступен, берём данные из локального бэкапа. Работа с ресурсами и промптамиРесурсы и промпты - это части MCP которые я долго игнорировал, считая второстепенными. Инструменты казались важнее - они выполняют действия, меняют данные, делают что-то полезное. Ресурсы просто отдают контент, промпты вообще выглядели как готовые текстовые шаблоны. Но когда разобрался, понял что недооценивал их возможности. Ресурс в MCP - это именованный источник данных который сервер предоставляет клиенту. У каждого ресурса есть URI по которому его можно запросить, MIME-тип и содержимое. Клиент получает список доступных ресурсов, модель решает какие нужны для ответа на запрос, клиент читает их и передаёт модели как контекст. Звучит просто, но дьявол в деталях. URI-схема полностью произвольная. Я использую префикс специфичный для домена - note:// для заметок, log:// для логов, doc:// для документации. После префикса идёт путь к конкретному ресурсу. Например note:///meeting-notes.md указывает на файл заметки, log:///2024/01/15/errors.log на лог-файл за конкретную дату.Регистрация ресурсов делается через метод resource() у сервера. Первый параметр - URI-паттерн, второй - описание, третий - обработчик который возвращает содержимое. Паттерн может содержать параметры в фигурных скобках - doc:///{section}/{page} матчится на любые значения section и page.
metrics:///server/cpu сервер опрашивает систему и возвращает текущую загрузку процессора.Бинарный контент передаётся через base64. Поле text заменяется на blob с закодированными данными. Я использую это для изображений в документации - модель может запросить картинку и получить её содержимое, хотя интерпретировать напрямую не может. Промпты - это готовые шаблоны для взаимодействия с моделью. Сервер предлагает набор промптов, каждый с параметрами. Клиент подставляет значения и использует результат как часть контекста. Честно говоря использую их редко - большинство задач решается через инструменты и ресурсы. Но есть сценарии где промпты удобнее. Регистрация промпта похожа на инструмент - имя, описание, схема аргументов и обработчик. Разница в том что промпт возвращает текст для модели, а не выполняет действие.
Шаблонизация промптов через параметры даёт гибкость. Один промпт с разными аргументами генерирует разные контексты. У меня есть промпт для code review - принимает путь к файлу и тип проверки (security/performance/style), формирует соответствующий запрос. Кэширование ресурсов критично для производительности. Если каждый раз читать файлы или дёргать API - будет медленно. Я добавил простой LRU-кэш который хранит последние N ресурсов в памяти. TTL настраивается отдельно для каждого типа ресурсов. Подписка на изменения ресурсов реализуется через notifications. Когда файл меняется, сервер шлёт notifications/resources/list_changed и клиент может перезапросить список. FileSystemWatcher в Node.js отслеживает изменения автоматически, не нужно поллить директорию.Ресурсы и промпты дополняют инструменты, давая модели больше контекста без явного вызова функций. Модель сама решает что читать и когда, а сервер предоставляет данные по запросу. Получается более естественное взаимодействие чем жёсткие API-вызовы. Реализация инструментов и командИнструменты - это сердце MCP сервера, то ради чего всё затевается. Когда я писал первые серверы, делал инструменты интуитивно - что казалось нужным, то и добавлял. Через пару недель понял что половину функций модель не использует потому что описания невнятные, а другая половина работает криво из-за плохой валидации параметров. Пришлось переписывать с нуля, применяя паттерны которые сейчас покажу. Хороший инструмент начинается с правильного имени. Не абстрактное execute_action или do_something, а конкретное create_note или send_email. Модель должна понимать что делает инструмент просто глядя на название. Я использую глагол в начале - create, read, update, delete, search, list. Дальше существительное - note, file, record. Получается self-documenting название которое не требует чтения описания. Описание инструмента пишется для модели, не для человека. Нужно объяснить когда использовать эту функцию, что она возвращает, какие есть ограничения. Я перестал писать общие фразы типа "работает с файлами" и начал быть конкретным: "создаёт новый markdown файл в указанной директории, возвращает полный путь или ошибку если файл уже существует". Схема параметров через Zod даёт не только валидацию но и документацию. Каждому полю добавляю describe() с пояснением что туда передавать. Модель читает эти описания и формирует правильные аргументы.
Идемпотентность инструментов экономит нервы при повторных вызовах. Если create_note с тем же названием вызвать дважды - первый раз создаст файл, второй вернёт ошибку "уже существует" или просто вернёт путь к существующему. Я добавил флаг overwrite для явного контроля поведения. Композиция сложных операций из простых инструментов работает лучше чем один универсальный tool. Вместо manage_project с кучей параметров делаешь create_project, add_member, update_settings - модель сама выстраивает последовательность вызовов. Дебажить проще, ошибки локализуются, переиспользовать легче. Возвращаемые данные должны быть структурированными и полными. Не просто "успех" или "ошибка", а детальная информация что произошло. Я возвращаю объекты с полями status, message, data. Модель может использовать эти данные для формирования ответа пользователю или принятия решений о следующих действиях. Обработка длительных операций требует асинхронности без блокировки. Если инструмент обрабатывает большой файл или делает медленный API-запрос - возвращаешь job_id сразу, процесс идёт в фоне. Отдельный инструмент check_job_status проверяет прогресс. Модель может периодически опрашивать статус и информировать пользователя. Логирование внутри инструментов помогает отследить что пошло не так. Я логирую входные параметры, промежуточные состояния, результаты. При ошибке вижу полный контекст - когда вызвали, с какими аргументами, на каком шаге упало. Это особенно критично для production где воспроизвести баг сложно. Тестирование инструментов делаю через mock-версии внешних зависимостей. База данных заменяется на in-memory объект, API-вызовы на фейковые ответы. Проверяю что инструмент корректно обрабатывает успешные сценарии, разные типы ошибок, edge cases типа пустых строк или огромных чисел. Автоматические тесты прогоняю перед каждым коммитом. Управление состоянием и контекстомПервая неделя работы с MCP научила меня жёсткому уроку - протокол stateless по дизайну. Каждый запрос самодостаточен, сервер не обязан помнить что было раньше. Это прекрасно для масштабирования и простоты, но ужасно когда нужно сохранить открытое соединение с базой или кэшировать результаты между вызовами. Я сделал сервер для работы с PostgreSQL который при каждом запросе открывал новое соединение - connection pool переполнялся за минуту под нагрузкой. Состояние приходится хранить явно. Простейший вариант - Map в памяти процесса. Ключ - идентификатор сессии или клиента, значение - объект с данными. Работает для stdio где каждое соединение это отдельный процесс, всё живёт и умирает вместе. Но для HTTP-сервера обслуживающего много клиентов нужна явная изоляция по сессиям.
Контекст для модели отличается от состояния сервера. Это информация которую модель должна помнить между сообщениями в рамках диалога. Пользователь спросил "покажи последние файлы", модель вызвала list_files и получила список. Дальше юзер говорит "открой первый" - модель должна помнить что было в том списке. MCP не даёт memory из коробки, это ответственность клиента или сервера. Я реализовал контекст через structured messages. При вызове инструмента возвращаю не просто текст а объект с metadata. Клиент может сохранить эту мету и передать при следующем запросе если модель её запросит. Получается явный контекст управляемый приложением.
Lifecycle управление критично для корректного освобождения ресурсов. При завершении сессии нужно закрыть БД-соединения, очистить кэш, завершить фоновые таски. Stdio-процессы умирают сами, но HTTP-серверы требуют явной логики cleanup при отключении клиента. Конкурентный доступ к состоянию создаёт race conditions если не синхронизировать. Два запроса параллельно модифицируют один cache entry - один затирает изменения другого. Async mutex или queue на критические секции решают проблему но добавляют сложность. Я стараюсь избегать shared mutable state где возможно, делая операции идемпотентными или используя copy-on-write. Кэширование контекста и оптимизация памятиМой первый production MCP сервер сожрал всю память на машине за три часа работы. Начинал с 50MB, через час уже 500MB, к концу дня Node.js процесс весил 2.5GB и система начала свопить. Оказалось - я кэшировал абсолютно всё без ограничений и очистки. Каждый запрос добавлял данные в Map, ничего не удалялось. Классический memory leak который учат избегать на первом курсе, но я умудрился наступить на грабли в боевом коде. Кэширование в MCP серверах балансирует между производительностью и потреблением памяти. Без кэша каждый запрос лезет в базу или файловую систему - медленно. С безлимитным кэшем процесс раздувается до гигабайтов - тоже плохо. Нужна стратегия которая держит горячие данные в памяти и выкидывает холодные. LRU (Least Recently Used) - первое что приходит в голову. Фиксированный размер кэша, выкидываешь то что давно не использовалось. Проблема в том что size ограничивается количеством записей, а не объёмом памяти. Сто маленьких объектов и сто огромных - разница на порядки, но LRU видит просто "100 элементов". Я написал size-aware кэш который отслеживает примерный объём данных:
TTL (Time To Live) дополняет LRU стратегию. Данные протухают через время независимо от частоты использования. Я ставлю разные TTL для разных типов: конфиги живут минуты, списки файлов секунды, результаты тяжёлых вычислений часы. Многоуровневый кэш разделяет горячие и холодные данные. L1 - маленький in-memory кэш для самого частого, L2 - Redis для теплых данных, L3 - файловая система или S3 для архива. Читаешь последовательно уровни, пишешь во все одновременно. Сложнее в реализации но гибче одноуровневой схемы. Слабые ссылки (WeakMap в JS) автоматически очищают память когда объект больше не нужен. Но работают только для объектов как ключей, строки и числа не подходят. Я использую их для кэширования промежуточных результатов внутри одного запроса - после завершения обработки всё очищается само. Lazy loading откладывает загрузку тяжёлых данных до момента реального использования. Вместо того чтобы читать весь файл в память при старте, держишь только метаданные и читаешь содержимое по требованию. Экономит память когда половина ресурсов не нужна большую часть времени. Profiling показывает где реально течёт память. Chrome DevTools для Node.js умеет снимать heap snapshots - видишь что занимает место, какие объекты не освобождаются. Запускаю сервер под нагрузкой, беру несколько снапшотов через интервалы, сравниваю что растёт. Обычно виноваты event listeners которые забыли отписать или замыкания держащие большие объекты. Мониторинг потребления памяти в production обязателен. process.memoryUsage() в Node.js показывает heapUsed и external память. Я логирую это каждую минуту и строю графики - видно когда начинается утечка задолго до краха системы. Alert при превышении порога даёт время разобраться до того как всё упадёт.Подводные камни и решения реальных проблемДокументация MCP выглядит просто и понятно - запустил процесс, отправил JSON, получил ответ. Первые два сервера я написал за вечер и они даже работали. Но стоило запустить третий в production с реальной нагрузкой, как посыпалось всё. Зависания без причины, потерянные сообщения, случайные падения. Следующие две недели я провёл в дебаггере и логах, вылавливая баги которые никто не описывает в туториалах. Буферизация stdio - первая ловушка куда попадают все. Операционная система не гарантирует что write() в stdout доставит данные атомарно. Большое сообщение разбивается на куски, приходит частями. Парсер на другой стороне получает половину JSON, пытается распарсить, падает с SyntaxError. У меня это проявилось при отправке больших ответов - результаты поиска с сотнями файлов. Первые несколько работали, потом начинались непредсказуемые ошибки. Решение - буферизовать чтение до символа новой строки. Не парсишь что пришло сразу, а копишь в строку пока не увидишь \n. Звучит очевидно, но SDK от Anthropic это не делает автоматически, приходится оборачивать самому:
Фикс - никогда не делать синхронных вызовов между сервером и клиентом внутри обработки запроса. Либо вся логика на стороне сервера, либо клиент сам выстраивает последовательность вызовов. Если нужна сложная оркестрация - возвращай промежуточный результат с указанием что делать дальше, пусть клиент решает. Кодировки съедают время на отладку. Файл с кириллицей или эмодзи читается в UTF-8, пишется в stdout как Buffer, но где-то по пути превращается в Latin-1 и всё ломается. JSON.stringify не всегда корректно обрабатывает специальные символы если не указать явно encoding. Я потерял день выясняя почему русские заметки отображаются кракозябрами пока не добавил везде { encoding: 'utf8' }:
Windows создаёт специфичные проблемы с путями и permissions. Backslashes в путях, case-insensitive файловая система, права доступа работают иначе чем в Unix. Я делаю кросс-платформенные пути через path.join() и path.resolve(), всегда нормализую перед использованием. Для критичных мест добавил явные проверки ОС:
Производительность и масштабированиеПервый раз задумался о масштабировании когда мой MCP сервер начал обслуживать двадцать человек одновременно. До этого тестировал на себе - работало отлично. Но стоило подключить команду, начались тормоза. Запросы висели секундами, Claude Desktop жаловался на таймауты, пользователи матерились в чате. Оказалось - я запускал отдельный stdio-процесс на каждого клиента, каждый тянул свою копию в памяти, и железо просто не справлялось. Stdio-транспорт имеет естественный предел масштабирования - количество процессов которые может поднять система. Linux комфортно живёт с сотнями процессов, но каждый жрёт базовые 50-100MB памяти плюс overhead на контекст. Сто клиентов - это 5-10GB только на процессы серверов, не считая самих приложений. На моём лаптопе с 16GB это критично, на сервере с 64GB терпимо, но не бесконечно. HTTP-сервер масштабируется иначе - один процесс обслуживает всех. Node.js крутит event loop, обрабатывает запросы асинхронно, память растёт линейно с количеством активных соединений. Тысяча клиентов - это тысяча открытых SSE-стримов, каждый по паре килобайт буфера. Держится в пределах гигабайта если правильно настроить. Горизонтальное масштабирование HTTP решается классически - ставишь nginx перед серверами, раскидываешь запросы round-robin или по нагрузке. Проблема в состоянии - если сессия привязана к конкретному инстансу, нельзя просто перекинуть клиента на другой. Я вынес state в Redis, теперь любой сервер видит данные любого клиента. Latency выросла на пару миллисекунд но масштабируемость появилась.
Connection pooling критичен когда сервер работает с базами или внешними API. Открывать новое соединение на каждый запрос - тратить десятки миллисекунд на TCP handshake и аутентификацию. Пул держит соединения открытыми, переиспользует между запросами. Я настроил pool size под реальную нагрузку - замерил сколько параллельных запросов бывает одновременно, добавил 20% запаса.
perf на Linux показало что половина времени уходит на markdown-парсинг - вынес в worker thread, latency упала вдвое.Метрики собираю не для красоты а для реальной диагностики. RPS, p95 latency, error rate, memory usage, CPU load - базовый набор. Дашборд в Grafana показывает что происходит в реальном времени. Когда latency взлетает - вижу коррелирует ли это с ростом RPS или проблема в конкретном инструменте. Алерты при превышении порогов дают время среагировать до полного краха. Кэширование на уровне балансировщика экономит ресурсы серверов. Идемпотентные GET-запросы кэшируются nginx'ом или CDN'ом - сервер даже не получает повторные вызовы. Но для MCP это работает редко, большинство запросов уникальны или имеют side effects. Обработка ошибок и восстановлениеМой первый production MCP сервер падал три раза в день без объяснений. Смотрю логи - пусто, смотрю stderr - пусто, только Claude Desktop молча отключается и всё. Выяснилось через неделю - я не обрабатывал исключения нигде, любая ошибка убивала процесс. База данных не ответила вовремя - упал. JSON не распарсился - упал. Файл не нашёлся - упал. Пришлось переписывать с нормальной обработкой ошибок на всех уровнях. Ошибки в MCP делятся на категории по источнику и способу обработки. Валидационные - когда клиент прислал кривые параметры, неправильный формат, отсутствующие поля. Их ловить легко, Zod делает за тебя. Инфраструктурные - БД недоступна, сеть упала, диск заполнен. Тут нужна retry-логика и fallback на альтернативные источники. Бизнес-логические - пользователь пытается удалить несуществующую запись или обновить заблокированный ресурс. Возвращаешь понятное сообщение что не так. JSON-RPC определяет стандартные коды ошибок но их недостаточно. Код -32602 означает invalid params - понятно что параметры кривые, но какие именно? Я добавляю в поле data детали: какой параметр не прошёл валидацию, что ожидалось, что пришло. Модель видит это и может скорректировать запрос.
Graceful degradation означает что сервер продолжает работать с ограниченной функциональностью когда что-то сломалось. База недоступна - отдаём данные из кэша с пометкой что они могут быть устаревшие. API не отвечает - используем локальные fallback-значения. Полная остановка только когда работать совсем невозможно. Я добавил health check эндпоинт который проверяет состояние зависимостей и возвращает статус сервера. Клиент периодически дёргает его и видит если что-то сломалось. Можно переключиться на резервный сервер пока основной чинится. В stdio это не работает, но для HTTP критично. Логирование ошибок делаю на разных уровнях. Error для реальных проблем требующих внимания, warn для временных сбоев которые зарезолвились, info для контекста. Структурированный JSON-формат позволяет парсить логи автоматически и строить алерты по паттернам. Когда error rate превышает порог - получаю уведомление в Slack. Версионирование и обратная совместимостьВерсионирование в MCP кажется простым пока не столкнёшься с реальностью. Протокол обновился, добавили новые capabilities, а у половины пользователей старые клиенты которые новую функциональность не понимают. Мой сервер упал при первом же запросе от обновлённого Claude Desktop - он прислал поля которых я не ожидал, валидация сломалась, всё встало. Версия протокола указывается в initialize request как строка формата YYYY-MM-DD. Anthropic обновляет спецификацию, меняя дату. Клиент говорит какую версию поддерживает, сервер проверяет совместимость. Но реально версия это не гарантия - старые клиенты могут использовать новый протокол частично, новые могут игнорировать устаревшие фичи. Полагаться только на строку версии наивно. Capabilities решают проблему гибче. При handshake стороны обмениваются списками возможностей которые поддерживают. Клиент может сказать "умею работать с resources и tools, промпты не поддерживаю". Сервер отвечает "у меня есть все три". Дальше общаются на пересечении - только то что понимают обе стороны. Я проверяю capabilities вручную и отключаю несовместимую функциональность:
Семантическое версионирование самого сервера помогает коммуницировать изменения. Major - breaking changes в API инструментов. Minor - новые инструменты или параметры обратно совместимые. Patch - багфиксы без изменений интерфейса. Публикую changelog с каждым релизом, описывая что поменялось и как мигрировать. Тестирование на разных версиях клиентов обязательно перед релизом. Я держу несколько версий Claude Desktop в Docker-контейнерах - последнюю стабильную, предыдущую, бета если есть. Прогоняю набор тестов на каждой, проверяю что ничего не сломалось. Звучит параноидально но спасло от публичного позора пару раз. Fallback механизмы активируются когда клиент не поддерживает новую фичу. Вместо ошибки возвращаю результат в старом формате или упрощённую версию функциональности. Модель может не получить все данные но хотя бы что-то работает. Graceful degradation важнее полного отказа. Первые три MCP сервера которые я написал, работали ровно до тех пор пока я не начал их реально использовать. В тестах всё было идеально, а в бою сыпались ошибки которых я не ожидал. Сейчас расскажу про грабли на которые наступают почти все новички, включая меня самого. Логирование в stdout убивает протокол мгновенно. Это ошибка номер один. Написал console.log("Server started") для отладки - сервер сразу перестал отвечать. Всё потому что stdout занят JSON-RPC сообщениями, любой посторонний вывод туда ломает парсинг на другой стороне. Клиент получает строку "Server started" вместо валидного JSON и падает с ошибкой. Использовать нужно только stderr для логов или писать в файл. Я потерял час выясняя почему мой сервер "не запускается", а оказалось просто забыл убрать один console.log из прошлого коммита.Не обработанные исключения роняют процесс бесшумно. Особенно коварно на Windows где нет нормальных логов по умолчанию. Инструмент упал с ошибкой - весь сервер завершился, Claude Desktop просто отключился без объяснений. Добавление глобальных обработчиков process.on('uncaughtException') и process.on('unhandledRejection') спасло кучу нервов:
./config.json не найдётся никогда. Использовать надо либо абсолютные пути через __dirname в Node.js, либо переменные окружения, либо передавать пути явно через конфиг клиента. У меня сервер молча стартовал но не находил файлы настроек - оказалось искал их не там.Забыл закрыть соединение с базой. Открыл connection в начале обработки инструмента, получил данные, вернул результат. Соединение осталось висеть. Через десять вызовов connection pool переполнился, дальнейшие запросы зависают в ожидании свободного слота. Использовать try-finally или async context обязательно для гарантированного освобождения ресурсов. Синхронные операции блокируют event loop. `fs.readFileSync()` вместо async версии останавливает обработку всех других запросов. Один клиент читает большой файл секунду - все остальные тупо ждут. В Node.js всё должно быть асинхронным, блокирующих вызовов избегать как чумы. Я поймал это только профайлером, когда увидел что сервер простаивает в чтении файла пока остальные запросы висят в очереди. Валидация параметров только через схему недостаточна. Zod проверит что filename это строка, но не проверит что это не ../../etc/passwd. Path traversal и подобные атаки нужно отлавливать вручную. Я добавляю явные проверки на подозрительные паттерны после схемной валидации:
Большинство этих ошибок выявляются только при реальном использовании, юнит-тесты их не ловят. Поэтому я всегда гоняю новый сервер под нагрузкой в тестовом окружении перед деплоем, имитируя поведение реальных пользователей. Стратегии тестирования MCP серверовТестирование MCP серверов - это не то же самое что тестирование REST API. Первые месяцы я писал обычные юнит-тесты как привык, проверял что функции возвращают правильные значения. В изоляции всё работало идеально, зелёные галочки по всему репозиторию. Запустил с реальным клиентом - половина сломалась. Проблема в том что MCP это не просто набор функций, это протокол взаимодействия между процессами, и тестировать нужно именно это взаимодействие. Юнит-тесты полезны для бизнес-логики внутри инструментов. Проверяешь что парсинг файлов работает корректно, валидация отсекает кривые данные, вычисления дают правильный результат. Но они не ловят проблемы уровня протокола - некорректный JSON-RPC, неправильные типы сообщений, гонки при конкурентных запросах. Я держу coverage юнит-тестов выше 80% для критичной логики, но это лишь базовый уровень проверки. Интеграционные тесты запускают реальный сервер и шлют ему JSON-RPC через stdio. Поднимаешь процесс, отправляешь initialize, ждёшь ответ, вызываешь инструменты. Проверяешь не только результат но и корректность протокольного обмена. Я написал тестовый клиент который имитирует поведение Claude Desktop:
Тестирование ошибочных сценариев критично. Клиент присылает невалидный JSON - сервер должен вернуть корректную ошибку а не упасть. Параметры инструмента не проходят валидацию - понятное сообщение что не так. База данных недоступна - graceful fallback на кэш. Я специально ломаю зависимости в тестах чтобы проверить что сервер не рухнет полностью. Автоматизация через CI прогоняет все тесты при каждом пуше. GitHub Actions или GitLab CI поднимает окружение, запускает юнит и интеграционные тесты, проверяет покрытие, генерит отчёт. Если что-то красное - пулл-реквест не мержится. Это спасло от регрессий десятки раз, когда рефакторинг ломал существующую функциональность незаметно. Ручное тестирование с реальным Claude Desktop остаётся финальной проверкой перед релизом. Автоматика не поймает UX проблемы - непонятные сообщения ошибок, медленный отклик, странное поведение в edge cases. Я трачу полчаса на реальную сессию с новой версией, пытаюсь сломать типичными действиями. Иногда нахожу баги которые никакой тест не выявил бы. Демонстрационное приложение: полнофункциональный MCP серверСобрал всё что описал выше в один работающий сервер - систему управления проектами с полным набором функций. Не игрушечный пример а реальное решение которое можно использовать для отслеживания задач, управления документацией и мониторинга статуса проектов. Делал его для себя изначально, потом понял что отличная демонстрация всех возможностей MCP. Архитектура построена на модульном принципе. Три основных домена - задачи, документы и уведомления. Каждый живёт в своём файле с собственными инструментами и ресурсами. Центральный сервер координирует работу модулей, управляет состоянием, обрабатывает ошибки. Получилось около 800 строк TypeScript кода который демонстрирует практически все паттерны из статьи.
Запускается стандартно - собрал через tsc, добавил в конфиг Claude Desktop, готово. За неделю использования нашёл пару багов которые в тестах не вылезли, но в целом работает стабильно. Обрабатывает десятки запросов в день без утечек памяти или падений. Это реальный рабочий инструмент, не просто демонстрация возможностей протокола. Модуль документации показывает работу с файлами и полнотекстовым поиском. Храню документы в markdown, индексирую содержимое для быстрого поиска, версионирую изменения. Поиск реализовал через простой inverted index - собираю все слова из документов, для каждого слова храню список файлов где оно встречается. Работает быстрее чем grep по всей директории на каждый запрос. [TS] // src/modules/docs.ts export const docModule = { register(server: any, state: any, logger: any) { server.tool( "search_docs", "Полнотекстовый поиск по документации проекта", { query: z.string().min(2) .describe("Поисковый запрос"), limit: z.number().min(1).max(50).default(10) .describe("Максимум результатов") }, async (args) => { logger.info('Searching docs', { query: args.query }); // Ищем в индексе const results = await state.docs.search(args.query, args.limit); if (results.length === 0) { return { content: [{ type: "text", text: Документы по запросу "${args.query}" не найдены}] }; } const formatted = results.map((r: any) => `${r.title} (релевантность: ${Math.round(r.score * 100)}%) ${r.excerpt}... Как задеплоить проект на гитхаб, если у меня есть фронтенд часть на реакте и бэкенд часть на nodeJS? Что такое router-view и как его переделать на nuxt ? Что такое метод super и как его использовать? “Сжать” список, переместив все ненулевые элементы в левую часть списка, не меняя их порядок, а все нули - в правую часть В PyCharm'e при рандоной перемещке часть букв - на одной строке, часть - на другой Создать сервер, запустить сервер и файл index.html Найдите ближайшее большее число m такое, что сумма его цифр была строго больше суммы цифр числа n Массив чисел, где есть такое число, что его сумма цифр равна сумме чисел после него Что такое stretch и с чем его едят? Найти наименьшее натуральное число Q такое, что произведение его цифр равно заданному числу N Возможно ли такое? Что бы на сайте был не только on-line калькулятор, но и что бы и формировались печатные формы. Django: Что это такое вообще? Что я пропустил в изучении Python? | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||


