AgentStack
MCP verified Apache-2.0 Self-run

Chatgpt App Typescript Template

mcp-pomerium-chatgpt-app-typescript-template · by pomerium

ChatGPT app template using Pomerium, OpenAI Apps SDK and Model Context Protocol (MCP), with a Node.js server and React widgets.

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add mcp-pomerium-chatgpt-app-typescript-template

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Chatgpt App Typescript Template? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

MCP Apps Template

A well-architected starter template demonstrating best practices for building MCP Apps using the Model Context Protocol (MCP) with React widgets. It leverages TypeScript, Tailwind CSS v4, Pino logging, Storybook, and Vitest for a robust development experience.

Features

  • MCP Server - Node.js server with McpServer and MCP Apps helpers
  • Echo Tool - Example tool with Zod validation and UI binding
  • React Widgets - Interactive Echo component with MCP Apps App API demo
  • Display Modes - Inline, picture-in-picture, and fullscreen with runtime toggling via requestDisplayMode()
  • App API Demo - callServerTool, openLink, sendMessage, updateModelContext showcased in the Echo widget
  • UI Capability Negotiation - Server detects host capabilities and falls back to text-only for non-UI clients
  • Inline Widget Assets - Self-contained HTML mode for hosts that sandbox iframes (e.g. Claude.ai)
  • Container Dimensions - Responsive widget sizing using host-provided containerDimensions
  • Mock App - Drop-in createMockApp() helper for testing and Storybook without a live MCP connection
  • Pino Logging - Structured logging with pretty printing in development
  • TypeScript - Strict mode with ES2023 target
  • Tailwind CSS v4 - Modern styling with dark mode support
  • Storybook - Component development with a11y addon
  • Testing - Vitest for server and widgets with accessibility checks
  • Build Optimizations - Parallel builds, content hashing, compression
  • Docker - Multi-stage builds with health checks
  • Production Ready - Session management, graceful shutdown, error handling

Architecture

graph TD
    A[MCP Host] -->|HTTPStreamable| B[MCP ServerNode.js + Express]
    B -->|_meta.ui.resourceUri| C[App ViewReact in iframe]

    B -.-> B1[Echo Tool]
    B -.-> B2[Resource Registration]
    B -.-> B3[text/html;profile=mcp-appMIME type]

    C -.-> C1[Receives App.ontoolresult]
    C -.-> C2[callServerTool, openLink,sendMessage, updateModelContext]
    C -.-> C3[Theme, displayMode, safeArea,containerDimensions]

    style A fill:#e1f5ff
    style B fill:#fff4e6
    style C fill:#f3e5f5

Quick Start

Setup time: ~5 minutes (first time)

Prerequisites

  • Node.js 24+ (required for ES2023 support and native type stripping)
  • Verify: node -v (should show v24.0.0 or higher)
  • npm 11+ (ships with Node 24)
  • Verify: npm -v (should show v10.0.0 or higher)

Supported platforms: macOS, Linux, Windows (via WSL2)

Installation & Setup

git clone https://github.com/pomerium/chatgpt-app-typescript-template your-chatgpt-app
cd your-chatgpt-app
npm install
npm run dev

This starts both the MCP server and widget dev server:

  • MCP Server: http://localhost:8080
  • Widget Assets: http://localhost:4444

> Note: The MCP server is a backend service. To test it, follow the host connection steps below (ChatGPT example) or use npm run inspect for local testing.

You should see output indicating both servers are running successfully:

❯ npm run dev

> chatgpt-app-typescript-template@1.0.0 dev
> concurrently "npm run dev:server" "npm run dev:widgets"

[1]
[1] > chatgpt-app-typescript-template@1.0.0 dev:widgets
[1] > npm run dev --workspace=widgets
[1]
[0]
[0] > chatgpt-app-typescript-template@1.0.0 dev:server
[0] > npm run dev --workspace=server
[0]
[1]
[1] > chatgpt-app-widgets@1.0.0 dev
[1] > vite
[1]
[0]
[0] > chatgpt-app-server@1.0.0 dev
[0] > tsx watch src/server.ts
[0]
[1]
[1] Found 1 widget(s):
[1]   - echo
[1]
[1]
[1]   VITE v6.4.1  ready in 151 ms
[1]
[1]   ➜  Local:   http://localhost:4444/
[1]   ➜  Network: use --host to expose
[0] [12:45:12] INFO: Starting MCP App Template server
[0]     port: 8080
[0]     nodeEnv: "development"
[0]     logLevel: "info"
[0]     assetsDir: "/Users/nicktaylor/dev/oss/chatgpt-app-typescript-template/assets"
[0] [12:45:12] INFO: Server started successfully
[0]     port: 8080
[0]     mcpEndpoint: "http://localhost:8080/mcp"
[0]     healthEndpoint: "http://localhost:8080/health"

Connect to a Host (ChatGPT example)

To test your app in ChatGPT, you need to expose your local server publicly. The fastest way is using Pomerium's SSH tunnel:

1. Create a public tunnel (in a new terminal, keep npm run dev running):

ssh -R 0 pom.run

First-time setup:

  1. You'll see a sign-in URL in your terminal:

`` Please sign in with hosted to continue https://data-plane-us-central1-1.dataplane.pomerium.com/.pomerium/sign_in?user_code=some-code ``

  1. Click the link and sign up
  2. Authorize via the Pomerium OAuth flow
  3. Your terminal will display connection details:

2. Find your public URL:

Look for the Port Forward Status section showing:

  • Status: ACTIVE (tunnel is running)
  • Remote: https://template.first-wallaby-240.pom.run (your unique URL)
  • Local: http://localhost:8080 (your local server)

3. Add to ChatGPT:

  1. Enable MCP apps dev mode in your ChatGPT settings
  2. Go to: Settings → Connectors → Add Connector
  3. Enter your Remote URL + /mcp, e.g. https://template.first-wallaby-240.pom.run/mcp
  4. Save the connector

4. Test it:

  1. Start a new chat in ChatGPT
  2. Add your app to the chat
  3. Send: echo today is a great day
  4. You should see the message displayed in an interactive widget

The tunnel stays active as long as the SSH session is running.

Other hosts: Claude Desktop, VS Code, Goose, and other MCP Apps hosts follow the same pattern—add a connector to your /mcp endpoint and refresh after changes.

Success! What's Next?

Now that your app is working, you can:

  • [Customize the echo tool](#adding-new-tools) - Modify the example tool or add your own logic
  • [Create a new widget](#widget-development) - Build custom UI components for your tools
  • [Test locally](#local-testing-with-mcp-inspector) - Use npm run inspect for debugging without a host
  • [Deploy to production](#production-deployment) - Take your app live when ready

Available Commands

Development

# Start everything (server + widgets in watch mode)
npm run dev

# Inlined assets mode for testing in Claude.ai or sharing remotely via ssh -R 0 pom.run
npm run dev:inline

# Start only MCP server (watch mode)
npm run dev:server

# Start only widget dev server
npm run dev:widgets

# Test with MCP Inspector
npm run inspect

Building

# Full production build (widgets + server)
npm run build

# Build only widgets
npm run build:widgets

# Build only server
npm run build:server

Testing

# Run all tests
npm test

# Run server tests only
npm run test:server

# Run widget tests only
npm run test:widgets

# Run tests with coverage
npm run test:coverage

Code Quality

# Lint all TypeScript files
npm run lint

# Format code with Prettier
npm run format

# Check formatting without modifying
npm run format:check

# Type check all workspaces
npm run type-check

Storybook

# Run Storybook dev server
npm run storybook

# Build Storybook for production
npm run build:storybook

Testing Your App

1. Local Testing with MCP Inspector
npm run inspect

This opens a browser interface to:

  • List available tools
  • Test tool invocations
  • Inspect responses and metadata
  • Verify widget resources load correctly
2. Connect from ChatGPT

For complete ChatGPT connection instructions, see the [Quick Start: Connect to a Host](#connect-to-a-host-chatgpt-example) section above.

Already connected? After making code changes:

  1. Settings → Connectors → Your App → Refresh
  2. This reloads tool definitions and metadata

Production Setup:

When deploying to production:

  1. Deploy your server to a public URL (see [Production Deployment](#production-deployment))
  2. In ChatGPT: Settings → Connectors → Add Connector
  3. Enter your server URL: https://your-domain.com/mcp
  4. Test the echo tool in ChatGPT

Project Structure

chatgpt-app-template/
├── server/                  # MCP server
│   ├── src/
│   │   ├── server.ts       # Main server with echo tool
│   │   ├── types.ts        # Type definitions
│   │   └── utils/
│   │       └── session.ts  # Session management
│   ├── tests/
│   │   └── echo-tool.test.ts
│   └── package.json        # Server dependencies
│
├── widgets/                 # React widgets
│   ├── src/
│   │   ├── widgets/
│   │   │   └── echo.tsx           # Widget entry (includes mounting code)
│   │   ├── echo/
│   │   │   ├── Echo.tsx           # Shared components
│   │   │   ├── Echo.stories.tsx
│   │   │   └── styles.css
│   │   ├── components/
│   │   │   └── ui/              # ShadCN components
│   │   ├── mocks/
│   │   │   └── mock-app.ts       # MCP Apps mock for tests/stories
│   │   └── types/
│   │       └── mcp-app.ts        # MCP Apps types for UI wiring
│   ├── .storybook/         # Storybook config
│   └── package.json        # Widget dependencies
│
├── assets/                  # Asset build artifacts
│   ├── echo.html
│   ├── echo-[hash].js
│   └── echo-[hash].css
│
├── docker/
│   ├── Dockerfile          # Multi-stage build
│   └── docker-compose.yml
│
└── package.json            # Root workspace

Adding New Tools

1. Define Tool Schema

// server/src/types.ts
export const MyToolInputSchema = z.object({
  input: z.string().min(1, 'Input is required'),
});

2. Register Tool (with UI)

registerAppTool(
  server,
  'my_tool',
  {
    title: 'My Tool',
    description: 'Does something cool',
    inputSchema: {
      type: 'object',
      properties: {
        input: { type: 'string', description: 'Tool input' },
      },
      required: ['input'],
    },
    _meta: {
      ui: { resourceUri: 'ui://my-widget' },
    },
  },
  async (args) => {
    const input = MyToolInputSchema.parse(args).input;
    return {
      content: [{ type: 'text', text: 'Result' }],
      structuredContent: { result: input },
    };
  }
);

3. Create Widget

Create widgets/src/widgets/my-widget.tsx:

// widgets/src/widgets/my-widget.tsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from '@modelcontextprotocol/ext-apps';
import { useEffect, useState } from 'react';

function MyWidget() {
  const [toolOutput, setToolOutput] = useState(null);
  const [theme, setTheme] = useState('light');

  useEffect(() => {
    const app = new App({ name: 'MyWidget', version: '1.0.0' });
    app.ontoolresult = (result) => setToolOutput(result.structuredContent ?? null);
    app.onhostcontextchanged = (context) => setTheme(context?.theme ?? 'light');
    app.connect();
  }, []);

  return (
    
      My Widget
      {JSON.stringify(toolOutput, null, 2)}
    
  );
}

// Mounting code - required at the bottom of each widget file
const rootElement = document.getElementById('my-widget-root');
if (rootElement) {
  createRoot(rootElement).render(
    
      
    
  );
}

4. Register Widget Resource

registerAppResource(
  server,
  'ui://my-widget',
  'ui://my-widget',
  { mimeType: RESOURCE_MIME_TYPE },
  async () => ({
    contents: [
      {
        uri: 'ui://my-widget',
        mimeType: RESOURCE_MIME_TYPE,
        text: await readWidgetHtml('my-widget'),
      },
    ],
  })
);

5. Build

npm run build:widgets
npm run dev:server

The build script auto-discovers widgets in widgets/src/widgets/*.{tsx,jsx} and bundles them with their mounting code

Widget Development

Widget Pattern

Widgets include both the component and mounting code:

1. Create widget entry point in widgets/src/widgets/[name].tsx:

import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { useEffect, useState } from 'react';
import { App } from '@modelcontextprotocol/ext-apps';

function MyWidget() {
  const [toolOutput, setToolOutput] = useState(null);

  useEffect(() => {
    const app = new App({ name: 'MyWidget', version: '1.0.0' });
    app.ontoolresult = (result) => setToolOutput(result.structuredContent ?? null);
    app.connect();
  }, []);
  return Widget content;
}

// Mounting code - required
const rootElement = document.getElementById('my-widget-root');
if (rootElement) {
  createRoot(rootElement).render(
    
      
    
  );
}

2. Build discovers and bundles widget:

npm run build:widgets

3. Widget available as ui://my-widget

The build system:

  • Auto-discovers all files in widgets/src/widgets/*.{tsx,jsx}
  • Bundles the component and mounting code together
  • Creates content-hashed bundles and HTML templates

MCP Apps App API Reference

Tool Results & Host Context
const app = new App({ name: 'Echo', version: '1.0.0' });
app.ontoolresult = (result) => {
  console.log(result.structuredContent);
};
app.onhostcontextchanged = (context) => {
  console.log(context?.theme, context?.displayMode);
};
await app.connect();
Display Modes

Widgets can run in three display modes provided by the host:

  • inline — Rendered within the chat message flow (default)
  • pip — Picture-in-picture floating window
  • fullscreen — Full-screen overlay

The current mode is available via hostContext.displayMode. Widgets can request a mode change at runtime:

// Toggle between inline and fullscreen
const result = await app.requestDisplayMode({ mode: 'fullscreen' });
console.log(result.mode); // the mode the host actually switched to

The host decides whether to honor the request — always use the returned result.mode as the source of truth.

Container Dimensions

Hosts provide containerDimensions in the host context so widgets can size themselves responsively:

app.onhostcontextchanged = (context) => {
  const { maxHeight, maxWidth } = context?.containerDimensions ?? {};
  // Use maxHeight/maxWidth to constrain your layout
};

This replaces viewport-based sizing and ensures widgets respect the host's available space (especially important in inline mode).

Runtime APIs
// Call other tools from the widget
const result = await app.callServerTool({
  name: 'tool_name',
  arguments: { arg: 'value' },
});

// Open an external link via the host
await app.openLink({ url: 'https://example.com' });

// Send a message to the host chat
await app.sendMessage({
  role: 'user',
  content: [{ type: 'text', text: 'Hello from the widget!' }],
});

// Push widget state to the model context for future turns
await app.updateModelContext({
  content: [{ type: 'text', text: 'Current widget state summary' }],
  structuredContent: { key: 'value' },
});

// Toggle display mode
await app.requestDisplayMode({ mode: 'fullscreen' });

UI Capability Negotiation

The server inspects the client's capabilities during session initialization and adapts its responses:

  • UI-capable hosts (ChatGPT, VS Code, etc.) — Tools include _meta.ui.resourceUri and return structuredContent for the widget to render
  • Text-only hosts (terminal clients, basic MCP consumers) — Tools omit UI metadata and return plain text responses

This happens automatically via getUiCapability() from @modelcontextprotocol/ext-apps/server. No widget changes are needed — the server handles the fallback.

Inline Widget Assets

Some hosts (e.g. Claude.ai) require fully self-contained HTML — external ` and tags won't load inside their sandboxed iframes. Inline mode is also useful when sharing your work remotely via ssh -R 0 pom.run`.

npm ru

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [pomerium](https://github.com/pomerium)
- **Source:** [pomerium/chatgpt-app-typescript-template](https://github.com/pomerium/chatgpt-app-typescript-template)
- **License:** Apache-2.0
- **Homepage:** https://www.pomerium.com

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.