diff --git a/CHANGELOG.md b/CHANGELOG.md index 29058510..8bdec8c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `; it now reads only the + `X-MCP-Token` header. Existing MCP clients (e.g. Claude Desktop) configured + with `Authorization: Bearer ` must be reconfigured to send + `X-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 diff --git a/apps/server/src/integrations/mcp/mcp.service.ts b/apps/server/src/integrations/mcp/mcp.service.ts index cb746b90..637f3e56 100644 --- a/apps/server/src/integrations/mcp/mcp.service.ts +++ b/apps/server/src/integrations/mcp/mcp.service.ts @@ -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 `; 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 `, 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 ` ' + + '(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.