Skip to content

Commit bee3646

Browse files
chris-yyauclaudehappy-otter
committed
fix: reconnect MCP bridge on SSE timeout + pre-parse server body (#900, #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 #900, closes #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>
1 parent 5f55adc commit bee3646

File tree

2 files changed

+34
-3
lines changed

2 files changed

+34
-3
lines changed

packages/happy-cli/src/claude/utils/startHappyServer.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,34 @@ export async function startHappyServer(client: ApiSessionClient) {
7777
const server = createServer(async (req, res) => {
7878
const mcp = createMcpServer(handler);
7979
try {
80+
// Pre-parse body to avoid @hono/node-server stream conversion issues
81+
let parsedBody: unknown;
82+
if (req.method === 'POST') {
83+
const chunks: Buffer[] = [];
84+
for await (const chunk of req) {
85+
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
86+
}
87+
try {
88+
parsedBody = JSON.parse(Buffer.concat(chunks).toString());
89+
} catch {
90+
res.writeHead(400, { 'Content-Type': 'application/json' });
91+
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' }, id: null }));
92+
mcp.close();
93+
return;
94+
}
95+
}
96+
8097
const transport = new StreamableHTTPServerTransport({
8198
sessionIdGenerator: undefined
8299
});
83100
await mcp.connect(transport);
84-
await transport.handleRequest(req, res);
101+
await transport.handleRequest(req, res, parsedBody);
85102
res.on('close', () => {
86103
transport.close();
87104
mcp.close();
88105
});
89106
} catch (error) {
90-
logger.debug("Error handling request:", error);
107+
logger.debug("[happyMCP] Error handling request:", error);
91108
if (!res.headersSent) {
92109
res.writeHead(500).end();
93110
}

packages/happy-cli/src/codex/happyMcpStdioBridge.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,18 @@ async function main() {
5656
{ capabilities: {} }
5757
);
5858
const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
59+
transport.onclose = () => {
60+
// Reset client on transport close so next call reconnects
61+
if (httpClient === client) {
62+
httpClient = null;
63+
}
64+
};
5965
await client.connect(transport);
66+
client.onclose = () => {
67+
if (httpClient === client) {
68+
httpClient = null;
69+
}
70+
};
6071
httpClient = client;
6172
return client;
6273
} finally {
@@ -87,9 +98,12 @@ async function main() {
8798
try {
8899
const client = await ensureHttpClient();
89100
const response = await client.callTool({ name: 'change_title', arguments: args });
90-
// Pass-through response from HTTP server
91101
return response as any;
92102
} catch (error) {
103+
// Clean up stale client so the next invocation reconnects
104+
const staleClient = httpClient;
105+
httpClient = null;
106+
try { await staleClient?.close(); } catch { /* ignore close errors */ }
93107
return {
94108
content: [
95109
{ type: 'text', text: `Failed to change chat title: ${error instanceof Error ? error.message : String(error)}` },

0 commit comments

Comments
 (0)