Build interactive MCP applications with rich UIs that work on both MCP Apps and ChatGPT from a single codebase.
# Create a new project
npx @mcp-apps-kit/create-app my-app
cd my-app
pnpm install
# Or add to an existing project
pnpm add @mcp-apps-kit/core @mcp-apps-kit/ui-react zod
Create an MCP server with tools that return structured data:
import { createApp, defineTool, defineUI } from "@mcp-apps-kit/core";
import { z } from "zod";
// Define a UI resource for the tool
const greetingWidget = defineUI({
name: "Greeting Widget",
html: "./ui/dist/greeting.html",
prefersBorder: true,
});
// Create the app
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
greet: defineTool({
title: "Greet User",
description: "Generate a personalized greeting",
input: z.object({
name: z.string().describe("Name to greet"),
}),
output: z.object({
message: z.string(),
timestamp: z.string(),
}),
ui: greetingWidget,
handler: async ({ name }) => ({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
}),
}),
},
});
// Start the server
await app.start({ port: 3000 });
Create a React component that displays the tool result. Wrap your app with AppsProvider:
// ui/src/App.tsx
import { AppsProvider } from "@mcp-apps-kit/ui-react";
import { GreetingWidget } from "./GreetingWidget";
export function App() {
return (
<AppsProvider>
<GreetingWidget />
</AppsProvider>
);
}
// ui/src/GreetingWidget.tsx
import { useToolResult, useHostContext } from "@mcp-apps-kit/ui-react";
interface GreetingResult {
message: string;
timestamp: string;
}
export function GreetingWidget() {
const result = useToolResult<GreetingResult>();
const { theme } = useHostContext();
if (!result) {
return <div>Loading...</div>;
}
return (
<div style={{
padding: "1rem",
background: theme === "dark" ? "#1a1a1a" : "#fff"
}}>
<h2>{result.message}</h2>
<p>Generated at: {new Date(result.timestamp).toLocaleString()}</p>
</div>
);
}
Export types from your server to get fully typed tool results in UI code:
// server/index.ts
import { createApp, defineTool, type ClientToolsFromCore } from "@mcp-apps-kit/core";
import { z } from "zod";
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: {
greet: defineTool({
input: z.object({ name: z.string() }),
output: z.object({ message: z.string(), timestamp: z.string() }),
handler: async (input) => ({
message: `Hello, ${input.name}!`,
timestamp: new Date().toISOString(),
}),
}),
},
});
// Export types for UI code
export type AppTools = typeof app.tools;
export type AppClientTools = ClientToolsFromCore<AppTools>;
Then use the types in your UI:
// ui/Widget.tsx
import { useToolResult, useAppsClient } from "@mcp-apps-kit/ui-react";
import type { AppClientTools } from "../server";
function Widget() {
const client = useAppsClient<AppClientTools>();
const result = useToolResult<AppClientTools>();
// TypeScript enforces correct tool name and input shape
const handleGreet = () => client.callTool("greet", { name: "Alice" });
// result?.greet?.message is typed as string | undefined
if (result?.greet) {
return <p>{result.greet.message}</p>;
}
return <button onClick={handleGreet}>Greet</button>;
}
| Hook | Purpose |
|---|---|
useAppsClient() |
Client instance for tool calls |
useToolResult<T>() |
Current tool result data |
useToolInput<T>() |
Tool input parameters |
useHostContext() |
Host info (theme, viewport, locale) |
useWidgetState() |
Persist state across reloads |
useDisplayMode() |
Toggle fullscreen mode |
import { useHostCapabilities, useHostVersion } from "@mcp-apps-kit/ui-react";
function Widget() {
const capabilities = useHostCapabilities();
const version = useHostVersion();
const themes = capabilities?.theming?.themes; // ["light", "dark"]
const modes = capabilities?.displayModes?.modes; // ["inline", "fullscreen"]
const hasFileUpload = !!capabilities?.fileUpload; // ChatGPT only
return <div>Host: {version?.name}</div>;
}
| Hook | Purpose |
|---|---|
useFileUpload() |
Upload files to host |
useFileDownload() |
Get file download URLs |
useModal() |
Show modal dialogs |
| Hook | Purpose |
|---|---|
useHostStyleVariables() |
Apply host CSS variables |
useDocumentTheme() |
Sync document theme |
useSafeAreaInsets() |
Safe area insets (ChatGPT) |
Add cross-cutting concerns with Koa-style middleware:
app.use(async (context, next) => {
console.log(`Tool called: ${context.toolName}`);
const start = Date.now();
await next();
console.log(`Completed in ${Date.now() - start}ms`);
});
Extend functionality with lifecycle hooks:
import { loggingPlugin } from "@mcp-apps-kit/core";
const app = createApp({
name: "my-app",
version: "1.0.0",
plugins: [loggingPlugin],
tools: { /* ... */ },
});
Create custom plugins:
const myPlugin = {
name: "my-plugin",
onInit: async () => console.log("App initializing..."),
onStart: async () => console.log("App started"),
beforeToolCall: async (context) => {
console.log(`Tool called: ${context.toolName}`);
},
afterToolCall: async (context) => {
console.log(`Tool completed: ${context.toolName}`);
},
};
Subscribe to app lifecycle events:
app.on("tool:called", ({ toolName }) => {
analytics.track("tool_called", { tool: toolName });
});
Enable debug logging to receive logs from client UIs:
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: { /* ... */ },
config: {
debug: {
logTool: true,
level: "debug",
},
},
});
Use the server-side logger:
import { debugLogger } from "@mcp-apps-kit/core";
debugLogger.info("User action", { userId: "456" });
Add bearer token validation:
const app = createApp({
name: "my-app",
version: "1.0.0",
tools: { /* ... */ },
config: {
oauth: {
protectedResource: "http://localhost:3000",
authorizationServer: "https://auth.example.com",
scopes: ["mcp:read", "mcp:write"],
},
},
});
Access auth context in handlers:
tools: {
get_user_data: defineTool({
input: z.object({}),
handler: async (_input, context) => {
const subject = context.subject; // Authenticated user ID
return { userId: subject };
},
}),
}
MCP Apps Kit automatically handles protocol differences:
| Feature | MCP Apps | ChatGPT |
|---|---|---|
| Tool execution | MCP protocol | OpenAI Apps SDK |
| UI rendering | iframe | Widget runtime |
| Theme support | Auto-detected | Auto-detected |
| File upload | Supported | Supported |
| Modals | Supported | Supported |
| Partial input | Supported | - |
| Size notifications | Supported | - |