Develop an MCP App
Build interactive apps using the MCP Apps extension — the open standard for rendering interactive UI components inside MCP hosts. The Pomerium template currently targets ChatGPT Apps (OpenAI's implementation of the standard), with broader MCP Apps support coming soon.
Your MCP server handles tool execution and returns structured data that the host renders as interactive UI components in a sandboxed iframe.
What you get:
- An MCP server that registers tools and returns widget-ready structured data
- React-based widgets rendered inside your MCP host as interactive iframes
- Secure authentication via Pomerium — use
pom.runfor local development or deploy permanently
Architecture
The template builds and serves two separate properties:
| Property | Dev | Production | Access policy |
|---|---|---|---|
MCP server (/mcp) | Public URL via pom.run tunnel | Public URL via Pomerium | Auth-gated — tool calls always require a Bearer token |
| Widget assets | localhost:4444 (rendered in your browser) | Public URL via Pomerium | Public — the MCP host renders widgets in a sandboxed iframe and cannot forward credentials |
The MCP server always needs a publicly reachable URL so the MCP host can reach it. Widget assets only need a public URL in production; during development your local browser loads them directly from localhost.
Tool calls always carry a Bearer token and should be gated by a strict Pomerium policy. Widgets must be publicly accessible because the MCP host renders them in a sandboxed iframe and cannot forward authentication tokens.
How it works
Your MCP server registers tools that return three things in each response:
- Text content — human-readable text for the MCP host's conversation
- Structured JSON data — passed to the widget via
window.openai.toolOutput - Widget metadata — a
_meta.outputTemplatepointing to a widget resource (e.g.,ui://echo)
ChatGPT renders your widget in an iframe. The widget receives tool output data and can call back into the MCP server via window.openai.callTool().
Prerequisites
- Node.js 22+ — verify with
node -v - npm 10+ — ships with Node 22, verify with
npm -v - An MCP client (e.g., a ChatGPT Plus subscription)
Step-by-step
1. Scaffold from the template
git clone https://github.com/pomerium/chatgpt-app-typescript-template my-chatgpt-app
cd my-chatgpt-app
npm install
npm run dev
This starts both the MCP server (http://localhost:8080) and widget dev server (http://localhost:4444).
2. Expose your MCP server with pom.run
The MCP host needs a public URL to reach your server — localhost won't work. In a new terminal (keep npm run dev running):
ssh -R 0 pom.run
Sign in and you'll get a public route URL like https://mcp.your-route-1234.pomerium.app/ that tunnels to your local MCP server. The widget dev server (localhost:4444) stays local — your browser loads it directly. For full tunneling details, see Tunnel to ChatGPT During Development.
3. Connect to ChatGPT
- In ChatGPT, go to Settings → Apps → Advanced settings and enable Developer mode
- Click Create app
- Set MCP Server URL to
https://mcp.your-route-1234.pomerium.app/mcp - Set Authentication to OAuth
- Test with:
@echo today is a great day

4. Build your own tools and widgets
The template's echo tool shows the full pattern. The key pieces when adding your own tool:
Tool response — return structuredContent and a _meta.outputTemplate pointing to your widget:
return {
content: [{type: 'text', text: 'Result'}],
structuredContent: {result: args.input},
_meta: {
outputTemplate: {
type: 'resource',
resource: {uri: 'ui://my-widget'},
},
},
};
Widget resource registration — the text/html+skybridge MIME type is required for ChatGPT to render the widget:
return {
contents: [{uri, mimeType: 'text/html+skybridge', text: html}],
};
Widget entry point — React component in widgets/src/widgets/my-widget.tsx, with mounting code at the bottom. The build auto-discovers all files matching widgets/src/widgets/*.{tsx,jsx}.
See the template README for the complete guide: project structure, window.openai API, Storybook, testing, environment variables, and troubleshooting.
For production deployment
You need two Pomerium routes — one for the MCP server (auth-gated) and one for the widgets (public):
runtime_flags:
mcp: true
routes:
# MCP server — fine-grained authorization required for tool calls
- from: https://my-chatgpt-app.your-domain.com
to: http://my-chatgpt-app:8080/mcp
name: My ChatGPT App (MCP server)
mcp:
server: {}
policy:
allow:
and:
- domain:
is: company.com
# Widget assets — must be public so the MCP host can render iframes without credentials
- from: https://my-chatgpt-app-ui.your-domain.com
to: http://my-chatgpt-app-widgets:4444
name: My ChatGPT App (widgets)
allow_public_unauthenticated_access: true
The MCP server URL you register in ChatGPT points to the first (auth-gated) route. Widget resources served by your MCP server reference the second (public) route. Never put the widget route behind an auth policy — MCP hosts cannot forward credentials when loading iframe content.
See Protect an MCP Server for the full setup guide.
Sample repos and next steps
- pomerium/chatgpt-app-typescript-template — Starter template for ChatGPT Apps (MCP Apps support coming soon) — full README with project structure, API reference, testing, Docker, and troubleshooting
- MCP Apps extension spec — Official standard for interactive UI in MCP hosts
- Tunnel to ChatGPT During Development — pom.run tunneling setup details
- Protect an MCP Server — Deploy permanently behind Pomerium
- MCP Full Reference — Token types, session lifecycle, configuration details