A comprehensive TypeScript library for building ChatGPT-integrated applications with React and Node.js. Provides type-safe utilities, React hooks, and helpers for working with OpenAI's MCP (Model Context Protocol) servers and ChatGPT widgets.
✨ Type-Safe - Full TypeScript support with comprehensive type definitions
🎯 Modular - Import only what you need with multiple entry points
⚛️ React Ready - Purpose-built hooks for ChatGPT widget development
🔧 Server-Friendly - Core utilities work in Node.js without React
📦 Tree-Shakeable - Optimized for modern bundlers
🧪 Well-Tested - Comprehensive test coverage
# For React applications
npm install chatgpt-app-kit react react-dom
# For server-only (MCP servers, Node.js backends)
npm install chatgpt-app-kitNote: React and React-DOM are optional peer dependencies. The library works in server environments without React.
import { useOpenAiGlobal, useWidgetState, useSendFollowUpMessage } from 'chatgpt-app-kit';
function MyWidget() {
const theme = useOpenAiGlobal('theme');
const [count, setCount] = useWidgetState({ value: 0 });
const sendMessage = useSendFollowUpMessage();
const increment = () => {
const newCount = count.value + 1;
setCount({ value: newCount });
sendMessage(`Clicked ${newCount} times!`);
};
return (
<div className={theme === 'dark' ? 'dark' : 'light'}>
<h1>Count: {count?.value ?? 0}</h1>
<button onClick={increment}>Increment</button>
</div>
);
}import { createWidgetMeta } from 'chatgpt-app-kit/server';
// Define a tool that returns a widget
const tool = {
name: 'show_chart',
handler: async (params) => {
return {
content: [
{
type: 'resource',
resource: {
uri: 'ui://widget/chart.html',
text: JSON.stringify(params),
mimeType: 'application/json'
}
}
],
_meta: createWidgetMeta('ui://widget/chart.html', {
invoking: 'Generating chart...',
invoked: 'Chart ready',
accessible: true,
description: 'Interactive data visualization'
})
};
}
};The library provides 4 entry points for different use cases:
| Entry Point | Size | Contents | React Required |
|---|---|---|---|
chatgpt-app-kit |
11KB | Everything (utilities + hooks) | Optional |
chatgpt-app-kit/core |
4KB | Utilities + Types only | ❌ No |
chatgpt-app-kit/react |
7KB | React Hooks only | ✅ Yes |
chatgpt-app-kit/server |
4KB | Server utilities (alias for /core) |
❌ No |
// Full package - React apps with all features
import { createMeta, useOpenAiGlobal } from 'chatgpt-app-kit';
// Core only - MCP servers, Node.js backends
import { createMeta, createWidgetMeta } from 'chatgpt-app-kit/core';
// Server alias - Explicit server-side usage
import { createMeta } from 'chatgpt-app-kit/server';
// React hooks only - Optimized component imports
import { useOpenAiGlobal, useWidgetState } from 'chatgpt-app-kit/react';
import { OpenAIMetadata } from 'chatgpt-app-kit/core';Subscribe to window.openai global state changes.
function MyComponent() {
const theme = useOpenAiGlobal<string>('theme');
const locale = useOpenAiGlobal<string>('locale');
const userAgent = useOpenAiGlobal('userAgent');
return <div className={theme}>Current locale: {locale}</div>;
}Available keys:
theme- Current theme ('light' | 'dark')locale- User locale (e.g., 'en-US')userAgent- Device and capability infodisplayMode- Current display mode ('inline' | 'pip' | 'fullscreen')safeArea- Safe area insets for layoutmaxHeight- Maximum available heighttoolInput- Current tool input datatoolOutput- Current tool output datatoolResponseMetadata- Tool response metadatawidgetState- Persistent widget state
Manage persistent widget state that syncs with ChatGPT.
function TodoWidget() {
const [todos, setTodos] = useWidgetState<string[]>([]);
const addTodo = (text: string) => {
setTodos([...todos, text]);
};
return (
<ul>
{todos?.map((todo, i) => <li key={i}>{todo}</li>)}
</ul>
);
}Access the current tool input data.
function DataDisplay() {
const input = useToolInput<{ query: string }>();
return <div>Query: {input?.query}</div>;
}Access the current tool output data.
function ResultsDisplay() {
const output = useToolOutput<{ results: any[] }>();
return <div>{output?.results.length} results</div>;
}Access the tool response metadata.
function StatusDisplay() {
const metadata = useToolResponseMetadata<{ status: string }>();
return <div>Status: {metadata?.status}</div>;
}Call MCP tools from your component.
function RefreshButton() {
const callTool = useCallTool();
const refresh = async () => {
const result = await callTool('refresh_data', { force: true });
console.log(result);
};
return <button onClick={refresh}>Refresh</button>;
}Send follow-up messages to the ChatGPT conversation.
function ShareButton({ data }: { data: any }) {
const sendMessage = useSendFollowUpMessage();
const share = () => {
sendMessage(`Here's the data: ${JSON.stringify(data)}`);
};
return <button onClick={share}>Share</button>;
}Open external links via the ChatGPT host.
function ExternalLink({ url }: { url: string }) {
const openExternal = useOpenExternal();
return (
<button onClick={() => openExternal({ href: url })}>
Open in browser
</button>
);
}Request display mode changes (inline, pip, fullscreen).
function ExpandButton() {
const requestDisplayMode = useRequestDisplayMode();
const goFullscreen = async () => {
const result = await requestDisplayMode({ mode: 'fullscreen' });
console.log('Granted mode:', result.mode);
};
return <button onClick={goFullscreen}>Expand</button>;
}Create OpenAI metadata objects with a user-friendly API.
const metadata = createMeta({
outputTemplate: 'ui://widget/dashboard.html',
toolInvocation: {
invoking: 'Loading dashboard...',
invoked: 'Dashboard ready'
},
widgetAccessible: true,
resultCanProduceWidget: true,
widgetCSP: {
connect_domains: ['https://api.example.com'],
resource_domains: ['https://cdn.example.com']
},
locale: 'en-US'
});MetaInput properties:
outputTemplate- Widget template URItoolInvocation- Loading/completion status messageswidgetAccessible- Widget accessibility flagresultCanProduceWidget- Enable widget renderingwidgetCSP- Content Security PolicywidgetDomain- Widget subdomainwidgetDescription- Description for the modelwidgetPrefersBorder- Border preferencelocale- Locale stringuserAgent- User agent stringuserLocation- User location datai18n- Legacy locale property
Shorthand for creating widget metadata.
const metadata = createWidgetMeta('ui://widget/chart.html', {
invoking: 'Generating chart...',
invoked: 'Chart ready',
accessible: true,
description: 'Interactive data visualization',
prefersBorder: false
});Clean and normalize ChatGPT response text.
const cleaned = cleanResponse(' Hello\n\n\nWorld ');
// Returns: "Hello\n\nWorld"Extract code blocks from markdown text.
const blocks = extractCodeBlocks('```javascript\nconsole.log("hi");\n```');
// Returns: [{ language: 'javascript', code: 'console.log("hi");' }]import type {
// Metadata
OpenAIMetadata,
WidgetCSP,
UserLocation,
// UI
Theme,
DisplayMode,
UserAgent,
SafeArea,
// API
CallTool,
CallToolResponse,
RequestDisplayMode,
// Global
OpenAIGlobals,
// Common
UnknownObject
} from 'chatgpt-app-kit';import {
useOpenAiGlobal,
useWidgetState,
useToolInput,
useSendFollowUpMessage,
useRequestDisplayMode
} from 'chatgpt-app-kit';
interface TodoItem {
id: string;
text: string;
done: boolean;
}
interface TodoState {
items: TodoItem[];
}
function TodoWidget() {
const theme = useOpenAiGlobal<string>('theme');
const input = useToolInput<{ initialTodos?: string[] }>();
const [state, setState] = useWidgetState<TodoState>({
items: input?.initialTodos?.map((text, i) => ({
id: String(i),
text,
done: false
})) ?? []
});
const sendMessage = useSendFollowUpMessage();
const requestDisplayMode = useRequestDisplayMode();
const addTodo = (text: string) => {
const newItem = {
id: String(Date.now()),
text,
done: false
};
setState({
items: [...(state?.items ?? []), newItem]
});
};
const toggleTodo = (id: string) => {
setState({
items: state?.items.map(item =>
item.id === id ? { ...item, done: !item.done } : item
) ?? []
});
};
const shareProgress = () => {
const done = state?.items.filter(i => i.done).length ?? 0;
const total = state?.items.length ?? 0;
sendMessage(`Progress: ${done}/${total} todos completed`);
};
return (
<div className={`todo-widget ${theme}`}>
<h1>Todo List</h1>
<ul>
{state?.items.map(item => (
<li key={item.id}>
<input
type="checkbox"
checked={item.done}
onChange={() => toggleTodo(item.id)}
/>
<span className={item.done ? 'done' : ''}>{item.text}</span>
</li>
))}
</ul>
<button onClick={shareProgress}>Share Progress</button>
<button onClick={() => requestDisplayMode({ mode: 'fullscreen' })}>
Expand
</button>
</div>
);
}import { createWidgetMeta, type OpenAIMetadata } from 'chatgpt-app-kit/server';
interface ChartData {
labels: string[];
values: number[];
}
export const chartTool = {
name: 'create_chart',
description: 'Create an interactive chart visualization',
inputSchema: {
type: 'object',
properties: {
data: {
type: 'object',
properties: {
labels: { type: 'array', items: { type: 'string' } },
values: { type: 'array', items: { type: 'number' } }
},
required: ['labels', 'values']
},
type: {
type: 'string',
enum: ['bar', 'line', 'pie']
}
},
required: ['data', 'type']
},
async handler(params: { data: ChartData; type: string }) {
const metadata: OpenAIMetadata = createWidgetMeta(
'ui://widget/chart.html',
{
invoking: 'Generating chart...',
invoked: 'Chart visualization ready',
accessible: true,
description: `${params.type} chart with ${params.data.labels.length} data points`
}
);
return {
content: [
{
type: 'resource',
resource: {
uri: 'ui://widget/chart.html',
text: JSON.stringify(params),
mimeType: 'application/json'
}
}
],
_meta: metadata
};
}
};// ✅ Good - Server-side
import { createMeta } from 'chatgpt-app-kit/server';
// ❌ Avoid - Pulls in React hooks unnecessarily
import { createMeta } from 'chatgpt-app-kit';// ✅ Good - Typed state
interface MyState {
count: number;
items: string[];
}
const [state, setState] = useWidgetState<MyState>({ count: 0, items: [] });
// ❌ Avoid - Untyped state
const [state, setState] = useWidgetState({ count: 0, items: [] });All hooks are SSR-safe and return null when window is undefined:
function MyComponent() {
const theme = useOpenAiGlobal('theme');
// Always check for null in SSR environments
if (!theme) {
return <div>Loading...</div>;
}
return <div className={theme}>Content</div>;
}// ✅ Good - Provide loading states
const metadata = createWidgetMeta('ui://widget/app.html', {
invoking: 'Fetching data...',
invoked: 'Data loaded successfully'
});
// ❌ Avoid - No user feedback
const metadata = createWidgetMeta('ui://widget/app.html');# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Build the library
npm run build
# Run linting
npm run lint
# Format code
npm run format
# Type check
npm run build- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT © 2025