From 2f9c02462c78f1a3f192e96616b849c20ec10946 Mon Sep 17 00:00:00 2001 From: "nikhil.sitaram" Date: Mon, 16 Feb 2026 00:46:54 -0600 Subject: [PATCH] fix(happy-cli): create fresh MCP server+transport per request for SDK >=1.26 compat MCP SDK v1.26.0 forbids reusing stateless transports (sessionIdGenerator: undefined). The previous code created a single McpServer and transport at startup, then reused the transport for every incoming HTTP request. The first tool call succeeded, but subsequent calls failed with "Stateless transport cannot be reused". Changed to a factory pattern: each HTTP request creates its own McpServer and StreamableHTTPServerTransport, connects them, handles the request, then cleans up on connection close. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- .../src/claude/utils/startHappyServer.ts | 94 ++++++++++--------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/packages/happy-cli/src/claude/utils/startHappyServer.ts b/packages/happy-cli/src/claude/utils/startHappyServer.ts index 2a6ba79e6..bb7d712d7 100644 --- a/packages/happy-cli/src/claude/utils/startHappyServer.ts +++ b/packages/happy-cli/src/claude/utils/startHappyServer.ts @@ -36,58 +36,67 @@ export async function startHappyServer(client: ApiSessionClient) { // Create the MCP server // - const mcp = new McpServer({ - name: "Happy MCP", - version: "1.0.0", - }); + // Factory: create a fresh McpServer + transport per request. + // MCP SDK >=1.26 forbids reusing stateless transports, so each + // incoming HTTP request needs its own server/transport pair. + const createMcpServer = () => { + const s = new McpServer({ + name: "Happy MCP", + version: "1.0.0", + }); - mcp.registerTool('change_title', { - description: 'Change the title of the current chat session', - title: 'Change Chat Title', - inputSchema: { - title: z.string().describe('The new title for the chat session'), - }, - }, async (args) => { - const response = await handler(args.title); - logger.debug('[happyMCP] Response:', response); - - if (response.success) { - return { - content: [ - { - type: 'text', - text: `Successfully changed chat title to: "${args.title}"`, - }, - ], - isError: false, - }; - } else { - return { - content: [ - { - type: 'text', - text: `Failed to change chat title: ${response.error || 'Unknown error'}`, - }, - ], - isError: true, - }; - } - }); + s.registerTool('change_title', { + description: 'Change the title of the current chat session', + title: 'Change Chat Title', + inputSchema: { + title: z.string().describe('The new title for the chat session'), + }, + }, async (args) => { + const response = await handler(args.title); + logger.debug('[happyMCP] Response:', response); - const transport = new StreamableHTTPServerTransport({ - // NOTE: Returning session id here will result in claude - // sdk spawn to fail with `Invalid Request: Server already initialized` - sessionIdGenerator: undefined - }); - await mcp.connect(transport); + if (response.success) { + return { + content: [ + { + type: 'text', + text: `Successfully changed chat title to: "${args.title}"`, + }, + ], + isError: false, + }; + } else { + return { + content: [ + { + type: 'text', + text: `Failed to change chat title: ${response.error || 'Unknown error'}`, + }, + ], + isError: true, + }; + } + }); + + return s; + }; // // Create the HTTP server // const server = createServer(async (req, res) => { + const mcp = createMcpServer(); try { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined + }); + await mcp.connect(transport); await transport.handleRequest(req, res); + res.on('close', () => { + transport.close(); + mcp.close(); + }); } catch (error) { logger.debug("Error handling request:", error); if (!res.headersSent) { @@ -110,7 +119,6 @@ export async function startHappyServer(client: ApiSessionClient) { toolNames: ['change_title'], stop: () => { logger.debug(`[happyMCP] server:stop sessionId=${client.sessionId}`); - mcp.close(); server.close(); } }