Curated repos, tools, and frameworks shaping the developer ecosystem.
Live data from GitHub.
by assistant-ui
Starter template for MCP App Studio
Starter template for building interactive apps for AI assistants with MCP App Studio.
Note: This template is automatically downloaded when you run
npx mcp-app-studio. You don't need to clone this repo directly.
Build once, deploy anywhere:
ui/* bridge)# npm (default)
npm install
npm run dev
Open http://localhost:3002 — you're in the workbench.
This project also works with pnpm/yarn/bun (use the equivalent install + run commands).
If you switch package managers (e.g. pnpm → npm), delete node_modules/ first to avoid confusing the installer.
The MCP server (when server/ exists) runs at http://localhost:3001/mcp by default. If 3001 is already in use, it will select the next available port and write it to server/.mcp-port.
The workbench simulates an MCP Apps host in an iframe. It also installs a
window.openai shim so you can exercise ChatGPT-only extensions during
development (optional, non-standard).
Use .agent/skills/mcp-app-development/SKILL.md as the default coding-agent workflow for this repo. It defines the 80/20 capability-slice loop:
red -> green) for parity checks| Command |
|---|
| Description |
|---|
npm run dev | Start workbench (Next.js + MCP server) |
npm run build | Production build |
npm run export | Generate widget bundle for deployment |
app/ Next.js pages
components/
├── examples/ Example widgets (POI Map)
├── workbench/ Workbench UI components
└── ui/ Shared UI components
lib/
├── sdk/ SDK exports for production
├── workbench/ React hooks + dev environment
└── export/ Production bundler
server/ MCP server (if included)
// components/my-widget/index.tsx
import {
useToolInput,
useCallTool,
useTheme,
useCapabilities,
useUpdateModelContext,
useWidgetState,
} from "@/lib/sdk";
export function MyWidget() {
const input = useToolInput<{ query: string }>();
const callTool = useCallTool();
const theme = useTheme();
const capabilities = useCapabilities();
const updateModelContext = useUpdateModelContext();
const [widgetState, setWidgetState] = useWidgetState();
const handleSearch = async () => {
const result = await callTool("search", { query: input.query });
console.log(result.structuredContent);
};
return (
<div
data-theme={theme}
className={theme === "dark" ? "dark bg-background text-foreground" : "bg-background text-foreground"}
style={{ colorScheme: theme }}
>
<p>Query: {input.query}</p>
<button onClick={handleSearch}>Search</button>
{/* Platform-specific features */}
{capabilities.modelContext && (
<button
onClick={() =>
updateModelContext({ structuredContent: { query: input.query } })
}
>
Update model context (host-dependent)
</button>
)}
{capabilities.widgetState && (
<button
onClick={() =>
setWidgetState({
...(widgetState ?? {}),
savedAt: Date.now(),
})
}
>
Save widget state (ChatGPT extensions)
</button>
)}
</div>
);
}
Add your component to lib/workbench/component-registry.tsx.
Configure mock tool responses in lib/workbench/mock-config/.
Full documentation: lib/workbench/README.md
These hooks work identically across MCP hosts (including ChatGPT):
| Hook | Description |
|---|---|
useToolInput<T>() | Get input arguments from tool call |
useTheme() | Get current theme ("light" or "dark") |
useCallTool() | Call backend tools |
useDisplayMode() | Get/set display mode |
useSendMessage() | Send messages to conversation |
| Hook | Description |
|---|---|
useCapabilities() | Get full capability object |
useFeature(name) | Check if specific feature is available |
These hooks only work on specific platforms. Check availability first:
| Hook | Platform | Description |
|---|---|---|
useWidgetState() | ChatGPT extensions | Optional OpenAI/ChatGPT host-managed state |
useUpdateModelContext() | Host-dependent | Update model-visible context dynamically |
useToolInputPartial() | Host-dependent | Streaming input during generation |
useLog() | Host-dependent | Structured logging to host |
openModal() helper | ChatGPT extensions (fallback-safe) | Use host modal when available, fallback locally |
useWidgetState() is not a standard MCP Apps persistence primitive.
For portable MCP Apps, use app-managed persistence such as localStorage or
server-backed tools.
Tool result metadata (_meta) is available via
readToolResponseMetadata() when the host exposes
window.openai.toolResponseMetadata.
MCP App Studio is MCP-first: prefer the MCP Apps bridge (ui/*) and feature-detect
optional ChatGPT extensions (window.openai) when needed.
| Feature | MCP Apps standard | ChatGPT extensions (optional) |
|---|---|---|
| Tool input | Yes | (alias: window.openai.toolInput) |
| Tool result | Yes | (alias: window.openai.toolOutput) |
Tool result metadata (_meta) | Yes | Yes (alias: window.openai.toolResponseMetadata) |
| Call tool | Yes | (alias: window.openai.callTool) |
| Send message | Host-dependent | (alias: window.openai.sendFollowUpMessage) |
| Update model context | Host-dependent | (extension: window.openai.setWidgetState) |
| Host-managed modal | No | Yes (window.openai.requestModal) |
| Widget state persistence | No | Yes (OpenAI/ChatGPT host-managed state) |
| File upload/download | No | Yes |
| Open in app link | No | Yes (window.openai.setOpenInAppUrl) |
| Instant checkout | No | Yes (window.openai.requestCheckout) (private beta) |
Use useCapabilities() or useFeature() to conditionally enable features.
window.openai.requestModal() only when you specifically need a ChatGPT-hosted modal template.if (typeof window !== "undefined" && window.openai?.requestModal) {
await window.openai.requestModal({ title: "Details", params: { id } });
} else {
// Fallback: local modal state or route navigation
}
window.openai.requestCheckout(...) is currently a ChatGPT private beta extension.window.openai.setOpenInAppUrl({ href }).import { requestCheckout, setOpenInAppUrl } from "@/lib/sdk";
setOpenInAppUrl("https://your-app.com/orders/123");
const outcome = await requestCheckout(
{ id: "checkout_123", payment_mode: "test" },
() => window.open("https://your-app.com/checkout/123", "_blank"),
);
if (outcome.mode === "fallback") {
// Non-ChatGPT host or checkout beta unavailable.
}
npm run export
Defaults for --entry and --export-name are read from mcp-app-studio.config.json (written by the CLI when you scaffold a project). You can override them via flags.
Generates:
export/
├── widget/
│ └── index.html Self-contained widget
├── manifest.json App manifest
└── README.md Deployment instructions
The exported widget uses the mcp-app-studio SDK which automatically detects the host platform and uses the appropriate bridge.
ui.* metadata is canonical in exported server code.["model","app"]) when no visibility keys are set.text/html;profile=mcp-app.Widget resource CSP must use MCP-standard keys:
ui: {
csp: {
connectDomains: ["https://api.example.com"],
resourceDomains: ["https://cdn.example.com"],
frameDomains: ["https://www.youtube.com"],
baseUriDomains: ["https://cdn.example.com"],
},
}
Deploy export/widget/ to any static host:
# Vercel
cd export/widget && vercel deploy
# Netlify
netlify deploy --dir=export/widget
# Or any static host (S3, Cloudflare Pages, etc.)
If you have a server/ directory:
cd server
npm run build
# Deploy to Vercel, Railway, Fly.io, etc.
For ChatGPT:
manifest.json with your deployed widget URLFor Claude Desktop:
The workbench includes an AI-powered SDK guide. To enable:
cp .env.example .env.local
# then set:
# OPENAI_API_KEY="your-key"
For production, restrict CORS to your widget domain:
cp server/.env.example server/.env
# then set:
# CORS_ORIGIN=https://your-widget-domain.com
Exported widgets inherit host theme and token variables. Follow the framework-agnostic contract in lib/workbench/THEMING_CONTRACT.md.
At minimum, support data-theme / .dark and semantic tokens:
.my-element {
background: var(--background);
color: var(--foreground);
border-color: var(--border);
}
A set of beautifully-designed, accessible components and a code distribution platform. Works with your favorite frameworks. Open Source. Open Code.