{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chatgpt-app-backend",
  "type": "registry:block",
  "title": "ChatGPT MCP Backend",
  "description": "MCP server bootstrap, widget asset builder, and tool registration for the ChatGPT widget.",
  "dependencies": [
    "@modelcontextprotocol/sdk",
    "zod@3"
  ],
  "files": [
    {
      "path": "registry/default/chatgpt-app/mcp-server.ts",
      "content": "import {\n  createServer,\n  type IncomingMessage,\n  type ServerResponse,\n} from 'node:http';\nimport { URL } from 'node:url';\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\n\nimport { registerAdd } from '@/registry/default/chatgpt-app/add';\n\nfunction createUtilCnServer(): McpServer {\n  const server = new McpServer({\n    name: 'utilcn-mcp',\n    version: '0.1.0',\n  });\n\n  registerAdd(server);\n  return server;\n}\n\ntype SessionRecord = {\n  server: McpServer;\n  transport: SSEServerTransport;\n};\n\nconst sessions = new Map<string, SessionRecord>();\n\nconst ssePath = '/mcp';\nconst postPath = '/mcp/messages';\n\nasync function handleSseRequest(res: ServerResponse) {\n  res.setHeader('Access-Control-Allow-Origin', '*');\n  const server = createUtilCnServer();\n  const transport = new SSEServerTransport(postPath, res);\n  const sessionId = transport.sessionId;\n\n  sessions.set(sessionId, { server, transport });\n\n  transport.onclose = () => {\n    sessions.delete(sessionId);\n  };\n\n  transport.onerror = (error) => {\n    console.error('SSE transport error', error);\n  };\n\n  try {\n    await server.connect(transport);\n  } catch (error) {\n    sessions.delete(sessionId);\n    console.error('Failed to start SSE session', error);\n    if (!res.headersSent) {\n      res.writeHead(500).end('Failed to establish SSE connection');\n    }\n  }\n}\n\nasync function handlePostMessage(\n  req: IncomingMessage,\n  res: ServerResponse,\n  url: URL,\n) {\n  res.setHeader('Access-Control-Allow-Origin', '*');\n  res.setHeader('Access-Control-Allow-Headers', 'content-type');\n  const sessionId = url.searchParams.get('sessionId');\n\n  if (!sessionId) {\n    res.writeHead(400).end('Missing sessionId query parameter');\n    return;\n  }\n\n  const session = sessions.get(sessionId);\n\n  if (!session) {\n    res.writeHead(404).end('Unknown session');\n    return;\n  }\n\n  try {\n    await session.transport.handlePostMessage(req, res);\n  } catch (error) {\n    console.error('Failed to process message', error);\n    if (!res.headersSent) {\n      res.writeHead(500).end('Failed to process message');\n    }\n  }\n}\n\nconst portEnv = Number(process.env.PORT ?? 8000);\nconst port = Number.isFinite(portEnv) ? portEnv : 8000;\n\nconst httpServer = createServer(\n  async (req: IncomingMessage, res: ServerResponse) => {\n    if (!req.url) {\n      res.writeHead(400).end('Missing URL');\n      return;\n    }\n\n    const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);\n\n    if (\n      req.method === 'OPTIONS' &&\n      (url.pathname === ssePath || url.pathname === postPath)\n    ) {\n      res.writeHead(204, {\n        'Access-Control-Allow-Origin': '*',\n        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',\n        'Access-Control-Allow-Headers': 'content-type',\n      });\n      res.end();\n      return;\n    }\n\n    if (req.method === 'GET' && url.pathname === ssePath) {\n      await handleSseRequest(res);\n      return;\n    }\n\n    if (req.method === 'POST' && url.pathname === postPath) {\n      await handlePostMessage(req, res, url);\n      return;\n    }\n\n    res.writeHead(404).end('Not Found');\n  },\n);\n\nhttpServer.on('clientError', (err: Error, socket) => {\n  console.error('HTTP client error', err);\n  socket.end('HTTP/1.1 400 Bad Request\\r\\n\\r\\n');\n});\n\nhttpServer.listen(port, () => {\n  console.log(`UtilCN MCP server listening on http://localhost:${port}`);\n  console.log(`  SSE stream: GET http://localhost:${port}${ssePath}`);\n  console.log(\n    `  Message post endpoint: POST http://localhost:${port}${postPath}?sessionId=...`,\n  );\n});\n",
      "type": "registry:file",
      "target": "src/mcp-server.ts"
    },
    {
      "path": "registry/default/chatgpt-app/widget-util.ts",
      "content": "import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nexport type UtilCnWidget = {\n  id: string;\n  title: string;\n  templateUri: string;\n  invoking: string;\n  invoked: string;\n  html: string;\n  responseText: string;\n  toolDescription: string;\n  widgetDescription: string;\n  inputSchema: {\n    type: 'object';\n    properties?: Record<string, unknown>;\n    required?: readonly string[];\n    additionalProperties: boolean;\n  };\n};\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst 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\n\nexport function buildWidgetHtml(componentName: string): string {\n  if (!fs.existsSync(ASSETS_DIR)) {\n    throw new Error(\n      `Widget assets not found. Expected directory ${ASSETS_DIR}. Run \"pnpm run build\" before starting the server.`,\n    );\n  }\n\n  const directPath = path.join(ASSETS_DIR, `${componentName}.html`);\n  let htmlContents: string | null = null;\n\n  if (fs.existsSync(directPath)) {\n    htmlContents = fs.readFileSync(directPath, 'utf8');\n  } else {\n    const candidates = fs\n      .readdirSync(ASSETS_DIR)\n      .filter(\n        (file) =>\n          file.startsWith(`${componentName}-`) && file.endsWith('.html'),\n      )\n      .sort();\n    const fallback = candidates[candidates.length - 1];\n    if (fallback) {\n      htmlContents = fs.readFileSync(path.join(ASSETS_DIR, fallback), 'utf8');\n    }\n  }\n\n  if (!htmlContents) {\n    throw new Error(\n      `Widget HTML for \"${componentName}\" not found in ${ASSETS_DIR}. Run \"pnpm run build\" to generate the assets.`,\n    );\n  }\n\n  return htmlContents;\n}\n\nexport function widgetMeta(widget: UtilCnWidget) {\n  return {\n    'openai/outputTemplate': widget.templateUri,\n    'openai/widgetDescription': widget.widgetDescription,\n    'openai/toolInvocation/invoking': widget.invoking,\n    'openai/toolInvocation/invoked': widget.invoked,\n    'openai/widgetAccessible': true,\n    'openai/resultCanProduceWidget': true,\n  } as const;\n}\n\nexport function registerWidgetResource(\n  server: McpServer,\n  widget: UtilCnWidget,\n) {\n  server.registerResource(\n    widget.id,\n    widget.templateUri,\n    {\n      _meta: {\n        'openai/widgetDescription': widget.widgetDescription,\n      },\n    },\n    async () => ({\n      contents: [\n        {\n          uri: widget.templateUri,\n          mimeType: 'text/html+skybridge',\n          text: widget.html,\n          _meta: {\n            'openai/widgetDescription': widget.widgetDescription,\n          },\n        },\n      ],\n    }),\n  );\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/chatgpt-app/add.ts",
      "content": "import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport z from 'zod';\nimport {\n  buildWidgetHtml,\n  registerWidgetResource,\n  type UtilCnWidget,\n  widgetMeta,\n} from '@/registry/default/chatgpt-app/widget-util';\n\nconst addInputSchema = {\n  type: 'object',\n  properties: {\n    a: { type: 'number', description: 'First number' },\n    b: { type: 'number', description: 'Second number' },\n  },\n  additionalProperties: false,\n} as const;\n\nconst addWidget: UtilCnWidget = {\n  id: 'add',\n  title: 'Adding 2 Numbers Together',\n  templateUri: 'ui://widget/add.html',\n  invoking: 'Summing the 2 numbers...',\n  invoked: 'The numbers have been added together!',\n  html: buildWidgetHtml('add'),\n  responseText: 'Displayed the two given numbers added together',\n  toolDescription:\n    'Use this when the user asks about adding two numbers together',\n  widgetDescription:\n    'A card that displays addition of two given numbers and their sum',\n  inputSchema: addInputSchema,\n};\n\nconst addInputParser = z.object({\n  a: z.number(),\n  b: z.number(),\n});\n\nexport function registerAdd(server: McpServer) {\n  registerWidgetResource(server, addWidget);\n\n  server.registerTool(\n    'add',\n    {\n      title: addWidget.title,\n      description: addWidget.toolDescription,\n      inputSchema: { a: z.number(), b: z.number() },\n      annotations: {\n        readOnlyHint: true,\n      },\n      _meta: widgetMeta(addWidget),\n    },\n    (args) => {\n      const typedArgs = addInputParser.parse(args);\n\n      const sum = typedArgs.a + typedArgs.b;\n      return {\n        content: [\n          {\n            type: 'text',\n            text: addWidget.responseText,\n          },\n        ],\n        structuredContent: {\n          a: typedArgs.a,\n          b: typedArgs.b,\n          sum,\n        },\n        _meta: widgetMeta(addWidget),\n      };\n    },\n  );\n}\n",
      "type": "registry:file",
      "target": "tools/add.ts"
    }
  ],
  "docs": "Install this block into your backend project. Run the `build-chatgpt-widgets.ts` script from the frontend block to prebuild widget assets before starting `mcp-server.ts`. Use the `tools/add.ts` tool to register the sample addition tool with your MCP server."
}