1. MCP Jam как лаборатория для авторизации
MCP Jam — это не «ещё один странный тул», а ваш лабораторный стенд, который умеет играть роль MCP‑клиента. По сути, это эмулятор поведения ChatGPT при работе с MCP‑сервером: он умеет читать .well-known/oauth-protected-resource, запускать OAuth‑флоу, прикручивать токены к запросам и показывать, что именно пошло не так.
Очень важный практический момент: если вы добились успешного Default OAuth‑флоу в MCP Jam, вы примерно на 80% готовы к интеграции с реальным ChatGPT App. Всё, что делает ChatGPT при привязке аккаунта (линкинге), Jam уже умеет, только с более прозрачными логами и кнопочками.
В прошлой лекции мы настроили базовую авторизацию для нашего учебного MCP‑сервера GiftGenius: выбрали вариант проверки токена (JWT или introspection), реализовали .well-known/oauth-protected-resource и мидлварь, которая защищает инструменты. Сейчас посмотрим, как всё это ведёт себя в MCP Jam в разных режимах авторизации.
Наша цель в этой лекции — научиться:
- осознанно переключать режимы авторизации в Jam (None, Bearer, OAuth with credentials, Default OAuth);
- понимать, что именно Jam отправляет MCP‑серверу в каждом режиме;
- диагностировать, какая часть системы сломалась: MCP Server, Auth Server или метаданные;
- проверить, что защищённые инструменты работают только с токеном, а открытые — и без него.
2. Наш учебный MCP‑сервер: что мы тестируем
Чтобы не говорить абстрактно, коротко напомним контекст. Продолжим историю с нашим учебным приложением GiftGenius — это ChatGPT‑App, который помогает подбирать подарки и показывает пользователю его заказы и вишлисты.
На стороне MCP‑сервера у нас уже есть:
- открытый инструмент, например search_gifts — его можно вызывать анонимно;
- защищённый инструмент, например list_user_orders — он должен работать только для аутентифицированного пользователя и требовать scope mcp:tools.
Сервер умеет:
- публиковать .well-known/oauth-protected-resource;
- проверять токен (JWT или через introspection — вы выбрали один подход в предыдущей лекции);
- извлекать из токена sub (user id), scope, aud и передавать их в обработчики инструментов.
Типичная мидлварь проверки токена в Node.js/TypeScript может выглядеть так:
// middleware/auth.ts
export function requireScope(requiredScope: string) {
return async (req: any, res: any, next: () => void) => {
const header = req.headers["authorization"];
if (!header?.startsWith("Bearer ")) {
res
.status(401)
.set(
"WWW-Authenticate",
`Bearer realm="mcp", resource_metadata="${process.env.BASE_URL}/.well-known/oauth-protected-resource", scope="${requiredScope}"`
)
.json({ error: "unauthorized" });
return;
}
// здесь вы уже проверяете токен (подпись, exp, aud, scope...)
// и кладёте результат в req.user
next();
};
}
Эта мидлварь будет использоваться перед защищёнными инструментами MCP. Если токена нет — мы отдаём 401 и корректный WWW-Authenticate с resource_metadata, как требует спецификация MCP Authorization. Подробный разбор проверки токена и реализации вспомогательных функций вы уже делали в предыдущей лекции, здесь используем это как данность.
3. Режимы авторизации в MCP Jam: обзор
В MCP Jam есть несколько режимов авторизации для подключения к MCP‑серверу. Они соответствуют типичным OAuth‑паттернам: от полного отсутствия токена до полноценного Authorization Code + PKCE.
Кратко перечислим:
- None (No Auth) — Jam вообще не добавляет заголовок Authorization. Это анонимный доступ. Подходит для открытых MCP‑серверов и проверки, что закрытые ресурсы корректно отказывают с 401 и WWW-Authenticate.
- Bearer Token — Jam добавляет Authorization: Bearer <токен>, который вы вручную вставляете в интерфейсе. Подходит для быстрых проверок: токен уже где‑то получен (curl, Keycloak UI), а вы хотите проверить поведение MCP‑ресурса.
- OAuth with credentials (Client Credentials) — Jam сам получает токен по client_credentials у Auth Server, используя указанные Client ID и Secret. Это режим «конфиденциального клиента», больше похож на сервер‑серверную авторизацию без участия пользователя.
- Default OAuth (Authorization Code + PKCE) — главный режим для ChatGPT‑подобных клиентов (public client без секрета). Jam сам читает resource_metadata, находит Auth Server, запускает браузер с /authorize, проводит PKCE‑флоу и получает пользовательский токен.
Для наглядности свернём это в табличку.
| Режим в Jam | Что шлёт Jam | Кто добывает токен | Типичный сценарий |
|---|---|---|---|
| None | Нет Authorization | Никто | Анонимные инструменты, проверка 401 |
| Bearer Token | Bearer <ручной> | Вы (curl, UI IdP) | Тестирование логики Resource Server |
| OAuth with cred. | Bearer <client token> | Jam по client_credentials | Сервисные/админ‑инструменты |
| Default OAuth | Bearer <user token> | Jam через Authorization Code+PKCE | Пользовательский логин как в ChatGPT |
Сейчас пройдёмся по каждому режиму и посмотрим, как прогонять через него наш GiftGenius MCP‑сервер.
4. Режим None: проверяем, что сервер правильно отказывает
Начнём с самого примитивного режима: никакой авторизации.
В MCP Jam вы выбираете ваш сервер (например, http://localhost:4000/mcp) и в настройках соединения выставляете режим авторизации None.
Что происходит при этом:
- Jam устанавливает MCP‑соединение;
- при вызове инструмента он не добавляет заголовок Authorization;
- вы можете вызывать любые открытые инструменты (например, search_gifts);
- при вызове защищённого инструмента (например, list_user_orders) ваш сервер должен ответить 401 Unauthorized.
Важно, чтобы на этот 401 сервер добавил правильный WWW-Authenticate. Пример ответа с дополнительными полями realm и scope, близкий к рекомендованному OpenAI и MCP Authorization spec:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="mcp",
resource_metadata="https://giftgenius.example.com/.well-known/oauth-protected-resource",
scope="mcp:tools"
Content-Type: application/json
{"error": "unauthorized"}
Jam, увидев такой ответ, понимает: ресурс защищён, вот откуда брать метаданные (resource_metadata), и какие scopes ожидаются. В режиме None он просто покажет вам ошибку, но в режиме Default OAuth он автоматически пойдёт по указанному resource_metadata и запустит OAuth‑флоу.
С точки зрения отладки, в режиме None вы проверяете:
- что открытые инструменты работают вообще без токена;
- что защищённые инструменты никогда не выполняются анонимно;
- что заголовок WWW-Authenticate соответствует спецификации (включает Bearer и resource_metadata).
Звучит как тривиальная проверка, но огромное количество проблем начинается с того, что 401 возвращается без WWW-Authenticate или с неправильным параметром в нём (например, устаревшее resource_metadata_uri вместо актуального resource_metadata).
5. Режим Bearer Token: быстрый тест логики Resource Server
Следующий шаг — режим, в котором у вас уже есть валидный токен (полученный вне Jam) и вы хотите проверить именно логику Resource Server: правильно ли он принимает/отклоняет этот токен, корректно ли работает со scope и audience и связывает sub с пользователем вашего сервиса.
В MCP Jam переключаете режим на Bearer Token и вставляете в поле токена, скажем:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Теперь Jam будет добавлять к каждому MCP‑запросу заголовок:
Authorization: Bearer eyJhbGciOi...
Ваш MCP‑сервер принимает запрос, проходит через мидлварь requireScope("mcp:tools"), декодирует JWT и проверяет claims. Типичный код проверки можно упрощённо написать так:
// auth/verifyToken.ts
import jwt from "jsonwebtoken";
export function verifyToken(header: string) {
const token = header.replace("Bearer ", "");
const payload = jwt.verify(token, process.env.JWT_PUBLIC_KEY!);
// здесь можно проверить aud, scope и т.д.
return payload as { sub: string; scope?: string };
}
И использовать его в мидлвари:
// внутри requireScope
const payload = verifyToken(header);
if (!payload.scope?.includes(requiredScope)) {
res.status(403).json({ error: "insufficient_scope" });
return;
}
(req as any).user = { id: payload.sub };
next();
В режиме Bearer вы можете экспериментировать:
- вставить токен без нужного scope и убедиться, что сервер отвечает 403/401;
- вставить токен с неправильной aud и посмотреть, что сервер его отвергает;
- вставить просроченный токен, чтобы проверить ошибку invalid_token.
Это режим локального «ударного тестирования» логики Resource Server без участия UI‑логина и PKCE. Всё, что вы проверяете здесь, потом один в один применяется к токенам, которые ChatGPT или Jam будут получать в режиме Default OAuth.
6. Режим OAuth with credentials (Client Credentials): токен «от имени приложения»
Теперь — более редкий, но полезный для понимания режим: OAuth with credentials, то есть client_credentials‑grant. В Jam вы указываете:
- Client ID
- Client Secret
- нужные scopes (например, mcp:tools)
Jam выполняет запрос к token_endpoint вашего Auth Server’а примерно такого вида:
POST /oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&
client_id=<ID>&
client_secret=<SECRET>&
scope=mcp:tools
Auth Server выдаёт токен, в котором sub обычно означает сам клиент (например, sub = "mcp-jam-test-client"), а не конкретного пользователя. Jam начинает использовать этот токен как обычный Bearer.
Для чего это может быть полезно в мире MCP:
- сервисные/админские инструменты, не привязанные к конкретному пользователю (например, выгрузка логов, health‑check, техподдержка);
- проверка, что MCP‑сервер умеет различать пользовательские токены и токены «клиента», если ваша бизнес‑логика это учитывает.
В контексте ChatGPT Apps этот режим обычно не используется, потому что ChatGPT как публичный клиент не хранит секреты (а у public client по определению не должно быть client_secret). Но в Jam он помогает увидеть разницу между:
- «Я просто подсунул готовый токен» (режим Bearer);
- «Jam сам сходил за токеном по клиентским реквизитам» (OAuth with credentials).
На учебном сервере можно, например, сделать специальный MCP‑tool admin_list_all_orders, который доступен только по токену с grant_type=client_credentials и соответствующей ролью. Это не обязательная часть сегодняшней лекции, но полезный эксперимент.
7. Режим Default OAuth: полный Authorization Code + PKCE, как у ChatGPT
Теперь — главная звезда программы: Default OAuth. Именно этот режим ближе всего к тому, что делает ChatGPT при привязке аккаунта вашего App. Клиент читает resource_metadata, идёт к Auth Server, открывает пользователю страницу логина, получает authorization code и меняет его на access token по схеме Authorization Code + PKCE S256.
Разберём последовательность шагов. Для наглядности — диаграмма.
sequenceDiagram
participant Jam as MCP Jam (Client)
participant RS as MCP Server (Resource)
participant PRM as /.well-known/oauth-protected-resource
participant AS as Auth Server (Keycloak/Auth0)
Jam->>RS: Вызов защищённого tool (без токена)
RS-->>Jam: 401 + WWW-Authenticate (resource_metadata=PRM)
Jam->>PRM: GET /.well-known/oauth-protected-resource
PRM-->>Jam: JSON с resource, authorization_servers, scopes_supported...
Jam->>AS: GET /authorize?client_id=...&code_challenge=...&scope=...
Note right of AS: Пользователь логинится и даёт согласие
AS-->>Jam: redirect с authorization_code
Jam->>AS: POST /token (code + code_verifier)
AS-->>Jam: { access_token, scope, expires_in, ... }
Jam->>RS: Вызов tool с Authorization: Bearer <access_token>
RS-->>Jam: Успешный результат инструмента
Что вам важно проверить в этом режиме:
- Корректный ответ 401/WWW-Authenticate от MCP‑сервера. Если сервер не отдаёт resource_metadata или отдаёт неверный URL, Jam не сможет прочитать PRM и запустить OAuth‑флоу.
- Валидный документ .well-known/oauth-protected-resource. В нём должны быть правильные resource, authorization_servers, scopes_supported и т.д., чтобы Jam смог понять, куда идти за токенами и какие scopes просить.
- Правильная настройка Auth Server.
- Включён Authorization Code Flow с PKCE S256.
- Client ID соответствует тому, что ожидается в PRM (или регистрируется через DCR — Dynamic Client Registration).
- Redirect URI в Auth Server точно совпадает с тем, что использует Jam.
- PKCE S256. Jam формирует code_challenge и ожидает, что Auth Server поддерживает метод S256. Если PKCE отключён или поддерживается только plain, флоу развалится.
- Scopes и audience. Auth Server должен выдавать токен с нужным aud и запрошенными scopes (mcp:tools и т.д.), а MCP‑сервер — проверять их.
В результате успешного Default OAuth вы получите:
- в Jam — соединение с MCP‑сервером, у которого защищённый инструмент list_user_orders возвращает корректные данные именно для того пользователя, под которым вы залогинились в Auth Server;
- в логах Auth Server — успешный authorize + token‑обмен;
- в логах MCP‑сервера — успешная валидация токена и извлечение sub.
Для отладки часто помогает добавить простой логгер в обработчик инструмента, чтобы убедиться, что вы действительно видите userId из токена:
// внутри обработчика MCP-инструмента list_user_orders
export async function listUserOrders(args: any, context: any) {
const user = context.user as { id: string };
console.log("[MCP] listUserOrders for user", user.id);
// дальше вы отдаёте заказы этого пользователя
}
8. Где что ломается: диагностика по режимам
Теперь давайте обсудим, как по симптомам в MCP Jam понять, где именно проблема: в MCP‑сервере, в Auth Server или в метаданных. Этот раздел — своего рода чек‑лист диагностики по режимам.
Если в режиме None:
Вы вызываете защищённый инструмент, сервер возвращает:
- 200 OK и выполняет действие даже без токена — значит, у вас нет проверки токена перед этим инструментом. Нужно добавить мидлварь или проверку scopes.
- 401, но без WWW-Authenticate или с кривым resource_metadata — Jam не узнает, куда идти за метаданными, и не сможет запустить Default OAuth. Исправляйте заголовок по примеру выше.
Если в режиме Bearer Token:
- Jam стабильно получает 401/403 даже с токеном, который вы уверены, что валиден при вызове напрямую (через curl или Postman). Скорее всего, что‑то не так в логике Resource Server: неправильная проверка aud/scope или не тот публичный ключ для подписи JWT.
- Если Bearer‑токен работает в Jam, но не работает затем в Default OAuth — значит, проблема не в MCP‑сервере, а в Auth Server или PRM: токен, полученный по Default OAuth, отличается по scope/aud от того, который вы тестировали руками.
Если в режиме OAuth with credentials:
- Если Jam не может получить токен (ошибка на шаге /token) — ищите причину в настройках клиента на Auth Server (неверный secret, неразрешённый client_credentials или запрещённый scope).
- Если токен есть, но MCP‑сервер его отвергает — возможно, ваш сервер ожидает пользовательский sub (email/ID пользователя), а в токене только идентификатор клиента. Либо aud/scope не совпадают с ожидаемыми.
Если в режиме Default OAuth:
Это наиболее богатый на подводные камни сценарий. Частые проблемы:
- Неверные redirect URI. Auth Server жалуется на invalid_redirect_uri или просто не выдаёт код. Убедитесь, что URI Jam внесён в настройки клиента IdP без лишних слэшей и опечаток.
- Отсутствие или неподдерживаемый PKCE. Если Auth Server требует PKCE, а Jam (или старая его версия) не отправляет code_challenge, или наоборот — Jam отправляет S256, а IdP не поддерживает этот метод, вы увидите invalid_request.
- Несовпадающие scopes. В PRM вы заявили mcp:tools, а клиенту в IdP разрешён только openid, или наоборот — Jam просит больше scope, чем IdP готов выдать.
- Не тот audience (aud). Токен выдаётся с aud отличным от того, который ожидает MCP‑сервер (например, URL другого ресурса). Сервер по делу его отклонит.
Очень важно научиться смотреть логи трёх мест:
- MCP Jam — ошибки при разборе PRM и при HTTP‑запросах к Auth Server;
- Auth Server — логи /authorize и /token подскажут, от чего он отказывается;
- MCP‑сервер — причины отклонения токена (invalid_token, insufficient_scope, wrong_audience).
9. Как это связано с реальным ChatGPT App
Почему мы так много времени тратим на игру с Jam, а не бежим сразу в Developer Mode ChatGPT? Потому что Jam — это именно лабораторный стенд: он даёт вам в руки управление режимами авторизации и выводит всю внутреннюю кухню флоу.
Когда вы запускаете Default OAuth в Jam и доводите его до успеха, вы фактически подтверждаете:
- .well-known/oauth-protected-resource у MCP‑сервера корректен;
- Auth Server (Keycloak/Auth0/…) настроен правильно;
- роли, scopes, audience и claims соответствуют ожиданиям;
- MCP‑сервер умеет проверять токен и привязывать его к пользователю.
ChatGPT, подключенный к тому же MCP‑серверу, будет делать то же самое: прочитает PRM, пойдёт на Auth Server, получит токен и начнёт вызывать инструменты с Authorization: Bearer.
Разница в том, что в ChatGPT вы видите только финальный результат («успешно залинковали аккаунт» или «что‑то пошло не так»), а в Jam — видите весь протокол и можете по шагам понять, где именно «не так».
10. Мини‑практика: последовательное тестирование нашего GiftGenius MCP‑сервера
Соберём всё в простой последовательный сценарий, который вы можете повторить на своём проекте.
Сначала запускаете ваш MCP‑сервер (например, pnpm dev:mcp), убедитесь, что:
- он слушает по http://localhost:4000/mcp (или ваш URL);
- endpoint /.well-known/oauth-protected-resource отдаёт корректный JSON;
- Auth Server (Keycloak) работает и имеет настроенный public‑client для Jam/ChatGPT.
Дальше:
- Режим None.
Подключаете Jam к MCP‑серверу без авторизации. Проверяете, что:- search_gifts отрабатывает;
- list_user_orders возвращает 401 с правильным WWW-Authenticate.
- Режим Bearer Token.
Получаете access token через Keycloak (через UI или curl). Подставляете в Jam, вызываете list_user_orders и убеждаетесь, что:- при валидном токене инструмент отрабатывает и возвращает заказы конкретного пользователя;
- при токене без mcp:tools или с другим aud — сервер отдаёт ошибку.
- Режим OAuth with credentials.
Если у вас есть конфиденциальный клиент: указываете client_id и client_secret в Jam, ставите нужный scope, вызываете технический инструмент (например, admin_list_all_orders) и проверяете, что работает только с таким сервисным токеном. - Режим Default OAuth.
Включаете Default OAuth, вызываете list_user_orders. Jam сам:- получит 401 + WWW-Authenticate,
- прочитает PRM,
- откроет браузер, где вы войдёте в Keycloak,
- получит токен по Authorization Code + PKCE,
- вызовет MCP‑инструмент с токеном, после чего вы увидите свои заказы в ответе.
Если все четыре режима отработали ожидаемо — поздравляю, вы не просто «что‑то там настроили с Keycloak», а реально понимаете, как проверять и отлаживать весь auth‑флоу.
11. Типичные ошибки при работе с MCP Jam и тестированием авторизации
В практической жизни эти проблемы часто проявляются в виде повторяющихся паттернов ошибок. Ниже — несколько типичных сценариев «как делать не надо», чтобы вы могли узнавать их по симптомам.
Ошибка №1: ожидание, что защищённый инструмент будет работать в режиме None.
Иногда разработчик включает Jam в режиме None, вызывает list_user_orders и удивляется 401‑му, а потом «на всякий случай» убирает проверку токена с сервера. В результате MCP‑инструмент начинает работать анонимно, что для личных данных и commerce‑сценариев категорически недопустимо. Режим None нужен, чтобы именно проверить, что сервер корректно отказывает без токена и отдаёт WWW-Authenticate с resource_metadata.
Ошибка №2: забытый или некорректный заголовок WWW-Authenticate.
Очень распространённый случай: сервер возвращает 401 без WWW-Authenticate или с устаревшим параметром resource_metadata_uri. Jam (как и ChatGPT) в таком случае не понимает, куда идти за Protected Resource Metadata, и Default OAuth просто не стартует. Минимально достаточный вариант — WWW-Authenticate: Bearer resource_metadata="https://.../.well-known/oauth-protected-resource". Поля realm и scope остаются опциональными; главное — не забыть само resource_metadata.
Ошибка №3: тестирование только Bearer‑режима и игнорирование Default OAuth.
Разработчик вручную получает токен, вставляет его в Jam, видит, что всё работает, и считает задачу решённой. А когда приходит время подключать реальный ChatGPT, оказывается, что .well-known некорректен, PKCE не поддерживается, redirect URI не совпадает, и линкинг падает. Тест Bearer‑режима — необходимый, но не достаточный шаг. Default OAuth нужно прогонять обязательно, иначе вы не проверите половину важнейших настроек Auth Server и PRM.
Ошибка №4: попытка использовать client_credentials там, где нужен пользовательский токен.
Иногда в отчаянии разработчик включает в Jam режим OAuth with credentials и начинает получать токены по client_credentials, а затем использовать их для пользовательских инструментов, вроде list_user_orders. В результате sub в токене — это client_id, а не реальный пользователь, и бизнес‑логика начинает вести себя странно (например, показывать «общие» данные или падать при попытке найти пользователя с таким ID). Для ChatGPT‑сценариев с реальными пользователями нужен Authorization Code + PKCE (Default OAuth), а client_credentials подходит только для сервисных задач.
Ошибка №5: несогласованность scopes и audience между PRM, Auth Server и MCP‑сервером.
В .well-known/oauth-protected-resource вы объявили, что ресурс — https://giftgenius.example.com, и поддерживаемые scopes — ["mcp:tools"]. В Auth Server клиенту выдали токен без aud, а MCP‑сервер при проверке токена ожидает строго aud = "https://giftgenius.example.com" и наличие mcp:tools. В результате токен, полученный через Default OAuth, MCP‑сервер отвергает, а вы тратите полдня на поиск «магии». Всегда проверяйте, что PRM, конфиг клиента в IdP и проверка на мидлвари MCP‑сервера договорились между собой по audience и scope.
Ошибка №6: использование старой версии MCP Jam.
Спецификация MCP Authorization активно развивается, появляются новые поля (resource_metadata, улучшенный PKCE‑флоу, вспомогательные отладчики). Если у вас старая версия Jam, она может не понимать свежие поля или работать с устаревшими именами параметров. Это приводит к сюрреалистическим багам: вы всё настроили по последнему RFC, а Jam просто не знает, что с этим делать. Перед тем как впадать в отчаяние, убедитесь, что Jam обновлён до актуальной версии.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ