docs(mcp): document the MCP_TOKEN header breaking change + one-time warning (#84)
The shared MCP_TOKEN guard moved from 'Authorization: Bearer <MCP_TOKEN>' to the X-MCP-Token header (Authorization is now per-user Basic/Bearer), silently breaking existing /mcp clients. Document it as a Breaking Change in CHANGELOG (reconfigure to X-MCP-Token). Add a once-per-process migration warning: when MCP_TOKEN is set, no x-mcp-token is present, and Authorization carries the old 'Bearer <MCP_TOKEN>', log a hint to migrate — without changing the auth decision (still rejected) or logging the token value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,12 @@ interface McpHttpModule {
|
||||
// failure return a clean 401 JSON instead of tearing a hijacked response.
|
||||
const MCP_RESOLVED = Symbol('mcpResolvedConfig');
|
||||
|
||||
// One-time-per-process latch for the legacy-auth migration warning. The shared
|
||||
// MCP token used to be sent as `Authorization: Bearer <MCP_TOKEN>`; it now lives
|
||||
// in its own `X-MCP-Token` header. When we still see the old style we log ONCE
|
||||
// (never the token value) so operators can migrate without log spam.
|
||||
let warnedLegacyMcpAuth = false;
|
||||
|
||||
// TS with module:commonjs downlevels a literal import() to require(), which
|
||||
// cannot load the ESM-only @docmost/mcp package. Indirect through Function so
|
||||
// the real dynamic import() survives compilation and can load ESM from
|
||||
@@ -354,6 +360,33 @@ export class McpService implements OnModuleDestroy {
|
||||
? sharedTokenMatches(sharedToken, req.headers['x-mcp-token'])
|
||||
: true;
|
||||
|
||||
// Back-compat hint (does NOT change the auth decision). When MCP_TOKEN is
|
||||
// configured but the request carries no `X-MCP-Token` and instead sends the
|
||||
// legacy `Authorization: Bearer <MCP_TOKEN>`, warn ONCE per process so the
|
||||
// operator migrates the client. The token value is never logged; the bearer
|
||||
// value is compared in constant time via sharedTokenMatches.
|
||||
if (
|
||||
sharedToken &&
|
||||
!warnedLegacyMcpAuth &&
|
||||
req.headers['x-mcp-token'] === undefined
|
||||
) {
|
||||
const auth = req.headers['authorization'];
|
||||
const header = Array.isArray(auth) ? auth[0] : auth;
|
||||
const bearer =
|
||||
typeof header === 'string' && header.startsWith('Bearer ')
|
||||
? header.slice('Bearer '.length)
|
||||
: undefined;
|
||||
if (bearer !== undefined && sharedTokenMatches(sharedToken, bearer)) {
|
||||
warnedLegacyMcpAuth = true;
|
||||
this.logger.warn(
|
||||
'MCP shared token received via `Authorization: Bearer <MCP_TOKEN>` ' +
|
||||
'(legacy). This is no longer accepted: send the shared token in the ' +
|
||||
'`X-MCP-Token` header instead, and reserve `Authorization` for ' +
|
||||
'per-user credentials. Reconfigure the MCP client to migrate.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Short-circuit checks (shared token, enablement) that do not need the auth
|
||||
// resolution. Compute them up front so the response mapping is a single pure
|
||||
// decision (mapAuthResultToResponse) that cannot leak the password/header.
|
||||
|
||||
Reference in New Issue
Block a user