Каталог готовых MCP-серверов закрывает типовое: файлы, гит, браузер, популярные SaaS. Внутренние системы в нём не появятся никогда — ни ваша админка, ни очередь задач, ни база, в которой лежит ответ на половину вопросов команды.

Хорошая новость: свой сервер — это не «реализовать протокол», а «обернуть готовую функцию». Библиотека берёт на себя JSON-RPC, а от вас требуется описать, что умеет ваша система. Если нужен контекст, что такое MCP и зачем он вообще, он есть в отдельной статье про MCP-серверы — здесь сразу практика.

Шаг 1. Решить, что выносить

Три примитива протокола различаются тем, кто принимает решение об использовании.

ПримитивКто вызываетДля чего
Tools (инструменты)МодельДействия: найти, создать, отправить, посчитать
Resources (ресурсы)Клиентское приложениеПассивные данные для контекста: документы, схемы, выгрузки
Prompts (промпты)ПользовательЗаготовки сценариев, обычно слэш-команды в интерфейсе

Большинство первых серверов состоит из одних инструментов, и это нормально. Правила, которые экономят время потом:

  • Один инструмент — одна операция. get_order_status лучше, чем универсальный orders с полем action. Модель выбирает по описанию, и чем уже назначение, тем точнее выбор.
  • Начинайте с чтения. Первая версия, которая умеет только смотреть, снимает большую часть рисков и сразу приносит пользу.
  • Не выносите то, что агент может сделать сам. Если данные лежат в файле репозитория, отдельный инструмент не нужен.

Шаг 2. Сервер на Python

Официальный SDK ставится вместе с CLI-обвязкой:

uv init orders-mcp
cd orders-mcp
uv venv
source .venv/bin/activate
uv add "mcp[cli]"

Минимальный сервер целиком:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("orders")

@mcp.tool()
async def get_order_status(order_id: str) -> str:
    """Вернуть статус заказа по его идентификатору.

    Args:
        order_id: идентификатор заказа в формате ORD-12345
    """
    order = await db.fetch_order(order_id)
    if order is None:
        return f"Заказ {order_id} не найден"
    return f"Статус: {order.status}, обновлён {order.updated_at:%d.%m.%Y}"

if __name__ == "__main__":
    mcp.run(transport="stdio")

Здесь важны две вещи, которые легко упустить. Аннотации типов превращаются в JSON-схему аргументов, поэтому order_id: str — не украшение, а часть контракта. Докстринг уходит в описание инструмента и читается моделью: это, по сути, кусок промпта, и писать его надо для читателя, который видит вашу систему впервые.

Ошибки возвращайте текстом, а не исключением. Строка «Заказ не найден» позволит модели объяснить пользователю, что произошло; необработанное исключение просто оборвёт вызов.

Шаг 3. То же на TypeScript

npm install @modelcontextprotocol/server zod
npm install -D @types/node typescript
import { McpServer } from '@modelcontextprotocol/server'
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio'
import { z } from 'zod'

const server = new McpServer({ name: 'orders', version: '1.0.0' })

server.registerTool(
  'get_order_status',
  {
    description: 'Вернуть статус заказа по его идентификатору',
    inputSchema: { orderId: z.string().describe('Идентификатор вида ORD-12345') },
  },
  async ({ orderId }) => {
    const order = await fetchOrder(orderId)
    return {
      content: [{
        type: 'text',
        text: order ? `Статус: ${order.status}` : `Заказ ${orderId} не найден`,
      }],
    }
  },
)

const transport = new StdioServerTransport()
await server.connect(transport)

Схема аргументов описывается через zod, а .describe() на каждом поле — то же самое, что докстринг в Python: подсказка модели, как заполнять аргумент.

Шаг 4. Подключить к клиенту

Локальный сервер общается с клиентом через stdio: клиент сам запускает процесс и разговаривает с ним через стандартные потоки. В конфигурации клиента это выглядит так:

{
  "mcpServers": {
    "orders": {
      "command": "uv",
      "args": ["--directory", "/absolute/path/orders-mcp", "run", "server.py"],
      "env": { "ORDERS_API_TOKEN": "..." }
    }
  }
}

Пути обязаны быть абсолютными — относительные не резолвятся, и это причина примерно половины сообщений «сервер не подключился». Файл конфигурации Claude Desktop лежит в ~/Library/Application Support/Claude/claude_desktop_config.json на macOS и в %APPDATA%\Claude\claude_desktop_config.json на Windows; после правки клиент нужно полностью перезапустить.

Если сервер не появился, смотрите логи: на macOS это ~/Library/Logs/Claude/mcp*.log, куда попадает и весь stderr вашего процесса.

Шаг 5. Отладка в Inspector

Не подключайте сырой сервер к боевому клиенту — это худший способ узнать, что схема аргументов кривая. Для проверки есть референсный инструмент, он же клиент:

npx @modelcontextprotocol/inspector uv --directory /absolute/path/orders-mcp run server.py

Команда поднимает веб-интерфейс, где видно список инструментов, их схемы, вызовы с произвольными аргументами и трафик протокола. У того же пакета есть режим --cli для скриптов и CI и --tui для терминала; для запуска нужен Node 22.19 или новее.

Проверьте по шагам: инструмент виден в списке, схема совпадает с ожиданиями, вызов с правильными аргументами возвращает результат, вызов с мусорными — понятную ошибку.

Отдельная классика для stdio-серверов: любой print в стандартный вывод ломает протокол, потому что вывод занят сообщениями. Логи пишите в stderr.

Шаг 6. Локально или по сети

Stdio подходит, когда сервер живёт на той же машине, что и клиент: личные инструменты, доступ к локальным файлам, обёртки вокруг CLI. Плюс в простоте — процесс запускает клиент, аутентификация не нужна.

Удалённый сервер по HTTP нужен, когда доступ требуется команде, сервер ходит во внутренние системы или его нужно обновлять централизованно. Здесь добавляются авторизация, ограничение доступа и всё, что положено сетевому сервису. Начинать проще со stdio и переносить в сеть, когда инструмент себя оправдал.

Безопасность и границы

Спецификация MCP (актуальная версия — 2026-07-28) отдельно оговаривает: инструменты означают выполнение произвольного кода, их описания недоверенные, а вызовы должны подтверждаться пользователем. К этому добавляется практика:

  • Отдельные права. Своя учётная запись и свой токен для сервера, только нужные операции. Сервер работает с правами того, кто его запустил.
  • Читать раньше, чем писать. Инструменты, которые что-то меняют, добавляйте отдельным шагом и осознанно.
  • Валидация на стороне сервера. Аргументы приходят от модели, а значит, косвенно от текста, который она прочитала. Проверяйте их так же, как ввод из внешней формы.
  • Секреты через окружение. Никаких ключей в коде сервера и в описаниях инструментов.
  • Аудит. Логируйте вызовы: кто, что, с какими аргументами. При разборе инцидента это единственный источник правды.

Вложить сюда стоит ровно столько же внимания, сколько в обычный внутренний сервис. MCP не добавляет магии — он добавляет ещё одного клиента вашей системы, и клиент этот действует по подсказкам из текста.

Частые ошибки

  • Описания в стиле «делает запрос». Модель выбирает инструмент по описанию: чем оно конкретнее, тем реже выбирается не то.
  • Один инструмент-комбайн. Универсальный вызов с полем action заставляет модель угадывать, а вас — разбирать, почему она угадала неверно.
  • Возврат сырого JSON на сотни килобайт. Ответ попадает в контекст. Отдавайте то, что нужно для решения, а не всю запись из базы.
  • Относительные пути в конфигурации. Самая частая причина «сервер не запускается».
  • Логи в stdout. Молчаливая поломка stdio-протокола.
  • Сразу писать в прод. Первая версия — только чтение, на тестовом контуре.

Что дальше

Когда базовый сервер работает, есть куда расти: в протоколе появились расширения — асинхронные задачи для долгих операций, интерактивные элементы интерфейса внутри диалога, структурированные инструкции для агентских сценариев. Всё это опциональное и согласуется при подключении, так что начинать с них не нужно.

Полезнее другое: посмотреть, как устроены зрелые серверы под похожие задачи. Разбор рабочих сборок и критериев выбора — в подборке MCP-серверов, а чтобы агент осмысленно пользовался вашими инструментами, ему пригодятся правила проекта — там же описано, куда их класть для Claude Code, Cursor и Copilot.

В IT-ХОЗЯЕВА такие интеграции разбирают на еженедельном воркшопе по vibe coding и в закрытых AI-чатах: приносите свою систему — вместе решим, что стоит выносить в инструменты, а что оставить за периметром. Разобрать архитектуру предметно можно с ментором из тарифа ХОЗЯИН за 2000 ₽/мес.