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:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -10,6 +10,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.91.0] - 2026-06-18
|
||||||
|
|
||||||
Gitmost is a community-focused fork of Docmost. This release drops the
|
Gitmost is a community-focused fork of Docmost. This release drops the
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ interface McpHttpModule {
|
|||||||
// failure return a clean 401 JSON instead of tearing a hijacked response.
|
// failure return a clean 401 JSON instead of tearing a hijacked response.
|
||||||
const MCP_RESOLVED = Symbol('mcpResolvedConfig');
|
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
|
// TS with module:commonjs downlevels a literal import() to require(), which
|
||||||
// cannot load the ESM-only @docmost/mcp package. Indirect through Function so
|
// cannot load the ESM-only @docmost/mcp package. Indirect through Function so
|
||||||
// the real dynamic import() survives compilation and can load ESM from
|
// 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'])
|
? sharedTokenMatches(sharedToken, req.headers['x-mcp-token'])
|
||||||
: true;
|
: 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
|
// 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
|
// resolution. Compute them up front so the response mapping is a single pure
|
||||||
// decision (mapAuthResultToResponse) that cannot leak the password/header.
|
// decision (mapAuthResultToResponse) that cannot leak the password/header.
|
||||||
|
|||||||
Reference in New Issue
Block a user