Summary
When gbrain serve --http --enable-dcr is used, every OAuth authorization_code token exchange fails with invalid_client / Invalid client_secret, including for clients that just successfully registered via /register and immediately used the returned secret. Same affects refresh_token grant. This makes DCR-based MCP clients (Claude.ai, etc.) completely unable to connect.
Root cause
Two interacting code paths:
-
src/core/oauth-provider.ts:145 — GBrainClientsStore.getClient() returns the SHA-256 hash in the client_secret field:
client_secret: r.client_secret_hash as string | undefined,
-
@modelcontextprotocol/sdk/././middleware/clientAuth.js:23 does plain string compare.
The plaintext secret from the request can never === the SHA-256 hex digest returned by getClient().
Workaround (one-line middleware patch)
app.post('/token', express.urlencoded({ extended: false }), (req, _res, next) => {
const grant = req.body?.grant_type;
if ((grant === 'authorization_code' || grant === 'refresh_token') && typeof req.body?.client_secret === 'string') {
req.body.client_secret = createHash('sha256').update(req.body.client_secret).digest('hex');
}
next();
});
Verified against Claude.ai's connector flow. Happy to send a PR.
Summary
When
gbrain serve --http --enable-dcris used, every OAuthauthorization_codetoken exchange fails withinvalid_client / Invalid client_secret, including for clients that just successfully registered via/registerand immediately used the returned secret. Same affectsrefresh_tokengrant. This makes DCR-based MCP clients (Claude.ai, etc.) completely unable to connect.Root cause
Two interacting code paths:
src/core/oauth-provider.ts:145—GBrainClientsStore.getClient()returns the SHA-256 hash in theclient_secretfield:@modelcontextprotocol/sdk/././middleware/clientAuth.js:23does plain string compare.The plaintext secret from the request can never
===the SHA-256 hex digest returned bygetClient().Workaround (one-line middleware patch)
Verified against Claude.ai's connector flow. Happy to send a PR.