What we learned running a memory MCP server that both Claude and ChatGPT connect to
We run a remote MCP server (agent memory backend) that gets traffic from Claude Desktop, the ChatGPT connector, Codex desktop/CLI, and assorted mcp-remote bridges. Same spec, four very different clients. Field notes from the last few weeks, in case they save someone the same debugging time.
**1. You cannot fingerprint clients from a bare GET.**
A streamable-HTTP client probing for the optional server-push stream sends the exact same headers as a 2024-era legacy SSE client: `accept: text/event-stream`, no protocol-version header. Measured with real clients, not mocks. Any "looks legacy → reject" logic breaks current clients. If you're deprecating the old transport, only requests with explicit legacy markers (`?transport=sse`, `?connection_id`) are provably legacy.
**2. mcp-remote's fallback logic substring-matches your error strings.**
Its `shouldFallbackOnError` checks whether the error contains "404" or "405" and, if so, permanently downgrades to sse-only. The spec permits answering 405 to a GET — doing so pushes the client onto the transport you're trying to retire. We use 410 Gone for retired paths and wrote the response body so those digit sequences appear nowhere in it, with a regression test pinning that. Your status codes are part of the client's control flow.
**3. Clients disagree on what a GET response *is*.**
`mcp-remote` happily consumes a never-closing keepalive stream. The ChatGPT connector sends `accept: */*` (empty UA) and *waits for the response to complete* — a never-closing stream stalls its OAuth discovery for minutes, which then outlives the one-shot loopback callback listener it opened on 127.0.0.1. Users see "verification failed" while your server logs show nothing but 200/302. The fix that held up: split on Accept semantics, not UA. No `text/event-stream` in Accept → reply and close immediately (11ms in prod); with it → keep the stream. OAuth's loopback-callback pattern means *any* multi-second server-side delay can burn the client's listener window.
**4. Every capability you declare in metadata is a contract some client will enforce against you.**
We declared `authorization_response_iss_parameter_supported: true` (RFC 9207) in our authorization server metadata — accurate, we do send `iss` on every redirect. The Codex build we tested (150.0.7871.x) reads that declaration and *enforces* "callback must carry iss"... with a loopback listener that doesn't parse `iss`. Clean-room test (PATH shim so nothing else could hit the listener): it received exactly one callback with `code`, `iss`, and `state` all present and still reported "missing issuer". Claude Desktop, same server, no such enforcement, passed first try. Fix was one line: stop declaring the capability, keep sending the parameter. Before re-declaring anything like this, we now test with the official CLI (`codex mcp login` scripts the whole flow headlessly).
**5. Auth page: session + one-click is not just nicer, it's the unanimous pattern.**
We surveyed 8 vendors running remote MCP connectors (Notion, Linear, Sentry, Atlassian, GitHub, Asana, Stripe, plus Cloudflare's workers-oauth-provider defaults): 8/8 do existing-web-session → single consent screen → one-click Allow → 302. 0/8 offer an API-key paste box on the auth page (keys are headless-config only, everywhere). ChatGPT's docs state it doesn't support API-key auth for connectors at all. We removed our key-paste input the same day. One implementation trap: the session cookie must be `SameSite=Lax`, not `Strict` — the authorize redirect is a top-level cross-site navigation, so `Strict` cookies never arrive and the one-click flow silently degrades to a login wall.
**6. (BYOK-specific) One tenant's dead key can starve everyone.**
If your pipeline claims work globally ordered by age and a tenant's expired provider key 401s forever, those rows monopolize every claim batch, your adaptive-concurrency controller reads the error rate and throttles *globally*, and "waiting for key" recovery that only checks key *presence* (not validity) resurrects the same corpses forever. We watched unrelated tenants' jobs sit 30+ minutes. Circuit-break per tenant on auth failures, and treat "retry N times → dead → auto-delete" as the data-loss path it is.
Happy to go deeper on any of these in the comments — the OAuth one in particular ate several days of our lives.
(Context for transparency: the server is Humaux Memory — the debugging notes above literally live in it, which is the dogfooding loop. Site: https://humaux.com?utm\_source=reddit&utm\_medium=social&utm\_campaign=bip-w1 )
Posted Jul 29, 2026, 12:55 PM UTC