{"id":44859073,"url":"https://github.com/pomerium/chatgpt-app-typescript-template","last_synced_at":"2026-02-17T08:37:59.609Z","repository":{"id":330032114,"uuid":"1121202440","full_name":"pomerium/chatgpt-app-typescript-template","owner":"pomerium","description":"ChatGPT app template using Pomerium, OpenAI Apps SDK and Model Context Protocol (MCP), with a Node.js server and React widgets.","archived":false,"fork":false,"pushed_at":"2026-02-05T20:47:39.000Z","size":2199,"stargazers_count":15,"open_issues_count":1,"forks_count":4,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-06T06:22:21.095Z","etag":null,"topics":["apps-sdk","chatgpt","express","identity-aware-proxy","mcp","model-context-protocol","openai","pomerium","proxy","react","template","typescript","vite","widgets","zero-trust"],"latest_commit_sha":null,"homepage":"https://www.pomerium.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pomerium.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2025-12-22T15:47:37.000Z","updated_at":"2026-02-05T15:36:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pomerium/chatgpt-app-typescript-template","commit_stats":null,"previous_names":["pomerium/chatgpt-app-typescript-template"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pomerium/chatgpt-app-typescript-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomerium%2Fchatgpt-app-typescript-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomerium%2Fchatgpt-app-typescript-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomerium%2Fchatgpt-app-typescript-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomerium%2Fchatgpt-app-typescript-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pomerium","download_url":"https://codeload.github.com/pomerium/chatgpt-app-typescript-template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pomerium%2Fchatgpt-app-typescript-template/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29537915,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T08:11:05.436Z","status":"ssl_error","status_checked_at":"2026-02-17T08:09:38.860Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apps-sdk","chatgpt","express","identity-aware-proxy","mcp","model-context-protocol","openai","pomerium","proxy","react","template","typescript","vite","widgets","zero-trust"],"created_at":"2026-02-17T08:37:59.517Z","updated_at":"2026-02-17T08:37:59.584Z","avatar_url":"https://github.com/pomerium.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ChatGPT App Template\n\nA well-architected starter template demonstrating best practices for building [ChatGPT apps](https://developers.openai.com/apps-sdk/) using the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) with [React](https://react.dev/) widgets. It leverages TypeScript, Tailwind CSS v4, Pino logging, Storybook, and Vitest for a robust development experience.\n\n## Features\n\n- **MCP Server** - Node.js server with base `Server` class (preserves `_meta` fields)\n- **Echo Tool** - Example tool with [Zod](https://zod.dev/) validation and widget response\n- **React Widgets** - Interactive Echo component with `callTool` demo\n- **[Pino](https://getpino.io/) Logging** - Structured logging with pretty printing in development\n- **TypeScript** - Strict mode with ES2023 target\n- **[Tailwind CSS v4](https://tailwindcss.com/)** - Modern styling with dark mode support\n- **[Storybook](https://storybook.js.org/)** - Component development with a11y addon\n- **Testing** - [Vitest](https://vitest.dev/) for server and widgets with accessibility checks\n- **Build Optimizations** - Parallel builds, content hashing, compression\n- **[Docker](https://www.docker.com/)** - Multi-stage builds with health checks\n- **Production Ready** - Session management, graceful shutdown, error handling\n\n## Architecture\n\n```mermaid\ngraph TD\n    A[ChatGPT] --\u003e|HTTPStreamable| B[MCP Server\u003cbr/\u003eNode.js + Express]\n    B --\u003e|_meta.outputTemplate| C[Widget\u003cbr/\u003eReact in iframe]\n\n    B -.-\u003e B1[Echo Tool]\n    B -.-\u003e B2[Resource Registration]\n    B -.-\u003e B3[text/html+skybridge\u003cbr/\u003eMIME type]\n\n    C -.-\u003e C1[Reads window.openai.toolOutput]\n    C -.-\u003e C2[Calls window.openai.callTool]\n    C -.-\u003e C3[Theme, displayMode, safeArea]\n\n    style A fill:#e1f5ff\n    style B fill:#fff4e6\n    style C fill:#f3e5f5\n```\n\n## Quick Start\n\n**Setup time: ~5 minutes (first time)**\n\n### Prerequisites\n\n- **[Node.js](https://nodejs.org/) 22+** (required for ES2023 support)\n  - Verify: `node -v` (should show v22.0.0 or higher)\n- **npm 10+** (ships with Node 22)\n  - Verify: `npm -v` (should show v10.0.0 or higher)\n\n**Supported platforms:** macOS, Linux, Windows (via WSL2)\n\n### Installation \u0026 Setup\n\n```bash\ngit clone https://github.com/pomerium/chatgpt-app-typescript-template your-chatgpt-app\ncd your-chatgpt-app\nnpm install\nnpm run dev\n```\n\nThis starts both the MCP server and widget dev server:\n\n- **MCP Server**: `http://localhost:8080`\n- **Widget Assets**: `http://localhost:4444`\n\n\u003e **Note:** The MCP server is a backend service. To test it, follow the ChatGPT connection steps below or use `npm run inspect` for local testing.\n\nYou should see output indicating both servers are running successfully:\n\n```\n❯ npm run dev\n\n\u003e chatgpt-app-typescript-template@1.0.0 dev\n\u003e concurrently \"npm run dev:server\" \"npm run dev:widgets\"\n\n[1]\n[1] \u003e chatgpt-app-typescript-template@1.0.0 dev:widgets\n[1] \u003e npm run dev --workspace=widgets\n[1]\n[0]\n[0] \u003e chatgpt-app-typescript-template@1.0.0 dev:server\n[0] \u003e npm run dev --workspace=server\n[0]\n[1]\n[1] \u003e chatgpt-app-widgets@1.0.0 dev\n[1] \u003e vite\n[1]\n[0]\n[0] \u003e chatgpt-app-server@1.0.0 dev\n[0] \u003e tsx watch src/server.ts\n[0]\n[1]\n[1] Found 1 widget(s):\n[1]   - echo\n[1]\n[1]\n[1]   VITE v6.4.1  ready in 151 ms\n[1]\n[1]   ➜  Local:   http://localhost:4444/\n[1]   ➜  Network: use --host to expose\n[0] [12:45:12] INFO: Starting ChatGPT App Template server\n[0]     port: 8080\n[0]     nodeEnv: \"development\"\n[0]     logLevel: \"info\"\n[0]     assetsDir: \"/Users/nicktaylor/dev/oss/chatgpt-app-typescript-template/assets\"\n[0] [12:45:12] INFO: Server started successfully\n[0]     port: 8080\n[0]     mcpEndpoint: \"http://localhost:8080/mcp\"\n[0]     healthEndpoint: \"http://localhost:8080/health\"\n```\n\n### Connect to ChatGPT\n\nTo test your app in ChatGPT, you need to expose your local server publicly. The fastest way is using [Pomerium's SSH tunnel](https://www.pomerium.com/docs/tcp/ssh):\n\n**1. Create a public tunnel** (in a new terminal, keep `npm run dev` running):\n\n```bash\nssh -R 0 pom.run\n```\n\n**First-time setup:**\n\n1. You'll see a sign-in URL in your terminal:\n\n   ```\n   Please sign in with hosted to continue\n   https://data-plane-us-central1-1.dataplane.pomerium.com/.pomerium/sign_in?user_code=some-code\n   ```\n\n2. Click the link and sign up\n3. Authorize via the Pomerium OAuth flow\n4. Your terminal will display connection details:\n\n![Pomerium SSH Tunnel UI](docs/images/pomerium-tui.png)\n\n**2. Find your public URL:**\n\nLook for the **Port Forward Status** section showing:\n\n- **Status**: `ACTIVE` (tunnel is running)\n- **Remote**: `https://template.first-wallaby-240.pom.run` (your unique URL)\n- **Local**: `http://localhost:8080` (your local server)\n\n**3. Add to ChatGPT:**\n\n1. [Enable ChatGPT apps dev mode](https://platform.openai.com/docs/guides/developer-mode) in your ChatGPT settings\n2. Go to: **Settings → Connectors → Add Connector**\n3. Enter your Remote URL + `/mcp`, e.g. `https://template.first-wallaby-240.pom.run/mcp`\n4. Save the connector\n\n**4. Test it:**\n\n1. Start a new chat in ChatGPT\n2. Add your app to the chat\n3. Send: `echo today is a great day`\n4. You should see the message displayed in an interactive widget\n\n![Echo tool in action](docs/images/echo-tool-widget.gif)\n\nThe tunnel stays active as long as the SSH session is running.\n\n### Success! What's Next?\n\nNow that your app is working, you can:\n\n- **[Customize the echo tool](#adding-new-tools)** - Modify the example tool or add your own logic\n- **[Create a new widget](#widget-development)** - Build custom UI components for your tools\n- **[Test locally](#local-testing-with-mcp-inspector)** - Use `npm run inspect` for debugging without ChatGPT\n- **[Deploy to production](#production-deployment)** - Take your app live when ready\n\n## Available Commands\n\n### Development\n\n```bash\n# Start everything (server + widgets in watch mode)\nnpm run dev\n\n# Start only MCP server (watch mode)\nnpm run dev:server\n\n# Start only widget dev server\nnpm run dev:widgets\n\n# Test with MCP Inspector\nnpm run inspect\n```\n\n### Building\n\n```bash\n# Full production build (widgets + server)\nnpm run build\n\n# Build only widgets\nnpm run build:widgets\n\n# Build only server\nnpm run build:server\n```\n\n### Testing\n\n```bash\n# Run all tests\nnpm test\n\n# Run server tests only\nnpm run test:server\n\n# Run widget tests only\nnpm run test:widgets\n\n# Run tests with coverage\nnpm run test:coverage\n```\n\n### Code Quality\n\n```bash\n# Lint all TypeScript files\nnpm run lint\n\n# Format code with Prettier\nnpm run format\n\n# Check formatting without modifying\nnpm run format:check\n\n# Type check all workspaces\nnpm run type-check\n```\n\n### Storybook\n\n```bash\n# Run Storybook dev server\nnpm run storybook\n\n# Build Storybook for production\nnpm run build:storybook\n```\n\n### Testing Your App\n\n#### 1. Local Testing with MCP Inspector\n\n```bash\nnpm run inspect\n```\n\nThis opens a browser interface to:\n\n- List available tools\n- Test tool invocations\n- Inspect responses and metadata\n- Verify widget resources load correctly\n\n#### 2. Connect from ChatGPT\n\nFor complete ChatGPT connection instructions, see the [Quick Start: Connect to ChatGPT](#connect-to-chatgpt) section above.\n\n**Already connected?** After making code changes:\n\n1. **Settings → Connectors → Your App → Refresh**\n2. This reloads tool definitions and metadata\n\n**Production Setup:**\n\nWhen deploying to production:\n\n1. Deploy your server to a public URL (see [Production Deployment](#production-deployment))\n2. In ChatGPT: **Settings → Connectors → Add Connector**\n3. Enter your server URL: `https://your-domain.com/mcp`\n4. Test the `echo` tool in ChatGPT\n\n## Project Structure\n\n```\nchatgpt-app-template/\n├── server/                  # MCP server\n│   ├── src/\n│   │   ├── server.ts       # Main server with echo tool\n│   │   ├── types.ts        # Type definitions\n│   │   └── utils/\n│   │       └── session.ts  # Session management\n│   ├── tests/\n│   │   └── echo-tool.test.ts\n│   └── package.json        # Server dependencies\n│\n├── widgets/                 # React widgets\n│   ├── src/\n│   │   ├── widgets/\n│   │   │   └── echo.tsx           # Widget entry (includes mounting code)\n│   │   ├── echo/\n│   │   │   ├── Echo.tsx           # Shared components\n│   │   │   ├── Echo.stories.tsx\n│   │   │   └── styles.css\n│   │   ├── components/\n│   │   │   └── ui/              # ShadCN components\n│   │   ├── hooks/\n│   │   │   └── use-openai-global.ts\n│   │   └── types/\n│   │       └── openai.d.ts\n│   ├── .storybook/         # Storybook config\n│   └── package.json        # Widget dependencies\n│\n├── assets/                  # Asset build artifacts\n│   ├── echo.html\n│   ├── echo-[hash].js\n│   └── echo-[hash].css\n│\n├── scripts/\n│   └── build-all.mts       # Parallel widget builds\n│\n├── docker/\n│   ├── Dockerfile          # Multi-stage build\n│   └── docker-compose.yml\n│\n└── package.json            # Root workspace\n```\n\n## Adding New Tools\n\n### 1. Define Tool Schema\n\n```typescript\n// server/src/server.ts\n\nconst myTool: Tool = {\n  name: 'my_tool',\n  description: 'Does something cool',\n  inputSchema: {\n    type: 'object',\n    properties: {\n      input: { type: 'string', description: 'Tool input' },\n    },\n    required: ['input'],\n  },\n};\n```\n\n### 2. Implement Tool Handler\n\n```typescript\n// In CallToolRequestSchema handler\n\nif (name === 'my_tool') {\n  const args = MyToolInputSchema.parse(request.params.arguments);\n\n  return {\n    content: [{ type: 'text', text: 'Result' }],\n    structuredContent: {\n      result: args.input,\n    },\n    _meta: {\n      outputTemplate: {\n        type: 'resource',\n        resource: { uri: 'ui://my-widget' },\n      },\n    },\n  };\n}\n```\n\n### 3. Create Widget\n\nCreate `widgets/src/widgets/my-widget.tsx`:\n\n```tsx\n// widgets/src/widgets/my-widget.tsx\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { useOpenAiGlobal } from '../hooks/use-openai-global';\n\nfunction MyWidget() {\n  const toolOutput = useOpenAiGlobal('toolOutput');\n  const theme = useOpenAiGlobal('theme');\n\n  return (\n    \u003cdiv className={theme === 'dark' ? 'dark' : ''}\u003e\n      \u003ch1\u003eMy Widget\u003c/h1\u003e\n      \u003cpre\u003e{JSON.stringify(toolOutput, null, 2)}\u003c/pre\u003e\n    \u003c/div\u003e\n  );\n}\n\n// Mounting code - required at the bottom of each widget file\nconst rootElement = document.getElementById('my-widget-root');\nif (rootElement) {\n  createRoot(rootElement).render(\n    \u003cStrictMode\u003e\n      \u003cMyWidget /\u003e\n    \u003c/StrictMode\u003e\n  );\n}\n```\n\n### 4. Register Widget Resource\n\n```typescript\n// In ReadResourceRequestSchema handler\n\nif (uri === 'ui://my-widget') {\n  const html = readWidgetHtml('my-widget');\n  return {\n    contents: [\n      {\n        uri,\n        mimeType: 'text/html+skybridge', // CRITICAL!\n        text: html,\n      },\n    ],\n  };\n}\n```\n\n### 5. Build\n\n```bash\nnpm run build:widgets\nnpm run dev:server\n```\n\nThe build script auto-discovers widgets in `widgets/src/widgets/*.{tsx,jsx}` and bundles them with their mounting code\n\n## Widget Development\n\n### Widget Pattern\n\nWidgets include both the component and mounting code:\n\n**1. Create widget entry point** in `widgets/src/widgets/[name].tsx`:\n\n```tsx\nimport { StrictMode } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { useOpenAiGlobal } from '../hooks/use-openai-global';\n\nfunction MyWidget() {\n  const toolOutput = useOpenAiGlobal('toolOutput');\n  return \u003cdiv\u003eWidget content\u003c/div\u003e;\n}\n\n// Mounting code - required\nconst rootElement = document.getElementById('my-widget-root');\nif (rootElement) {\n  createRoot(rootElement).render(\n    \u003cStrictMode\u003e\n      \u003cMyWidget /\u003e\n    \u003c/StrictMode\u003e\n  );\n}\n```\n\n**2. Build discovers and bundles widget**:\n\n```bash\nnpm run build:widgets\n```\n\n**3. Widget available as** `ui://my-widget`\n\nThe build system:\n\n- Auto-discovers all files in `widgets/src/widgets/*.{tsx,jsx}`\n- Bundles the component and mounting code together\n- Creates content-hashed bundles and HTML templates\n\n### window.openai API Reference\n\n#### State \u0026 Data\n\n```typescript\nconst toolOutput = useOpenAiGlobal('toolOutput'); // Tool's structured content\nconst toolInput = useOpenAiGlobal('toolInput'); // Tool arguments\nconst widgetState = useOpenAiGlobal('widgetState'); // Persistent state from host\n```\n\n**Setting State**: Use `window.openai.setWidgetState(newState)` to persist state.\n\n**State Limits**: Keep `widgetState` under **4,000 tokens** for optimal performance.\n\n#### Context Signals\n\n```typescript\nconst theme = useOpenAiGlobal('theme'); // 'light' | 'dark'\nconst displayMode = useOpenAiGlobal('displayMode'); // 'inline' | 'pip' | 'fullscreen'\nconst maxHeight = useOpenAiGlobal('maxHeight'); // Max height in pixels\nconst safeArea = useOpenAiGlobal('safeArea'); // Insets for responsive layout\nconst viewport = useOpenAiGlobal('viewport'); // { width, height }\nconst locale = useOpenAiGlobal('locale'); // User locale (e.g., 'en-US')\n```\n\n#### Runtime APIs\n\n```typescript\n// Call other tools from widget\nconst result = await window.openai?.callTool('tool_name', { arg: 'value' });\n\n// Toggle display mode\nawait window.openai?.requestDisplayMode({ mode: 'fullscreen' });\n\n// Send follow-up message\nawait window.openai?.sendFollowUpMessage({ prompt: 'Continue...' });\n\n// File operations\nconst { fileId } = await window.openai?.uploadFile(file);\nconst { url } = await window.openai?.getFileDownloadUrl({ fileId });\n\n// Open external links\nwindow.openai?.openExternal({ href: 'https://example.com' });\n\n// Close widget\nawait window.openai?.requestClose();\n```\n\n### Example: Full Widget with Safe Area\n\n```tsx\n// widgets/src/widgets/my-widget.tsx\nimport { StrictMode, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { useOpenAiGlobal } from '../hooks/use-openai-global';\n\nfunction MyWidget() {\n  const toolOutput = useOpenAiGlobal('toolOutput');\n  const theme = useOpenAiGlobal('theme');\n  const safeArea = useOpenAiGlobal('safeArea');\n  const widgetState = useOpenAiGlobal('widgetState');\n\n  const count = widgetState?.count ?? 0;\n\n  const containerStyle = {\n    paddingTop: safeArea?.insets?.top || 0,\n    paddingBottom: safeArea?.insets?.bottom || 0,\n  };\n\n  const handleIncrement = () =\u003e {\n    window.openai?.setWidgetState({ count: count + 1 });\n  };\n\n  return (\n    \u003cdiv style={containerStyle} className={theme === 'dark' ? 'dark' : ''}\u003e\n      \u003ch1\u003eMy Widget\u003c/h1\u003e\n      \u003cp\u003eTool output: {JSON.stringify(toolOutput)}\u003c/p\u003e\n      \u003cbutton onClick={handleIncrement}\u003eCount: {count}\u003c/button\u003e\n    \u003c/div\u003e\n  );\n}\n\n// Mounting code - required at the bottom of each widget file\nconst rootElement = document.getElementById('my-widget-root');\nif (rootElement) {\n  createRoot(rootElement).render(\n    \u003cStrictMode\u003e\n      \u003cMyWidget /\u003e\n    \u003c/StrictMode\u003e\n  );\n}\n```\n\n## Configuration\n\n### Environment Variables\n\nCreate `.env` file (see `.env.example`):\n\n```bash\n# Server\nNODE_ENV=development\nPORT=8080\nLOG_LEVEL=info          # fatal, error, warn, info, debug, trace\n\n# Session Management\nSESSION_MAX_AGE=3600000 # 1 hour in milliseconds\n\n# CORS (development)\nCORS_ORIGIN=*\n\n# Asset Base URL (for CDN)\n# BASE_URL=https://cdn.example.com/assets\n```\n\n### Critical Configuration Notes\n\n#### text/html+skybridge MIME Type\n\n**Required** for widgets to load in ChatGPT:\n\n```typescript\nreturn {\n  contents: [\n    {\n      uri: 'ui://my-widget',\n      mimeType: 'text/html+skybridge', // ← CRITICAL\n      text: html,\n    },\n  ],\n};\n```\n\n#### Bundle Size Limits\n\n- **Widget bundles**: Warn at 500kb (configured in Vite)\n- **Widget state**: Keep under 4,000 tokens for performance\n\n## API Reference\n\n### MCP Server Endpoints\n\n| Endpoint                       | Method | Description                                           |\n| ------------------------------ | ------ | ----------------------------------------------------- |\n| `/health`                      | GET    | Health check (returns status, version, session count) |\n| `/mcp`                         | GET    | SSE connection endpoint for MCP clients               |\n| `/mcp/messages?sessionId=\u003cid\u003e` | POST   | Message handling for MCP protocol                     |\n\n### Echo Tool Schema\n\n```json\n{\n  \"name\": \"echo\",\n  \"description\": \"Echoes back the user's message in an interactive widget\",\n  \"inputSchema\": {\n    \"type\": \"object\",\n    \"properties\": {\n      \"message\": {\n        \"type\": \"string\",\n        \"description\": \"The message to echo back\"\n      }\n    },\n    \"required\": [\"message\"]\n  }\n}\n```\n\n### Tool Response Format\n\n```typescript\n{\n  content: [{ type: 'text', text: 'Human-readable message' }],\n  structuredContent: {\n    // JSON data passed to widget via window.openai.toolOutput\n    echoedMessage: 'Hello',\n    timestamp: '2025-01-...'\n  },\n  _meta: {\n    outputTemplate: {\n      type: 'resource',\n      resource: { uri: 'ui://echo' }\n    }\n  }\n}\n```\n\n## Testing \u0026 Quality Assurance\n\n### Running Tests\n\n```bash\n# Run all tests (server + widgets)\nnpm test\n\n# Run specific workspace tests\nnpm run test:server\nnpm run test:widgets\n\n# Run with coverage report\nnpm run test:coverage\n```\n\n### Test Structure\n\n**Server Tests** (`server/tests/`):\n\n- Input validation with Zod\n- Tool response structure\n- Session management\n- Error handling\n\n**Widget Tests** (`widgets/tests/`):\n\n- Component rendering\n- User interactions\n- Accessibility (a11y) compliance\n- window.openai API mocking\n\n### MCP Inspector Workflow\n\n```bash\n# 1. Start server\nnpm run dev:server\n\n# 2. Build widgets\nnpm run build:widgets\n\n# 3. Test with Inspector\nnpm run inspect\n\n# 4. Verify:\n#    - Tools list correctly\n#    - Tool invocations work\n#    - Widget HTML loads\n#    - structuredContent is correct\n```\n\n## Production Deployment\n\n### Building for Production\n\nThe production build process compiles widgets with optimizations and prepares the server:\n\n```bash\n# Full production build\nnpm run build\n```\n\nThis runs:\n\n1. `npm run build:widgets` - Builds optimized widget bundles with content hashing\n2. `npm run build:server` - Compiles TypeScript server code\n\n**Build outputs:**\n\n- `assets/` - Optimized widget bundles (JS/CSS with content hashes)\n- `server/dist/` - Compiled server code\n\n### Manual Deployment\n\n```bash\n# 1. Install dependencies\nnpm install\n\n# 2. Build for production\nnpm run build\n\n# 3. Start production server\nNODE_ENV=production npm start\n```\n\nThe server will:\n\n- Serve MCP on `http://localhost:8080/mcp`\n- Load pre-built widgets from `assets/`\n- Use structured logging (JSON format)\n- Run with production optimizations\n\n### Docker Deployment\n\n```bash\n# Build image\ndocker build -f docker/Dockerfile -t chatgpt-app:latest .\n\n# Run with docker-compose\ndocker-compose -f docker/docker-compose.yml up -d\n\n# Check logs\ndocker-compose -f docker/docker-compose.yml logs -f\n\n# Health check\ncurl http://localhost:8080/health\n```\n\n### Production Checklist\n\n**Environment Variables:**\n\n- Set `NODE_ENV=production`\n- Configure `CORS_ORIGIN` to your domain (not `*`)\n- Set `LOG_LEVEL=warn` or `error` for production\n- Configure `SESSION_MAX_AGE` based on your use case\n- Set `BASE_URL` if using a CDN for widget assets\n\n**Deployment Requirements:**\n\n- Deploy to a publicly accessible URL (ChatGPT requires HTTPS)\n- Ensure `assets/` directory is deployed with the server\n- Configure reverse proxy if needed (nginx, Caddy, etc.)\n- Set up SSL/TLS certificates\n\n**Monitoring:**\n\n- Monitor `/health` endpoint for server status\n- Set up logging aggregation (Pino outputs JSON in production)\n- Configure alerts for errors and performance issues\n\n## Troubleshooting\n\n### Widget Not Loading\n\n**Symptom**: Widget doesn't appear in ChatGPT\n\n**Solutions**:\n\n1. Verify `text/html+skybridge` MIME type in resource registration\n2. Check assets directory exists: `ls assets/`\n3. Rebuild widgets: `npm run build:widgets`\n4. Restart server and refresh connector in ChatGPT\n\n### Tool Not Listed\n\n**Symptom**: Tool doesn't appear in ChatGPT\n\n**Solutions**:\n\n1. Check server logs for errors\n2. Test with MCP Inspector: `npm run inspect`\n3. Refresh connector: Settings → Connectors → Refresh\n4. Verify tool schema is valid JSON Schema\n\n### Session Issues\n\n**Symptom**: \"Session not found\" errors\n\n**Solutions**:\n\n1. Check `SESSION_MAX_AGE` setting\n2. Review session cleanup logs\n3. Ensure SSE connection is maintained\n4. Check CORS configuration\n\n### Build Failures\n\n**Symptom**: `npm run build:widgets` fails\n\n**Solutions**:\n\n1. Clear node_modules: `rm -rf node_modules \u0026\u0026 npm install`\n2. Check for TypeScript errors: `npm run type-check`\n3. Verify all dependencies installed\n4. Check Node.js version: `node -v` (should be 22+)\n\n### Port Already in Use\n\n**Symptom**: `Error: listen EADDRINUSE: address already in use :::8080`\n\n**Solutions**:\n\n1. Change port in `.env`: `PORT=3001`\n2. Kill existing process: `lsof -ti:8080 | xargs kill`\n\n## Architecture Decisions\n\n### Why Base `Server` Class?\n\nThe template uses the **base `Server` class** from `@modelcontextprotocol/sdk/server/index.js`, not the higher-level `McpServer` class, because:\n\n- ChatGPT apps require the `_meta` field for widget references\n- Higher-level abstractions might strip custom metadata\n- Proven pattern from OpenAI's official examples\n\n### Why Node.js 22 + ES2023?\n\n- Native type stripping support\n- Immutable array methods (`.toSorted()`, `.toReversed()`)\n- Better performance and modern JavaScript features\n\n### Why Tailwind CSS v4?\n\n- Modern, performant, and well-documented\n- Great dark mode support out of the box\n- Smaller bundle sizes with new engine\n\n### Why Pino for Logging?\n\n- Fast, structured logging for production\n- Pretty printing in development\n- Easy integration with monitoring tools\n\n## Contributing\n\nContributions welcome! Please:\n\n1. Follow existing code style (ESLint + Prettier)\n2. Add tests for new features\n3. Update documentation\n4. Ensure TypeScript strict mode compliance\n\n## License\n\nMIT\n\n---\n\n**Built with**:\n\n- [OpenAI Apps SDK](https://developers.openai.com/apps-sdk/)\n- [Model Context Protocol](https://modelcontextprotocol.io/)\n- [React 19](https://react.dev/)\n- [Tailwind CSS v4](https://tailwindcss.com/)\n- [Vite](https://vitejs.dev/)\n- [Pino](https://getpino.io/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpomerium%2Fchatgpt-app-typescript-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpomerium%2Fchatgpt-app-typescript-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpomerium%2Fchatgpt-app-typescript-template/lists"}