shadimage
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),
      };
    },
  );
}

More from utilcn

Browse all components in this library →