Skip to content

fix: MCP title generation fails — reconnect bridge + pre-parse body (#900)#908

Open
chris-yyau wants to merge 3 commits intoslopus:mainfrom
chris-yyau:fix/mcp-title-reconnect-900
Open

fix: MCP title generation fails — reconnect bridge + pre-parse body (#900)#908
chris-yyau wants to merge 3 commits intoslopus:mainfrom
chris-yyau:fix/mcp-title-reconnect-900

Conversation

@chris-yyau
Copy link

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.onclose reset the client reference. Failed calls close the stale client to free SSE streams/timers.
  • 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 now returns proper 400.

Root cause

The SSE stream times out every ~300s. The bridge's httpClient becomes stale but isn't reset, so subsequent change_title calls fail. The server's reliance on @hono/node-server body stream conversion intermittently produces 500 errors.

Depends on

Closes #900, closes #768

Test plan

  • change_title works for Claude Code sessions (direct HTTP)
  • change_title works for Codex sessions (STDIO bridge)
  • Title updates survive SSE stream timeout (~5 min idle)
  • Invalid JSON POST returns 400, not 500

🤖 Generated with Claude Code via Happy

chris-yyau and others added 2 commits March 24, 2026 20:13
…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>
Copilot AI review requested due to automatic review settings March 24, 2026 12:38
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 parsedBody into StreamableHTTPServerTransport.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.

Comment on lines +52 to +66
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 = () => {
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +88
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());
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants