fix(http): bridge ChatGPT MCP connector + Claude confidential-client OAuth#746
Open
panda850819 wants to merge 3 commits intogarrytan:masterfrom
Open
fix(http): bridge ChatGPT MCP connector + Claude confidential-client OAuth#746panda850819 wants to merge 3 commits intogarrytan:masterfrom
panda850819 wants to merge 3 commits intogarrytan:masterfrom
Conversation
…tive_date backfill
Two upstream races blocking 0.28.x → 0.30.0 upgrade on existing Postgres brains:
1. v0_29_1.ts orchestrator Phase B/C call createEngine but skip engine.connect(),
so the first executeRaw throws "No database connection: connect() has not
been called". Added explicit connect() in both phases.
2. backfill-effective-date.ts wraps per-batch UPDATEs with executeRaw('BEGIN')
+ executeRaw('SET LOCAL statement_timeout=...') + executeRaw('COMMIT').
postgres.js routes each executeRaw to a separate pool connection, so the
BEGIN never wraps anything and postgres.js refuses with UNSAFE_TRANSACTION.
Removed the BEGIN/COMMIT (per-row UPDATEs are short enough that the
statement_timeout protection wasn't load-bearing at typical brain scale).
Local fork patch — separate from the schema.sql ADD COLUMN race that was
worked around manually with a one-shot bun script.
4 tasks
…1.1.1-fixwave # Conflicts: # src/commands/migrations/v0_29_1.ts # src/commands/serve-http.ts # src/core/backfill-effective-date.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ChatGPT's Custom MCP Connector and Claude (
client_secret_post) OAuth both fail end-to-end againstgbrain serve --http --enable-dcrtoday. Twelve targeted patches inserve-http.ts+oauth-provider.tsclose the gaps.Hit while wiring a self-hosted gbrain MCP server (v0.30.0) into ChatGPT's Custom Connector and verifying Claude.ai web + Claude Code stayed working. Each patch was triaged from real edge traces + Postgres
oauth_clients/oauth_codes/oauth_tokensintrospection rather than guessing from client-side error messages — those messages are systematically misleading (one root cause cycled through "doesn't support DCR" → "DCR endpoint 404" → "doesn't implement OAuth" → "invalid_mcp_response 405").Verified end-to-end: ChatGPT lists
search+fetchand successfully callssearchagainst the brain; Claude.ai web + Claude Code DCR + token exchange +tools/listall green.Happy to split this into three PRs if you'd prefer (oauth security / chatgpt compat / search-fetch shim) — flagged the natural split below.
What's actually landing
1. OAuth correctness (every confidential or PKCE-only DCR client)
/tokenpre-hash middleware. SDKclientAuth.js:45strict-comparesclient.client_secretreturned byclientsStore.getClient()to the request body'sclient_secret. gbrain'soauth_clients.client_secret_hashcolumn stores sha256 hex; the client holds the plaintext returned at DCR time. The literal string compare always fails forclient_secret_postclients (Claude.ai web, Claude Code) → everyauthorization_code/refresh_tokenexchange gets400 invalid_client: \"Invalid client_secret\". SHA-256 the request's plaintext before SDK clientAuth runs so the comparison is hash-vs-hash. Skip forclient_credentialsgrant — gbrain's own handler hashes itself and would double-hash.getClient()stripsclient_secretfornoneclients. Same SDK clientAuth path: it demands a secret wheneverclient.client_secretis truthy, regardless oftoken_endpoint_auth_method. PKCE-only public clients (ChatGPT, mcporter, Hermes Agent, codex-cli) registered withnonetherefore get rejected with\"Client secret is required\". Hide the stored hash so SDK falls through to the PKCE-only path. (Durable fix:registerClientshould not generate or store a secret fornoneclients in the first place — left for a follow-up.)2. ChatGPT MCP connector compatibility
Catch-all
.well-knownrewrite middleware. ChatGPT exhaustively probes nine metadata URL variants before considering discovery complete:Any 404 makes ChatGPT abort and surface a misleading "DCR endpoint 404". Single regex rewrites every variant onto the SDK's canonical paths so the same metadata body answers every probe — beats enumerated alias whack-a-mole.
resourceServerUrl: '/mcp'so PRMresourcematches the URL users enter. Both confirmed-working open-source ChatGPT MCP connector references (tae0y/real-estate-mcp + Auth0, GetLarge fastify-mcp + Ory Hydra) publish PRMresourceas the/mcpURL. With this set, SDK serves PRM bodyresource: \"https://<host>/mcp\".UA-conditional OIDC stub fields. OpenAI's Apps SDK auth doc says ChatGPT accepts OAuth 2.0 metadata or OIDC metadata. Empirically, ChatGPT silently aborts DCR if the AS metadata document lacks
subject_types_supported,id_token_signing_alg_values_supported,userinfo_endpoint,jwks_uri— both confirmed-working references are full OIDC providers, not coincidence. Inject these fields viares.jsonpatch, gated onUser-Agentmatching/aiohttp|openai-mcp/iso non-ChatGPT clients keep clean OAuth 2.1 metadata. SDK's metadata document is a shared singleton, so we clone-before-mutate (otherwise one ChatGPT request would leak OIDC fields into every subsequent client's response)./userinfoand/.well-known/jwks.jsonstub routes back the OIDC pointers without changing token semantics. Userinfo returns soft 200 with{ sub: \"anonymous\" }(401 reads as auth failure to ChatGPT and aborts token exchange); jwks returns{ keys: [] }since gbrain uses opaque tokens, not JWTs.WWW-Authenticateon/mcp401 carriesresource_metadata=per RFC 9728 / MCP 2025-06-18 authorization spec.Slash-collapse middleware strips leading
//fromreq.url. gbrain publishes issuer with a trailing slash (URL canonical form), so naiveissuer + \"/register\"concat in clients produces//register→ Express 404.GET /mcpreturns 200 + idle SSE stream (15s heartbeat) instead of 405. MCP 2025-06-18 §StreamableHTTP permits 405 when no SSE is offered, but ChatGPT'sopenai-mcp/1.0.0treats it asinvalid_mcp_responsefatal error. Bearer-gated so unauth probes still get the spec'd 401 challenge.DELETE /mcpreturns 405 +Allowheader instead of Express's default 404.3. Opt-in ChatGPT search/fetch shim
ChatGPT Connector mode only displays tools named exactly
searchandfetchwith specific input schemas; everything else is silently filtered client-side. With gbrain's 30+ ops surfaced raw, ChatGPT shows zero tools.agentName.startsWith('ChatGPT')triggers a two-tool mode:tools/listreturns onlysearch+fetchwithinputSchemamatching OpenAI's connector spec.tools/callrewritesfetch→get_pageand projects results viatoChatgptShape():search→{ results: [{ id, title, text, url }] }fetch→{ id, title, text, url, metadata }structuredContent(machine-read) andcontent[].text(legacy JSON string) for max compat.Other MCP clients see the full op surface unchanged.
Diagnostics
appendFileSyncedge-trace logger writes every inbound request to~/.gbrain/logs/edge-trace.logsynchronously, bypassing bun's block-buffered stdout under launchdStandardOutPath. Without this,gbrain serve --http's log file lags real activity by minutes-to-hours and live debug is blind. Cheap (one fs call per request, no formatting). Happy to gate behind--debug-edge-traceif you'd prefer it not be always-on.Codex review follow-ups (not in this PR)
External review of the diff flagged 5 items I'd address in follow-up PRs once the foundation lands:
registerClientshould not generate or store aclient_secret_hashfortoken_endpoint_auth_method='none'clients (durable fix for the PKCE secret-leak workaround)./mcpshould enforce the RFC 8707resourceindicator as token audience (currently stored, not validated).source_id:slugfor cross-source dedup safety.client_name.startsWith('ChatGPT')(DCRclient_nameis client-controlled). Better gated by--enable-chatgpt-compatflag or explicit/mcp/chatgptroute.outputSchema.Verification
End-to-end against a self-hosted production deploy:
POST /register(201) →GET /authorize(302) →POST /token(200) →oauth_tokensrow issued with both access + refresh.GET /mcpopens SSE stream with bearer;POST /mcpJSON-RPC dispatches.search+fetch;search(\"<query>\")returns{ results: [...] }populated from gbrain./tokenpre-hash +getClientstrip patches (had been silently 401'ing on every confidential-clientclient_secret_postexchange).Need help on this PR? Tag
@codesmithwith what you need.