MCP-сервер не должен заставлять людей вставлять токен

oauth

В прошлом месяце я перевёл MCP-сервер Kookee на личные токены. Каждый создаёт свой, ассистент действует от имени этого человека, и у каждого изменения видно, кто его сделал. В Claude Code и Cursor это работает хорошо: вставляете токен в одну команду или один файл конфигурации, и готово.

А теперь попробуйте объяснить это человеку, который пишет заметки о релизе в Claude на ноутбуке или разбирает обратную связь с телефона. В чат-приложениях Claude и ChatGPT нет поля для заголовка Authorization. К удалённому MCP-серверу они подключаются одним способом: вы даёте им URL, они отправляют вас на страницу входа и сохраняют сессию. Это OAuth, и до этой недели Kookee его не поддерживал.

Теперь поддерживает. Вы добавляете https://api.kookee.dev/api/mcp в Claude, ChatGPT или VS Code, входите в Kookee, выбираете «Чтение и запись» или «Только чтение», и на этом настройка закончена. Ниже — что я выпустил и какие решения заняли больше времени, чем код.

Как теперь выглядит подключение

На странице MCP-сервера каждого проекта появилась новая карточка с URL и шагами для каждого приложения. Для Claude есть кнопка Добавить в Claude. Она открывает в Claude диалог Add custom connector, где название и URL уже заполнены, так что остаётся только нажать Add.

Карточка «Подключите ИИ-приложение» на странице MCP-сервера: URL коннектора с кнопкой копирования, вкладки Claude, ChatGPT, VS Code и Claude Code, три шага и кнопка «Добавить в Claude»

Затем Claude открывает страницу Kookee. Вы входите или создаёте аккаунт и решаете, что приложению можно делать:

Страница согласия Kookee: «Разрешить Claude использовать ваш аккаунт Kookee?», выбор между «Чтение и запись» (выбрано) и «Только чтение», кнопки «Разрешить» и «Отмена»

Создавать, копировать, хранить и менять токен не нужно. После этого коннектор появляется в Claude в браузере, на компьютере и на телефоне.

Семь решений, из которых сложился OAuth для MCP в Kookee

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

Мой первый план повторял модель токенов: токен принадлежит одному проекту, значит, и подключение тоже. Claude этот план похоронил. Он не даёт добавить второй пользовательский коннектор с тем же URL, так что подключение, привязанное к одному проекту, ограничило бы каждого пользователя Claude одним проектом. Отдельный URL для каждого проекта это обходит, но тогда у человека в пяти проектах пять коннекторов, которые нужно настроить и за которыми нужно следить.

Поэтому подключение принадлежит вашему аккаунту и даёт доступ ко всем проектам, где вы участник, с вашей ролью в каждом. Все инструменты, кроме list_projects, принимают необязательный аргумент project. Если вы в одном проекте, ассистенту он не нужен никогда. Если в нескольких, вы говорите «в проекте Lumen», и ассистент передаёт slug. list_projects нужен на случай, когда ассистенту приходится спросить.

Если проект не указан или указан неверно, сервер не отвечает HTTP-ошибкой. Он возвращает обычный результат инструмента, где сказано, что не так, и перечислены ваши проекты:

JSON
{
  "error": "MCP_PROJECT_REQUIRED",
  "message": "This account belongs to several projects. Pass `project` with the slug of the one the user means.",
  "projects": [
    { "slug": "lumen", "name": "Lumen", "role": "OWNER" },
    { "slug": "lumen-docs", "name": "Lumen Docs", "role": "MEMBER" }
  ]
}

Модель читает это и пробует снова с правильным slug. А ответ 400 от транспорта она воспринимает как сломанный сервер.

2. «Только чтение» выбирают на экране согласия

С токенами доступ выбирают при создании токена. В OAuth страницы с токенами нет, поэтому выбор переехал в момент, когда вы одобряете приложение.

Под капотом эти два варианта — OAuth-скоупы read и write, и выбор «Только чтение» убирает write ещё до того, как приложение получит код. Как и с токенами, подключение только на чтение не просто отклоняет запись: оно вообще не показывает инструменты записи. Модель, которая видит move_feedback, строит вокруг него весь разбор отзывов и спотыкается на последнем шаге. Модель, которая его не видит, сразу говорит вам, что подключение только на чтение.

3. «Отключить» должно значить «отключено»

На это ушло больше всего времени. У OAuth-приложения два токена: токен доступа, который живёт час, и refresh-токен, который приложение обменивает на новые токены доступа. Из коробки обмен refresh-токена в OAuth-сервере, на котором построен Kookee, не проверял, разрешаете ли вы ещё приложению доступ. Если отозвать согласие, приложение всё равно могло бы получать свежие токены доступа.

Поэтому теперь происходят две вещи. При каждом запросе к MCP сервер одной выборкой из базы находит токен доступа, согласие, по которому он выдан, и аккаунт и отклоняет запрос, если чего-то из этого нет или аккаунт отключён. А отключение удаляет согласие, refresh-токены и токены доступа вместе, в одной транзакции. Отключите приложение в Kookee, и его следующий же запрос будет отклонён.

Смена или сброс пароля тоже отключает все приложения. Если вы сбрасываете пароль, потому что что-то пошло не так, меньше всего вам нужен где-то ассистент с рабочей сессией. Гостевые аккаунты подключаться не могут вообще.

4. Токены, которые можно отозвать, а не переждать

Для токенов доступа OAuth часто выбирают JWT: они подписаны, самодостаточны и проверяются без обращения к базе. А ещё их нельзя отозвать до истечения срока. Для ассистента, который может публиковать от вашего имени, «перестанет работать в течение часа» — недостаточно.

В Kookee это случайные строки, которые хранятся только в виде хешей SHA-256, со своими префиксами: mcpa_ у токенов доступа и mcpr_ у refresh-токенов. Сканер секретов или человек, читающий лог, отличит их от личного токена (kkm_) или API-ключа (kk_). Цена — одна выборка из базы на каждый запрос, а её сервер и так делает.

5. Только известные приложения

MCP-клиенты представляются через документ с метаданными клиента: client ID — это URL, и сервер загружает его, чтобы узнать название приложения и куда можно вернуть вас после входа. Схема аккуратная. Но она же значит, что сервер, который принимает любой URL, загрузит всё, на что ему укажут, и заведёт под это клиента ещё до того, как кто-то вошёл.

Поэтому Kookee принимает короткий список: Claude, Claude Code, ChatGPT и VS Code, и узнаёт каждое приложение по адресу, где лежит его документ с метаданными. Тогда экран согласия может написать «Разрешить Claude» и не соврать, а разместить где-то ещё документ, который называет себя Claude, никто не сможет.

У этого есть цена. Cursor регистрируется через динамическую регистрацию клиентов, которую я не стал включать, поэтому пользователи Cursor по-прежнему подключаются по личным токенам. Они работают точно так же, как раньше.

6. Страницу согласия нельзя встроить во фрейм

Страница с кнопкой «Разрешить» — классическая цель кликджекинга: её загружают в невидимый фрейм и подкладывают под кнопку что-нибудь заманчивое. Страница Kookee отправляет Content-Security-Policy: frame-ancestors 'none' и X-Frame-Options: DENY, поэтому ни один другой сайт не может встроить её во фрейм. Это всего два заголовка, и их легко забыть, пока кто-нибудь об этом не напишет.

7. Авторство записывается как раньше

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

Что вы видите в Kookee

Всё, что вы подключили, перечислено в настройках аккаунта, в разделе Безопасность → Подключённые приложения, с уровнем доступа и датой подключения. Чтобы отключить приложение, нужен один клик и подтверждение.

Подключённые приложения в разделе «Настройки → Безопасность»: Claude Code и Claude с доступом «Чтение и запись», VS Code с доступом «Только чтение», у каждого кнопка «Отключить»

Личные токены никуда не делись. Они по-прежнему подходят для Cursor, для скриптов и CI и для .mcp.json, который коммитит вся команда, а теперь собраны в одной карточке на странице MCP-сервера, под новой.

Если у вас свой MCP-сервер

  • Поддерживайте OAuth. Люди в Claude, ChatGPT и других чат-приложениях не могут вставить заголовок.

  • Делайте подключение на человека, а рабочее пространство передавайте аргументом инструмента. Claude не примет один и тот же URL дважды.

  • Вынесите выбор «только чтение» на экран согласия и не показывайте инструменты, которые выданный доступ не разрешает.

  • Проверяйте согласие при каждом запросе и удаляйте refresh-токены при отключении. Не рассчитывайте, что обмен refresh-токена сделает это за вас.

  • Выбирайте непрозрачные токены, которые можно отозвать, а не JWT, которые приходится пережидать.

  • Принимайте список известных URL метаданных клиентов, а не загружайте всё, что пришлёт клиент.

  • Не давайте встраивать страницу согласия во фреймы.

  • Отвечайте на «какой проект?» результатом инструмента, с которым модель может работать, а не HTTP-ошибкой.


MCP-сервер Kookee теперь работает с Claude, ChatGPT и VS Code через вход в аккаунт. Шаги описаны в статье Подключение Claude, ChatGPT или VS Code к Kookee, а что умеет подключённый ассистент — в статье Управление контентом из ИИ-ассистента (MCP).