fix: MCP title generation fails — reconnect bridge + pre-parse body (#900)#908
fix: MCP title generation fails — reconnect bridge + pre-parse body (#900)#908chris-yyau wants to merge 3 commits intoslopus:mainfrom
Conversation
…t a function" Three fixes for intermittent "undefined is not a function" crashes: 1. **apiSocket.ts**: Replace non-null assertions (`this.socket!`, `this.encryption!`) with explicit null checks using local variable capture to prevent TOCTOU races across `await` boundaries. Throws descriptive errors instead of crashing. 2. **happyMcpStdioBridge.ts**: Add promise lock to `ensureHttpClient()` with `try/finally` cleanup to prevent race conditions and stale rejected promises. 3. **startHappyServer.ts**: Add missing error handler on HTTP server binding. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…pus#900, slopus#768) 1. **happyMcpStdioBridge.ts**: Auto-reconnect when HTTP MCP client drops. transport.onclose/client.onclose reset the client reference. Failed calls close the stale client to free SSE streams/timers and let the next invocation create a fresh connection. 2. **startHappyServer.ts**: Pre-parse POST body and pass as `parsedBody` to transport.handleRequest(), bypassing @hono/node-server stream conversion that causes HTTP 500. Invalid JSON returns proper 400. Closes slopus#900, closes slopus#768 Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
There was a problem hiding this comment.
Pull request overview
This PR fixes MCP session title generation failures by improving resilience of the STDIO→HTTP MCP bridge (reconnect/cleanup on dropped connections) and by pre-parsing POST bodies in the local MCP HTTP server to avoid intermittent request handling failures.
Changes:
- Add auto-reconnect + stale-client cleanup to the MCP STDIO bridge’s HTTP client.
- Pre-parse POST request bodies and pass
parsedBodyintoStreamableHTTPServerTransport.handleRequest, returning a proper JSON-RPC 400 on invalid JSON. - Harden socket RPC methods in the app by guarding against null socket/encryption and surfacing server-provided RPC errors.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/happy-cli/src/codex/happyMcpStdioBridge.ts | Adds connection locking and resets/cleanup on close/error to support reconnecting after SSE timeouts. |
| packages/happy-cli/src/claude/utils/startHappyServer.ts | Buffers+parses POST bodies before MCP transport handling to avoid stream conversion issues; improves error handling + server error rejection. |
| packages/happy-app/sources/sync/apiSocket.ts | Replaces non-null assertions with explicit guards and improves RPC error messaging. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| connectPromise = (async () => { | ||
| try { | ||
| const client = new Client( | ||
| { name: 'happy-stdio-bridge', version: '1.0.0' }, | ||
| { capabilities: {} } | ||
| ); | ||
| const transport = new StreamableHTTPClientTransport(new URL(baseUrl)); | ||
| transport.onclose = () => { | ||
| // Reset client on transport close so next call reconnects | ||
| if (httpClient === client) { | ||
| httpClient = null; | ||
| } | ||
| }; | ||
| await client.connect(transport); | ||
| client.onclose = () => { |
There was a problem hiding this comment.
In ensureHttpClient(), if client.connect(transport) throws, the newly created Client/transport are never closed (the reference is lost when the promise rejects). That can leave behind open SSE streams/timers in failure cases. Consider wrapping the connect in a try/catch and explicitly close()/dispose the client (and/or transport) before rethrowing so retries don't accumulate resources.
| const chunks: Buffer[] = []; | ||
| for await (const chunk of req) { | ||
| chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); | ||
| } | ||
| try { | ||
| parsedBody = JSON.parse(Buffer.concat(chunks).toString()); |
There was a problem hiding this comment.
The request body is fully buffered into memory with no size limit before parsing. Even though the server binds to 127.0.0.1, a local client can still send an arbitrarily large POST and cause high memory usage. Add a maximum body size (and return 413 / abort reading) to prevent accidental or malicious oversized requests from impacting the CLI process.
If client.connect() throws, the Client and transport were leaked. Now explicitly closes the client in the catch block before rethrowing. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
Summary
Fixes "Streamable HTTP error: Error POSTing to endpoint" that prevents session title generation. Builds on #907 (socket null guards).
happyMcpStdioBridge.ts: Auto-reconnect when HTTP MCP client drops.transport.onclose/client.onclosereset the client reference. Failed calls close the stale client to free SSE streams/timers.startHappyServer.ts: Pre-parse POST body and pass asparsedBodytotransport.handleRequest(), bypassing@hono/node-serverstream conversion that causes HTTP 500. Invalid JSON now returns proper 400.Root cause
The SSE stream times out every ~300s. The bridge's
httpClientbecomes stale but isn't reset, so subsequentchange_titlecalls fail. The server's reliance on@hono/node-serverbody stream conversion intermittently produces 500 errors.Depends on
Closes #900, closes #768
Test plan
change_titleworks for Claude Code sessions (direct HTTP)change_titleworks for Codex sessions (STDIO bridge)🤖 Generated with Claude Code via Happy