Files
hapi/cli/src/codex/happyMcpStdioBridge.ts
T
weishu 17eeba10d6 feat(cli): add Bun single executable binary support
Enables building hapi as standalone Bun-compiled executables for macOS,
Linux, and Windows (x64/arm64). Adds build script, bootstrap entry point,
runtime asset management, and automatic deployment of bundled tools
(ripgrep, difftastic). Includes MCP stdio bridge support and proper
environment handling for compiled binaries. Updates documentation with
build and installation instructions for single executable distribution.
2025-12-20 21:41:11 +08:00

105 lines
3.2 KiB
TypeScript

/**
* Happy MCP STDIO Bridge
*
* Minimal STDIO MCP server exposing a single tool `change_title`.
* On invocation it forwards the tool call to an existing Happy HTTP MCP server
* using the StreamableHTTPClientTransport.
*
* Configure the target HTTP MCP URL via env var `HAPPY_HTTP_MCP_URL` or
* via CLI flag `--url <http://127.0.0.1:PORT>`.
*
* Note: This process must not print to stdout as it would break MCP STDIO.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { z } from 'zod';
function parseArgs(argv: string[]): { url: string | null } {
let url: string | null = null;
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === '--url' && i + 1 < argv.length) {
url = argv[i + 1];
i++;
}
}
return { url };
}
export async function runHappyMcpStdioBridge(argv: string[]): Promise<void> {
try {
// Resolve target HTTP MCP URL
const { url: urlFromArgs } = parseArgs(argv);
const baseUrl = urlFromArgs || process.env.HAPPY_HTTP_MCP_URL || '';
if (!baseUrl) {
// Write to stderr; never stdout.
process.stderr.write(
'[happy-mcp] Missing target URL. Set HAPPY_HTTP_MCP_URL or pass --url <http://127.0.0.1:PORT>\n'
);
process.exit(2);
}
let httpClient: Client | null = null;
async function ensureHttpClient(): Promise<Client> {
if (httpClient) return httpClient;
const client = new Client(
{ name: 'happy-stdio-bridge', version: '1.0.0' },
{ capabilities: {} }
);
const transport = new StreamableHTTPClientTransport(new URL(baseUrl));
await client.connect(transport);
httpClient = client;
return client;
}
// Create STDIO MCP server
const server = new McpServer({
name: 'Happy MCP Bridge',
version: '1.0.0',
});
// Register the single tool and forward to HTTP MCP
server.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) => {
try {
const client = await ensureHttpClient();
const response = await client.callTool({ name: 'change_title', arguments: args });
// Pass-through response from HTTP server
return response as any;
} catch (error) {
return {
content: [
{ type: 'text', text: `Failed to change chat title: ${error instanceof Error ? error.message : String(error)}` },
],
isError: true,
};
}
}
);
// Start STDIO transport
const stdio = new StdioServerTransport();
await server.connect(stdio);
} catch (err) {
try {
process.stderr.write(`[happy-mcp] Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
} finally {
process.exit(1);
}
}
}