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:
claude code agent 227
2026-06-21 03:40:40 +03:00
parent a11c87c4dc
commit d45ca00bcc
2 changed files with 44 additions and 0 deletions

View File

@@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Breaking Changes
- **MCP shared-token auth moved to its own header.** The `/mcp` shared guard
no longer reads `Authorization: Bearer <MCP_TOKEN>`; it now reads only the
`X-MCP-Token` header. Existing MCP clients (e.g. Claude Desktop) configured
with `Authorization: Bearer <MCP_TOKEN>` must be reconfigured to send
`X-MCP-Token: <MCP_TOKEN>` instead. The `Authorization` header is now
reserved for per-user HTTP Basic / Bearer access JWT credentials. See
`MCP_TOKEN` in `.env.example`. As a one-time aid, the server logs a single
migration warning when it sees the old-style header.
## [0.91.0] - 2026-06-18
Gitmost is a community-focused fork of Docmost. This release drops the

View File

@@ -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.