Files
hapi/cli/src/claude/utils/startHappyServer.ts
T
SmallSpiderandGitHub 841b7cc035 Add Codex multi-agent timeline support (#588)
* checkpoint codex multiagent UI state

* fix codex multiagent event scoping

* fix: stabilize codex subagent timeline

* chore: remove codex subagent nesting prompt

* test(web): stabilize tool result rendering tests

* fix: collapse codex agent trace rows by default

* fix(web): keep chat scrolled to bottom

* fix(web): backfill agent-run-heavy message loads

* fix(codex): fail stuck subagent spawns

* fix(codex): preserve wrapped child event scope

* fix(codex): surface agent tool completions
2026-05-07 10:46:16 +08:00

125 lines
3.8 KiB
TypeScript

/**
* HAPI MCP server
* Provides HAPI CLI specific tools including chat session title management
*/
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createServer } from "node:http";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { AddressInfo } from "node:net";
import { z } from "zod";
import { logger } from "@/ui/logger";
import { ApiSessionClient } from "@/api/apiSession";
import { randomUUID } from "node:crypto";
type StartHappyServerOptions = {
emitTitleSummary?: boolean;
};
export async function startHappyServer(client: ApiSessionClient, options: StartHappyServerOptions = {}) {
const emitTitleSummary = options.emitTitleSummary ?? true;
// Handler that sends title updates via the client
const handler = async (title: string) => {
logger.debug('[hapiMCP] Changing title to:', title);
try {
if (emitTitleSummary) {
// Send title as a summary message, similar to title generator.
client.sendClaudeSessionMessage({
type: 'summary',
summary: title,
leafUuid: randomUUID()
});
}
return { success: true };
} catch (error) {
return { success: false, error: String(error) };
}
};
//
// Create the MCP server
//
const mcp = new McpServer({
name: "HAPI MCP",
version: "1.0.0",
});
// Avoid TS instantiation depth issues by widening the schema type.
const changeTitleInputSchema: z.ZodTypeAny = z.object({
title: z.string().describe('The new title for the chat session'),
});
mcp.registerTool<any, any>('change_title', {
description: 'Change the title of the current chat session',
title: 'Change Chat Title',
inputSchema: changeTitleInputSchema,
}, async (args: { title: string }) => {
const response = await handler(args.title);
logger.debug('[hapiMCP] Response:', response);
if (response.success) {
return {
content: [
{
type: 'text' as const,
text: `Successfully changed chat title to: "${args.title}"`,
},
],
isError: false,
};
} else {
return {
content: [
{
type: 'text' as const,
text: `Failed to change chat title: ${response.error || 'Unknown error'}`,
},
],
isError: true,
};
}
});
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);
//
// Create the HTTP server
//
const server = createServer(async (req, res) => {
try {
await transport.handleRequest(req, res);
} catch (error) {
logger.debug("Error handling request:", error);
if (!res.headersSent) {
res.writeHead(500).end();
}
}
});
const baseUrl = await new Promise<URL>((resolve) => {
server.listen(0, "127.0.0.1", () => {
const addr = server.address() as AddressInfo;
resolve(new URL(`http://127.0.0.1:${addr.port}`));
});
});
return {
url: baseUrl.toString(),
toolNames: ['change_title'],
stop: () => {
logger.debug('[hapiMCP] Stopping server');
mcp.close();
server.close();
}
}
}