utilcn
blockchatgpt-app-backendsource: ok · hit
ChatGPT MCP Backend
MCP server bootstrap, widget asset builder, and tool registration for the ChatGPT widget.
npx shadcn add "https://utilcn.dev/r/chatgpt-app-backend.json"free
Where it goes
chat
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@modelcontextprotocol/sdkzod@3
Files3 · source cached 10/14/2026, 1:28:04 PM
registry/default/chatgpt-app/mcp-server.ts
import {
createServer,
type IncomingMessage,
type ServerResponse,
} from 'node:http';
import { URL } from 'node:url';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { registerAdd } from '@/registry/default/chatgpt-app/add';
function createUtilCnServer(): McpServer {
const server = new McpServer({
name: 'utilcn-mcp',
version: '0.1.0',
});
registerAdd(server);
return server;
}
type SessionRecord = {
server: McpServer;
transport: SSEServerTransport;
};
const sessions = new Map<string, SessionRecord>();
const ssePath = '/mcp';
const postPath = '/mcp/messages';
async function handleSseRequest(res: ServerResponse) {
res.setHeader('Access-Control-Allow-Origin', '*');
const server = createUtilCnServer();
const transport = new SSEServerTransport(postPath, res);
const sessionId = transport.sessionId;
sessions.set(sessionId, { server, transport });
transport.onclose = () => {
sessions.delete(sessionId);
};
transport.onerror = (error) => {
console.error('SSE transport error', error);
};
try {
await server.connect(transport);
} catch (error) {
sessions.delete(sessionId);
console.error('Failed to start SSE session', error);
if (!res.headersSent) {
res.writeHead(500).end('Failed to establish SSE connection');
}
}
}
async function handlePostMessage(
req: IncomingMessage,
res: ServerResponse,
url: URL,
) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
const sessionId = url.searchParams.get('sessionId');
if (!sessionId) {
res.writeHead(400).end('Missing sessionId query parameter');
return;
}
const session = sessions.get(sessionId);
if (!session) {
res.writeHead(404).end('Unknown session');
return;
}
try {
await session.transport.handlePostMessage(req, res);
} catch (error) {
console.error('Failed to process message', error);
if (!res.headersSent) {
res.writeHead(500).end('Failed to process message');
}
}
}
const portEnv = Number(process.env.PORT ?? 8000);
const port = Number.isFinite(portEnv) ? portEnv : 8000;
const httpServer = createServer(
async (req: IncomingMessage, res: ServerResponse) => {
if (!req.url) {
res.writeHead(400).end('Missing URL');
return;
}
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
if (
req.method === 'OPTIONS' &&
(url.pathname === ssePath || url.pathname === postPath)
) {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'content-type',
});
res.end();
return;
}
if (req.method === 'GET' && url.pathname === ssePath) {
await handleSseRequest(res);
return;
}
if (req.method === 'POST' && url.pathname === postPath) {
await handlePostMessage(req, res, url);
return;
}
res.writeHead(404).end('Not Found');
},
);
httpServer.on('clientError', (err: Error, socket) => {
console.error('HTTP client error', err);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
httpServer.listen(port, () => {
console.log(`UtilCN MCP server listening on http://localhost:${port}`);
console.log(` SSE stream: GET http://localhost:${port}${ssePath}`);
console.log(
` Message post endpoint: POST http://localhost:${port}${postPath}?sessionId=...`,
);
});
registry/default/chatgpt-app/widget-util.ts
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
export type UtilCnWidget = {
id: string;
title: string;
templateUri: string;
invoking: string;
invoked: string;
html: string;
responseText: string;
toolDescription: string;
widgetDescription: string;
inputSchema: {
type: 'object';
properties?: Record<string, unknown>;
required?: readonly string[];
additionalProperties: boolean;
};
};
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ASSETS_DIR = path.resolve(__dirname, '..', '..', '..', 'web', 'assets'); // <-- TODO: Get to the built chatgpt `assets` directory of your project which will appear after running `pnpm run build` in your front end project
export function buildWidgetHtml(componentName: string): string {
if (!fs.existsSync(ASSETS_DIR)) {
throw new Error(
`Widget assets not found. Expected directory ${ASSETS_DIR}. Run "pnpm run build" before starting the server.`,
);
}
const directPath = path.join(ASSETS_DIR, `${componentName}.html`);
let htmlContents: string | null = null;
if (fs.existsSync(directPath)) {
htmlContents = fs.readFileSync(directPath, 'utf8');
} else {
const candidates = fs
.readdirSync(ASSETS_DIR)
.filter(
(file) =>
file.startsWith(`${componentName}-`) && file.endsWith('.html'),
)
.sort();
const fallback = candidates[candidates.length - 1];
if (fallback) {
htmlContents = fs.readFileSync(path.join(ASSETS_DIR, fallback), 'utf8');
}
}
if (!htmlContents) {
throw new Error(
`Widget HTML for "${componentName}" not found in ${ASSETS_DIR}. Run "pnpm run build" to generate the assets.`,
);
}
return htmlContents;
}
export function widgetMeta(widget: UtilCnWidget) {
return {
'openai/outputTemplate': widget.templateUri,
'openai/widgetDescription': widget.widgetDescription,
'openai/toolInvocation/invoking': widget.invoking,
'openai/toolInvocation/invoked': widget.invoked,
'openai/widgetAccessible': true,
'openai/resultCanProduceWidget': true,
} as const;
}
export function registerWidgetResource(
server: McpServer,
widget: UtilCnWidget,
) {
server.registerResource(
widget.id,
widget.templateUri,
{
_meta: {
'openai/widgetDescription': widget.widgetDescription,
},
},
async () => ({
contents: [
{
uri: widget.templateUri,
mimeType: 'text/html+skybridge',
text: widget.html,
_meta: {
'openai/widgetDescription': widget.widgetDescription,
},
},
],
}),
);
}
registry/default/chatgpt-app/add.ts
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import z from 'zod';
import {
buildWidgetHtml,
registerWidgetResource,
type UtilCnWidget,
widgetMeta,
} from '@/registry/default/chatgpt-app/widget-util';
const addInputSchema = {
type: 'object',
properties: {
a: { type: 'number', description: 'First number' },
b: { type: 'number', description: 'Second number' },
},
additionalProperties: false,
} as const;
const addWidget: UtilCnWidget = {
id: 'add',
title: 'Adding 2 Numbers Together',
templateUri: 'ui://widget/add.html',
invoking: 'Summing the 2 numbers...',
invoked: 'The numbers have been added together!',
html: buildWidgetHtml('add'),
responseText: 'Displayed the two given numbers added together',
toolDescription:
'Use this when the user asks about adding two numbers together',
widgetDescription:
'A card that displays addition of two given numbers and their sum',
inputSchema: addInputSchema,
};
const addInputParser = z.object({
a: z.number(),
b: z.number(),
});
export function registerAdd(server: McpServer) {
registerWidgetResource(server, addWidget);
server.registerTool(
'add',
{
title: addWidget.title,
description: addWidget.toolDescription,
inputSchema: { a: z.number(), b: z.number() },
annotations: {
readOnlyHint: true,
},
_meta: widgetMeta(addWidget),
},
(args) => {
const typedArgs = addInputParser.parse(args);
const sum = typedArgs.a + typedArgs.b;
return {
content: [
{
type: 'text',
text: addWidget.responseText,
},
],
structuredContent: {
a: typedArgs.a,
b: typedArgs.b,
sum,
},
_meta: widgetMeta(addWidget),
};
},
);
}