Главная Документация Создание сервера

Как создать свой MCP-сервер

За 15 минут — от пустой папки до сервера, который Claude может вызывать.

Эта страница адаптирована из официальной документации MCP — здесь упрощённая версия с фокусом на главное. Полный туториал и SDK для других языков:

modelcontextprotocol.io/docs/develop/build-server

Из чего состоит MCP-сервер

У MCP-сервера есть три типа «способностей». Большинство серверов реализует только первый — инструменты (tools). Остальные — опциональны.

Tools — инструменты

Функции, которые AI может вызвать (с разрешения пользователя). Например: «получить погоду в Москве», «прочитать строку из базы», «отправить запрос к API».

Resources — ресурсы

Файловидные данные, которые клиент может прочитать сам: содержимое файла, результат API-запроса, схема таблицы. Используются реже инструментов.

Prompts — шаблоны

Заранее написанные промпт-шаблоны для типовых сценариев — пользователь выбирает их вручную в клиенте.

В этом руководстве мы сделаем простой сервер с одним инструментом. Этого достаточно, чтобы Claude увидел и вызвал вашу логику.

Что вам понадобится

  • Python 3.10+ (если выберете путь Python) и SDK mcp ≥ 1.2.0
  • Node.js 16+ (если выберете путь TypeScript) и пакет @modelcontextprotocol/sdk
  • Установленный Claude Desktop — чтобы протестировать сервер локально

Сервер на Python (рекомендуется для старта)

Будем использовать FastMCP — обёртку над SDK, которая автоматически генерирует схему инструментов из типов и докстрингов Python. Никакой ручной регистрации.

1. Установка зависимостей (используем uv — быстрый менеджер пакетов)

# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

uv init weather && cd weather
uv venv && source .venv/bin/activate
uv add "mcp[cli]" httpx

2. Минимальный сервер weather.py

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("weather")

@mcp.tool()
def hello(name: str) -> str:
    """Поприветствовать пользователя по имени."""
    return f"Привет, {name}!"

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

Декоратор @mcp.tool() превращает любую Python-функцию в MCP-инструмент. Имя функции становится именем инструмента, docstring — описанием, аннотации типов — схемой входных параметров.

3. Запуск

uv run weather.py

Сервер слушает stdio и ждёт соединения от MCP-клиента. Если видите окно без ошибок — всё в порядке.

Сервер на TypeScript

Альтернативный путь — официальный SDK на TypeScript. Удобно, если у вас уже npm-проект или вы хотите завернуть существующий REST API.

1. Установка

mkdir weather && cd weather
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript
mkdir src && touch src/index.ts

2. Минимальный сервер src/index.ts

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const server = new McpServer({ name: "weather", version: "1.0.0" });

server.registerTool(
    "hello",
    {
        description: "Поприветствовать пользователя по имени",
        inputSchema: { name: z.string().describe("Имя пользователя") }
    },
    async ({ name }) => ({
        content: [{ type: "text", text: `Привет, ${name}!` }]
    })
);

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

registerTool принимает имя, схему параметров через Zod и async-обработчик. Ответ должен быть в формате { content: [{ type: "text", text: "..." }] }.

3. Сборка и запуск

npx tsc
node build/index.js

Также понадобится tsconfig.json с target ES2022 и module Node16 — полный пример в репозитории SDK.

Подводный камень: логирование

В транспорте stdio любая запись в stdout сломает протокол JSON-RPC. Это самая частая причина «сервер не отвечает».

  • В Python используйте print(..., file=sys.stderr) или модуль logging. Обычный print() — запрещён.
  • В TypeScript используйте console.error() — он пишет в stderr. console.log() сломает сервер.
  • Для HTTP-транспорта (как у серверов MCPBay) это правило не действует — stdout безопасен.

Подключение к Claude Desktop

Откройте конфиг (пути см. в разделе «Подключение»). Добавьте свой сервер в секцию mcpServers — для локальных stdio-серверов запись отличается от удалённых.

Запись для Python-сервера

{
  "mcpServers": {
    "weather": {
      "command": "uv",
      "args": [
        "--directory",
        "/абсолютный/путь/к/weather",
        "run",
        "weather.py"
      ]
    }
  }
}

Запись для TypeScript-сервера

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/абсолютный/путь/к/weather/build/index.js"]
    }
  }
}

Замените абсолютный путь на свой. На macOS/Linux получите его командой pwd, на Windows — cd. На Windows используйте двойные обратные слэши или прямые слэши.

После сохранения полностью перезапустите Claude Desktop. Инструмент появится в выпадающем списке под полем ввода.

Что дальше

HTTP-транспорт

Для публичных серверов используйте Streamable HTTP вместо stdio. Это то, что использует MCPBay — сервер живёт по URL, не требует локальной установки клиенту.

MCP Inspector

Официальный отладчик: npx @modelcontextprotocol/inspector. Запускает GUI, где видно все запросы/ответы сервера.

SDK для других языков

Кроме Python и TypeScript есть официальные SDK для Java (Spring AI), Kotlin, C# (Microsoft.Extensions.AI), Rust, Go и Swift. Список и ссылки — в оригинальной документации.

Полная документация

Концепции, спецификация протокола, расширенные возможности (sampling, OAuth, completion):

modelcontextprotocol.io

Как опубликовать свой MCP в каталоге

Публикация — это когда ваш сервер появляется в публичном каталоге MCPBay, чтобы другие люди могли найти его и подключить в своём AI-клиенте — Claude, Cursor, ChatGPT.

Это не то же самое, что кнопка «Добавить MCP сервер»

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

Перед публикацией

Есть два пути. Свой хостинг: рабочий удалённый MCP-сервер, уже запущенный и доступный по публичному HTTPS-адресу — каталог ссылается на ваш эндпоинт. Или хостинг MCPBay: собственная инфраструктура не нужна — при подаче укажите GitHub-репозиторий с Dockerfile, и платформа соберёт и запустит сервер сама (см. раздел «Хостинг на MCPBay» ниже).

Как подать заявку

  1. Откройте форму «Добавить сервер».
  2. Заполните название, URL сервера (ваш MCP-эндпоинт), категорию, короткое описание и подробное описание, а также логотип. Логотип показывается на карточке в каталоге и в кабинетах пользователей.
  3. Хотите хостинг MCPBay? Отметьте «Захостить на mcpbay.pro», укажите ссылку на GitHub-репозиторий в поле «Исходный код» и привяжите GitHub — приложение MCPBay Hosting должно быть установлено на этот репозиторий. Поле «URL сервера» при этом можно оставить пустым — адрес выдаст платформа.
  4. Отправьте заявку. Она уходит на модерацию, и после одобрения сервер публикуется в публичном каталоге.
Открыть форму подачи

Советы для хорошей карточки

  • Понятное название и краткое описание того, что делает сервер.
  • Правильная категория — чтобы те, кто листает каталог, нашли сервер там, где ожидают.
  • Рабочий публичный URL (для серверов на своём хостинге) — модераторы открывают его и проверяют, что сервер отвечает.
  • Логотип — серверы без него показываются с нейтральной заглушкой.

Если вашему MCP нужны личные ключи или токены пользователя: для сервера на хостинге MCPBay объявите их в mcpbay.json с "scope": "user", для остальных понадобится манифест полей. Смотрите руководство по манифесту полей ниже — там описан формат и как его подключить.

Хотите, чтобы пользователи видели в кабинете своё использование вашего сервера? Добавьте интеграцию «Моё использование» ниже — связывание аккаунта и пер-юзерные события использования.

Хостинг на MCPBay

Не хотите держать собственный сервер? Включите хостинг при подаче заявки: платформа соберёт ваш MCP из GitHub-репозитория и запустит его на изолированной инфраструктуре. Сервер получит адрес вида https://имя.run.mcpbay.pro/mcp, а перед ним автоматически встанет OAuth-гейт платформы.

Требования к серверу

  • GitHub-репозиторий с исходниками и Dockerfile — в корне или в папке сервера (см. «Как устроить репозиторий» ниже).
  • Привязанный GitHub-аккаунт и установленное приложение MCPBay Hosting на этот репозиторий — так платформа подтверждает владение и получает доступ к коду (приватные репозитории тоже работают).
  • CMD или ENTRYPOINT в Dockerfile — в exec-форме (JSON-массив, например CMD ["node","build/index.js"]), не в shell-форме.
  • Сервер слушает порт из переменной окружения PORT.
  • MCP-эндпоинт отвечает по пути /mcp (Streamable HTTP).
  • Один долгоживущий процесс — без фоновых демонов, переживающих основной процесс.

Как устроить репозиторий

В форме подачи (и в кабинете при смене репозитория) укажите ссылку на GitHub. Как только GitHub привязан, форма покажет, из какого Dockerfile и какой ветки будет собран сервер; если Dockerfile несколько, его можно выбрать из списка.

Принимаются ссылки:

  • на репозиторий: https://github.com/владелец/репозиторий;
  • на папку сервера: …/tree/ветка/папка — удобно для монорепо;
  • на сам Dockerfile: …/blob/ветка/путь/Dockerfile;
  • SSH-адрес: git@github.com:владелец/репозиторий.git.

Какой Dockerfile соберём, если в репозитории их несколько:

  1. ссылка ведёт на файл — этот файл;
  2. в папке из ссылки лежит файл Dockerfile — он (ссылка без папки — это корень репозитория);
  3. внутри папки из ссылки ровно один Dockerfile (для ссылки на корень — во всём репозитории) — он;
  4. иначе форма попросит выбрать Dockerfile из списка.

Dockerfile — это файлы Dockerfile, Dockerfile с любым окончанием через точку (Dockerfile.prod), файлы с окончанием .Dockerfile (server.Dockerfile) и Containerfile. Папки node_modules, vendor и .devcontainer не просматриваются.

Ветка — та, что в ссылке (…/tree/ветка/…). В ссылке ветки нет — ветка по умолчанию вашего репозитория.

Файл mcpbay.json ищется рядом с Dockerfile, а если там его нет — в корне репозитория.

Ограничение: сборка видит только папку, в которой лежит Dockerfile. Dockerfile в подпапке, который копирует файлы из корня репозитория (общий lock-файл, пакеты рабочей области, ../shared), не соберётся. Сделайте папку сервера самодостаточной или положите Dockerfile в корень.

Манифест хостинга (mcpbay.json)

Положите файл mcpbay.json рядом с Dockerfile (или в корень репозитория) и объявите в разделе env ключи и другие переменные, которые нужны серверу. Для каждой укажите, чей это ключ, — от этого зависит, кто и где его вводит.

{
  "env": [
    { "name": "SERVICE_KEY", "scope": "server", "isRequired": true,
      "description": "Key from the service dashboard (shared by all users)" },
    { "name": "USER_API_KEY", "scope": "user", "isRequired": true, "isSecret": true,
      "description": "Your personal key: Settings → API in your service account",
      "placeholder": "sk_live_..." },
    { "name": "USER_REGION", "scope": "user", "isRequired": false, "isSecret": false,
      "description": "Region of your service account", "placeholder": "eu-west-1" }
  ]
}

Поля переменной — те же, что у переменных в server.json официального реестра MCP, плюс scope. name — имя (A–Z, цифры, _). scope — чей ключ: "server" (по умолчанию) — общий ключ сервера, вы вводите его один раз в кабинете в режиме Developer, раздел «Секреты окружения», и сервер получает его как переменную окружения; "user" — личный ключ, каждый пользователь вводит свой в кабинете в режиме User, а вы его значений не видите. isRequired — true, если без ключа сервер не работает: без общего ключа деплой ждёт в статусе «Ждёт секреты», без личного пользователь получит сообщение, что ключ нужно указать. isSecret — по умолчанию true: значение скрыто при вводе и никогда не показывается повторно; false — для несекретных настроек вроде региона. description — подсказка тому, кто вводит ключ: что это и где его взять. placeholder — пример значения в пустом поле ввода. Старое поле required тоже понимается. Ссылки в description кликабельны: адрес https://… или www.… и markdown-ссылка [текст](https://…) — например, «Ключ выдаётся на [странице API сервиса](https://example.com/api)».

Dockerfile проверяется уже при подаче заявки: CMD/ENTRYPOINT должны быть в exec-форме (JSON-массив). После запуска используйте кнопку «Проверить сейчас» в кабинете, чтобы убедиться, что сервер отвечает и инструменты видны.

Манифест главнее формы: если в mcpbay.json есть раздел env, ключи берутся из него — при подаче заявки и при каждой сборке, а поля ключей в форме не используются. Поменяли ключи — закоммитьте манифест и нажмите «Собрать заново». Нет манифеста или в нём нет раздела env — ключи берутся из формы подачи.

Что происходит после одобрения

  1. Платформа клонирует репозиторий и собирает контейнерный образ на изолированном сборщике.
  2. Образ проходит скан безопасности: критические уязвимости, для которых существует исправление, блокируют выкат.
  3. Сервер запускается в изолированной виртуальной машине и получает поддомен имя.run.mcpbay.pro.
  4. Перед сервером автоматически работает OAuth-гейт: пользователи подключают сервер, входя своим аккаунтом mcpbay.pro в ИИ-клиенте; запросы без токена получают 401. Статистику вызовов инструментов собирает платформа.

Секреты и переменные окружения

Общие ключи вашего сервера (scope "server") задаются в кабинете в режиме Developer — секция «Секреты окружения» в блоке деплоя заявки (появляется после первого деплоя). Значения шифруются, применяются к серверу как переменные окружения и никогда не показываются повторно. Личные ключи пользователей (scope "user") сюда не попадают: каждый вводит свой в своём кабинете, и вам они не видны. Имена с префиксами MCPBAY_ и FLY_, а также PORT, зарезервированы платформой.

Личные ключи в коде сервера

Сервер один на всех пользователей, поэтому личные ключи (scope "user") приходят не в переменных окружения, а в каждом запросе от платформы — в HTTP-заголовке X-Mcpbay-Credentials. Его значение — JSON-объект {ИМЯ: значение}, закодированный в base64url без выравнивания «=». Имена — как в mcpbay.json. В объекте только те ключи, которые пользователь сохранил. Заголовок ставит сама платформа: всё, что клиент прислал в X-Mcpbay-*, вырезается до вашего сервера, а токен пользователя mcpbay до сервера не доходит. Читайте ключи в обработчике инструмента, при каждом вызове:

TypeScript (официальный SDK, версии 1 и 2):

// Works with the official TypeScript SDK v1 (extra.requestInfo.headers)
// and v2 (ctx.http.req.headers).
function mcpbayKeys(ctx: any): Record<string, string> {
  const h = ctx?.http?.req?.headers ?? ctx?.requestInfo?.headers;
  const raw = typeof h?.get === "function"
    ? h.get("x-mcpbay-credentials")
    : h?.["x-mcpbay-credentials"];
  if (typeof raw !== "string" || !raw) return {};
  return JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
}

server.registerTool("search", {
  description: "Search listings",
  inputSchema: { query: z.string() },
}, async ({ query }, ctx) => {
  const apiKey = mcpbayKeys(ctx).USER_API_KEY;
  if (!apiKey) {
    return { isError: true, content: [{ type: "text",
      text: "Add your USER_API_KEY in MCPBay → Connections." }] };
  }
  // call the third-party API with apiKey ...
});

Python (официальный SDK, FastMCP):

import base64, json
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("example")

def mcpbay_keys(ctx: Context) -> dict[str, str]:
    request = getattr(ctx.request_context, "request", None)
    raw = request.headers.get("x-mcpbay-credentials") if request is not None else None
    if not raw:
        return {}
    return json.loads(base64.urlsafe_b64decode(raw + "=" * (-len(raw) % 4)))

@mcp.tool()
async def search(query: str, ctx: Context) -> str:
    api_key = mcpbay_keys(ctx).get("USER_API_KEY")
    if not api_key:
        # FastMCP turns an exception into a tool error (isError).
        raise ValueError("Add your USER_API_KEY in MCPBay → Connections.")
    # call the third-party API with api_key ...

Если у пользователя не задан обязательный личный ключ (isRequired: true), платформа сама ответит на вызов инструмента ошибкой со ссылкой на «Подключения» — ваш сервер не вызывается. Необязательного ключа в заголовке может не быть: обработайте это сами. Список инструментов (tools/list) и рукопожатие приходят без ключей — сервер должен работать и так. Никогда не пишите значения ключей в логи и не возвращайте их в ответах. Общие ключи сервера (scope "server") по-прежнему приходят переменными окружения.

Статус деплоя — сборка, адрес, ошибки — виден в кабинете в карточке вашей заявки.

Имейте в виду

Ёмкость хостинга ограничена — модерация одобряет заявки с учётом свободных мест и может приостановить или снять сервер с хостинга.

Пока хостинг активен, заявку нельзя удалить из кабинета — сначала остановка хостинга через модерацию.

Инструменты и проверки

Сервер показывается в каталоге и на главной, только если у него есть хотя бы один инструмент и он отвечает на проверки платформы. Подать сервер без инструментов и держать его в кабинете можно — он просто не попадёт в каталог, а кабинет объяснит почему.

Как платформа проверяет сервер

По спецификации MCP проверка идёт на уровне протокола: MCP-рукопожатие (initialize) и список инструментов (tools/list). Платформа никогда не вызывает ваши инструменты и не передаёт ключи API. Если несколько проверок подряд не прошли, сервер скрывается из каталога и возвращается сам после первой удачной; уведомление приходит в обоих случаях. Спящий сервер — не сбой: проверка его будит.

Исправили сервер? Нажмите в кабинете «Собрать заново», если он на хостинге MCPBay, — платформа заберёт свежий коммит с GitHub, соберёт и проверит его, — или «Проверить сейчас», если хостите сами. Без этого сервер на своём хостинге перепроверяется раз в сутки.

Серверы, которым нужны ключи стороннего API

Если сервер работает с платным или закрытым API, для проверок вам не нужны свой ключ и подписка. Сервер должен запускаться и перечислять инструменты вовсе без ключа, а ключ читать только при вызове инструмента. Если ключа нет, верните ошибку инструмента (isError: true) с просьбой добавить ключ — не завершайте процесс и не отказывайте в рукопожатии. Если ключ у каждого пользователя свой, объявите его в mcpbay.json с "scope": "user".

Инструменты в mcpbay.json

Список инструментов платформа узнаёт у самого сервера. Запасной вариант — например, до первой удачной сборки или если сервер не может перечислить инструменты без ключа — раздел «tools» в mcpbay.json (рядом с Dockerfile или в корне репозитория). Ответ самого сервера всегда важнее объявленного списка.

{
  "tools": [
    { "name": "search_listings",
      "title": "Search listings",
      "description": "Find listings by keyword and price range" }
  ]
}

У каждого инструмента обязательны имя (латиница, цифры, _ - ., до 128 символов, без повторов) и описание; title и inputSchema — по желанию. До 200 инструментов. Файл читается при подаче сервера и при каждой сборке.

Сервер не на хостинге MCPBay? Опубликуйте тот же список по адресу https://<ваш-хост>/.well-known/mcp/server-card.json — платформа читает его, когда до сервера не достучаться.

Если сборка упала

У каждого отказа есть код, и блок развёртывания на карточке заявки его объясняет: что произошло, что сделать, полный лог сборки и — если сработал гейт безопасности — конкретные уязвимости. Кнопка «Собрать заново» там же клонирует последний коммит вашей ветки и собирает образ ещё раз.

Гейт безопасности блокирует выкладку только из-за уязвимостей уровня CRITICAL, для которых уже вышло исправление; находки без исправления выкладку не останавливают. Исключений из этого правила нет — чинится это обновлением базового образа или пакета, а не отменой проверки.

Лог сборки хранится ограниченное время и виден в вашем кабинете. Не печатайте секреты во время сборки: всё, что сборка пишет в stdout, попадёт в этот лог.

Сервер запустился, но не отвечает

Контейнер работает, но не отвечает на MCP-рукопожатие по адресу /mcp. Чаще всего сервер падает сразу после старта, слушает не тот порт или требует переменные окружения, которых у него нет.

Откройте логи сервера в кабинете — там видна настоящая причина. Если серверу нужны ключи, которые вводит каждый пользователь, объявите их в манифесте (раздел ниже), а не требуйте на старте: хостимый сервер обязан подниматься без пользовательских ключей.

Уязвимости в образе

Образ собрался, но сканер нашёл критические уязвимости, для которых уже вышло исправление. Такой образ мы не выкладываем.

Обновите базовый образ до свежего тега или поднимите версии пакетов из списка ниже до колонки «Исправлено в», затем соберите заново.

Сборка образа не удалась

Команда сборки завершилась с ошибкой. Причина — в логе сборки.

Откройте лог, найдите первую строку с ошибкой и исправьте её. Проверить локально: docker build .

Не удалось получить код

Платформа не смогла склонировать репозиторий: ветки нет, репозиторий пуст или доступ отозван.

Проверьте, что ветка существует и содержит код, а приложение MCPBay Hosting установлено на репозиторий.

Dockerfile не найден

По ссылке на репозиторий из заявки Dockerfile не найден.

Добавьте Dockerfile в репозиторий или укажите ссылку на папку сервера, где он лежит (…/tree/ветка/папка).

CMD или ENTRYPOINT в shell-форме

Платформа запускает ваш процесс под своим прокси и обязана знать точную команду запуска. Shell-форма её скрывает.

Замените CMD npm start на exec-форму: CMD ["npm","start"].

Небезопасный контекст сборки

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

Удалите ссылку и запушьте изменения.

Нет доступа к репозиторию

Приложение MCPBay Hosting не имеет доступа к репозиторию: его удалили с аккаунта GitHub, исключили из него этот репозиторий или приостановили.

В кабинете нажмите «Переподключить GitHub» и установите приложение MCPBay Hosting на этот репозиторий, затем «Собрать заново». Если репозиторий переехал — смените его в блоке «Репозиторий GitHub».

GitHub отказал в доступе

Не удалось получить токен доступа к репозиторию.

Проверьте установку приложения MCPBay Hosting на репозиторий и соберите заново.

Контейнер не запустился

Образ собран и проверен, но запустить его не удалось — чаще всего контейнер падает сразу после старта.

Убедитесь, что сервер слушает порт из переменной PORT и не завершается сразу после запуска.

Сервер завершается сразу после запуска

Образ выкачен, но процесс сервера выходит через несколько секунд после старта, и платформа перезапускает его, пока не сдастся. Сервер не опубликован; в карточке отказа в кабинете — последние строки, которые он напечатал.

Прочитайте строки лога сервера в карточке отказа — причина обычно там. Если сервер требует ключи, которые вводит каждый пользователь, объявите их в mcpbay.json с "scope": "user", а не требуйте на старте: хостимый сервер обязан подниматься без пользовательских ключей. Свои ключи задайте в разделе «Секреты окружения». Затем нажмите «Собрать заново».

Серверу нужен ключ, чтобы запуститься

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

Сделайте так, чтобы сервер запускался без ключей: отвечал на MCP-рукопожатие и отдавал список инструментов вовсе без ключа, а ключ читал только при вызове инструмента — и если ключа нет, возвращал ошибку инструмента с просьбой его добавить. Ключи, которые вводит пользователь, объявите в mcpbay.json с "scope": "user". Если ключ ваш, а не пользователя, задайте его в разделе «Секреты окружения». Затем нажмите «Собрать заново».

Сервер не отвечает

Процесс запущен, но за три минуты так и не начал принимать соединения на своём порту — запрос до него не доходит. Сервер не опубликован.

Убедитесь, что сервер слушает порт из переменной PORT, а не зашитый в код, и работает по HTTP, а не через stdio. Посмотрите строки лога сервера в карточке отказа, затем нажмите «Собрать заново».

Не хватает переменных окружения

Сервер объявил обязательные переменные, значения которых ещё не заданы.

Задайте значения в разделе «Секреты окружения» ниже — сборка продолжится сама.

Сборка не уложилась во время

Сборка превысила отведённый лимит времени.

Уменьшите образ: лёгкий базовый образ, многоэтапная сборка, .dockerignore.

Манифест ключей: чтобы у пользователя появились поля для токенов

Серверу на хостинге MCPBay этот раздел не нужен: личные ключи объявляются в mcpbay.json с "scope": "user". Ниже — манифест полей для серверов, которые оператор подключает вручную. Это небольшой JSON-файл, который описывает, какие ключи или токены ваш сервер запрашивает у каждого пользователя. С ним пользователь получает форму, куда вставляет свои значения; без него поля для ключей у вашего сервера не появляются.

Когда он нужен

Только если вашему MCP нужны личные ключи или токены пользователя от стороннего сервиса — например, собственный API-ключ пользователя для внешней платформы.

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

Как это работает для пользователя

  1. Манифест объявляет поля, которые нужны вашему серверу.
  2. Каждый пользователь вставляет свои значения в разделе «Подключения» в своём /account.
  3. Значения хранятся в зашифрованном виде, по каждому пользователю отдельно — их никто не видит и они не показываются обратно.
  4. В момент вызова ваш MCP получает их автоматически (store-and-inject).

Нет манифеста — нет и формы для ключей у вашего сервера.

Формат

Один JSON-файл на провайдера, с именем manifests/<provider>.json. Ключи ниже фиксированы; строковые значения в примере — иллюстративные.

{
  "provider": "acme",
  "purpose": "ACME lets each seller manage their own catalog. Enter the API key from your ACME account settings.",
  "fields": [
    {
      "name": "api_key",
      "label": "ACME API Key",
      "kind": "secret",
      "help": "Found in ACME → Settings → API."
    }
  ]
}
  • provider — строчный слаг (шаблон ^[a-z][a-z0-9_-]{1,30}$). Он должен совпадать с полем credential_provider вашей заявки — именно так MCPBay связывает манифест с вашим сервером.
  • purpose — показывается пользователю: что он подключает и зачем.
  • fields[] — по одной записи на каждое запрашиваемое значение. В каждой записи:
  • name — слаг поля (шаблон ^[a-z][a-z0-9_]{0,40}$). Под этим ключом ваш MCP получает значение.
  • label — человекочитаемая подпись, которую видит пользователь рядом с полем.
  • kind — text или secret. secret отображается как маскированное поле-пароль только для записи: после сохранения значение обратно не показывается.
  • help — необязательная подсказка, например где взять ключ.

Советы

  • Объявляйте только действительно нужные поля — собирайте минимум (минимизация данных).
  • Напишите понятные purpose и help, чтобы пользователь точно понимал, что вставлять и где это взять.
  • Для всего чувствительного указывайте kind: "secret" — поле станет маскированным и только для записи.

Как его подключить

Манифесты проходят проверку оператором и поставляются вместе с сервисом. Сейчас оператор вручную связывает ваш манифест с заявкой (выставляет поле credential_provider).

Поэтому отправьте JSON манифеста вместе с заявкой — вставьте его в описание или пришлите через поддержку — и его подключат. Загрузка манифеста прямо через форму подачи пока недоступна.

«Моё использование»: чтобы пользователь видел свою статистику в кабинете

Пока ваш MCP не интегрирован, статистика обезличена, и в кабинете пользователя написано «Аккаунт не связан». После интеграции пользователь связывает аккаунт одноразовым кодом и затем видит своё использование и траты в разделе «Мои MCP» → «Моё использование» — это больше доверия и удержания. Это же — фундамент для будущих платных tools (биллинга).

Два пути линковки

  1. MCP на хостинге MCPBay. Разработчику делать ничего не нужно: платформа сама ставит перед сервером OAuth-гейт своего IdP и собирает статистику использования — пользователь просто входит аккаунтом mcpbay.pro в своём ИИ-клиенте. Привязка аккаунтов и пер-юзерная статистика для размещённых серверов — забота платформы, а не вашего кода.
  2. MCP со своей авторизацией. Если у вашего MCP уже есть собственная пер-юзерная личность (свой OAuth) — используйте device-code трек ниже без изменений: tool get_mcpbay_link_code + billing.create_link_code + usage.record_tool_use.

Как работает связывание (device-code flow)

  1. В кабинете пользователь жмёт «Моё использование» у несвязанного MCP и получает окно «введите код».
  2. В AI-клиенте ассистент вызывает инструмент get_mcpbay_link_code в вашем MCP. Сервер генерирует одноразовый код и возвращает его пользователю.
  3. Пользователь вставляет код в кабинете. MCPBay гасит его и связывает аккаунт mcpbay с личностью внутри вашего MCP.
  4. Дальше события использования, которые пишет ваш MCP, атрибутируются этому пользователю.

Что вы добавляете в свой MCP

  1. Стабильная пер-юзерная личность внутри MCP. external_subject — например claim sub из OAuth. Без неё связывание неприменимо: серверу без пер-юзерного субъекта некого атрибутировать.
  2. Инструмент с именем ровно get_mcpbay_link_code. Кабинет называет пользователю именно это имя, поэтому оно должно совпадать. Инструмент генерирует код из 8 символов алфавита A–Z2–9 (без 0/O/1/I), показывает его как XXXX-XXXX, хранит только SHA-256-hash через billing.create_link_code(...) и возвращает код с краткой инструкцией (ввести на mcpbay.pro в «Моё использование», действует 10 минут).
  3. События использования после каждого вызова. Вызывайте usage.record_tool_use(...) один раз на каждый вызов tool'а. Функция — тихий no-op, пока субъект не связан (а для бесплатных tools — пока сервер в «Мои MCP» пользователя, консент-гейт), поэтому вызывать можно безусловно.

Готовая заготовка (Python / FastMCP)

Вставьте в свой сервер, задайте SUBMISSION_ID и подключите get_current_subject() к своей пер-юзерной личности. Обе SQL-функции живут в общей Postgres MCPBay — роль БД вашего MCP получает на них EXECUTE при подключении (см. примечание ниже).

# ── MCPBay "My usage" integration ─────────────────────────────
# Mint a one-time link code + record per-user tool usage.
import hashlib
import secrets

import asyncpg  # or psycopg — any async Postgres driver
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("your-server")

# Your numeric id from the MCPBay catalog (ask the operator at onboarding).
SUBMISSION_ID = 0  # TODO: replace with your submission id

# Code alphabet: A–Z 2–9 without ambiguous chars (I, L, O, U, 0, 1).
# MCPBay normalises the entered code to ^[A-Z2-9]{8}$, so this is a safe subset.
_ALPHABET = "ABCDEFGHJKMNPQRSTVWXYZ23456789"


def _make_code() -> tuple[str, bytes]:
    """Return (display 'XXXX-XXXX', sha256 hash) — only the hash is stored."""
    raw = "".join(secrets.choice(_ALPHABET) for _ in range(8))
    digest = hashlib.sha256(raw.encode("ascii")).digest()
    return f"{raw[:4]}-{raw[4:]}", digest


async def get_current_subject() -> str | None:
    """Your OAuth identity for the current user (e.g. the 'sub' claim).
    Return a stable string per user, or None if no identity is present."""
    raise NotImplementedError  # TODO: wire to your auth


async def _pool() -> asyncpg.Pool:
    """Your shared MCPBay Postgres pool (DSN from env)."""
    raise NotImplementedError  # TODO: return your asyncpg pool


@mcp.tool()
async def get_mcpbay_link_code() -> str:
    """Get a one-time code to link this account to your mcpbay.pro cabinet.
    The user enters it under "My MCPs" → "My usage". Valid 10 minutes."""
    subject = await get_current_subject()
    if subject is None:
        return "Sign in first, then ask for a link code."
    display, code_hash = _make_code()
    pool = await _pool()
    async with pool.acquire() as conn:
        # Stores only the hash; ttl 1–60 min (default 10). One active code per
        # (submission, subject) — calling again reissues and voids the previous.
        expires_at = await conn.fetchval(
            "SELECT billing.create_link_code($1, $2, $3, $4)",
            SUBMISSION_ID, subject, code_hash, 10,
        )
    return (
        f"Your link code: {display}\n"
        f"Enter it on mcpbay.pro under 'My usage'. Valid ~10 minutes "
        f"(until {expires_at})."
    )


async def record_usage(tool_name: str, status: str, duration_ms: int) -> None:
    """Record one usage event. Silent no-op until the subject is linked —
    call it unconditionally after every tool, success or error."""
    subject = await get_current_subject()
    if subject is None:
        return
    pool = await _pool()
    async with pool.acquire() as conn:
        await conn.execute(
            "SELECT usage.record_tool_use($1, $2, $3, $4, $5)",
            SUBMISSION_ID, subject, tool_name, status, duration_ms,
        )

Запросите подключение к usage-API при размещении

Обе функции под защитой: least-privilege роль БД вашего MCP получает на них EXECUTE и привязывается к вашей заявке (анти-спуфинг привязывает связь к вашей роли, поэтому чужая заявка недостижима). Оператор настраивает это вручную при онбординге — self-service пока нет. Поэтому запросите подключение к usage-API, когда размещаете сервер.

Готовы опубликовать свой сервер?

Подайте сервер в каталог MCPBay — со своим публичным HTTPS-эндпоинтом или сразу с хостингом на инфраструктуре MCPBay.

Подать заявку