mcp-apps-builder
Build MCP Apps with interactive React UIs using the Leanmcp UI SDK. Use this skill when users ask for "MCP with UI", "MCP App", "interactive MCP", "leanmcp UI", "MCP dashboard", or want to create rich visual interfaces for their MCP tools.
Install / Use
npx skills add Leanmcp/gateway-skills --skill mcp-apps-builderInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
AI & Machine LearningSupported Platforms
Our assessment of mcp-apps-builder
mcp-apps-builder scores 94/100 on our quality scale, 182nd of 946 AI & Machine Learning skills we index (top 20%).
Its SKILL.md is 14 KB long, well organised into 36 sections with 22 code examples: a thorough specification that gives an agent plenty to work with.
With 2,111 GitHub stars, it is one of the more widely adopted skills in the catalogue.
Maintenance, license and trust
- The repository was last updated 16 days ago, so mcp-apps-builder is actively maintained.
- It is released under the MIT license, a permissive license that allows use, modification and commercial use with attribution.
- Its trust signals score 100/100, with no cautions. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.
mcp-apps-builder compared with similar skills
All 4 of these similar skills score higher than mcp-apps-builder; compare them before choosing.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| mcp-apps-builder (this skill)by Leanmcp | 94 | 2.1k | 16d ago | SKILL.md |
| claude-memby thedotmack | 100 | 102.6k | today | CLAUDE.md |
| Agent-Reachby Panniantong | 100 | 101.4k | 1d ago | CLAUDE.md |
| Understand-Anythingby Egonex-AI | 100 | 88.0k | 1d ago | CLAUDE.md |
| headroomby headroomlabs-ai | 100 | 77.7k | today | CLAUDE.md |
Frequently asked questions
- How do I install mcp-apps-builder?
- Run
npx skills add Leanmcp/gateway-skills --skill mcp-apps-builder. The install tabs above show the steps for each supported agent. - Which AI agents does mcp-apps-builder work with?
- It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
- Is mcp-apps-builder safe to use?
- It is MIT-licensed and scores 100/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
- Is mcp-apps-builder still maintained?
- The repository was last updated 16 days ago, so mcp-apps-builder is actively maintained.
Skill content
View source on GitHubname: mcp-apps-builder description: Build MCP Apps with interactive React UIs using the Leanmcp UI SDK. Use this skill when users ask for "MCP with UI", "MCP App", "interactive MCP", "leanmcp UI", "MCP dashboard", or want to create rich visual interfaces for their MCP tools. This skill covers @UIApp decorator, React components with useTool/useResource hooks, ToolDataGrid, ToolForm, ToolButton, and ChatGPT Apps support.
MCP Apps Builder Skill
This skill guides you in building MCP Apps - MCP servers with interactive React UIs using the Leanmcp UI SDK.
When to Use This Skill
- User asks for "MCP with UI"
- User wants "interactive MCP server"
- User mentions "MCP App" or "leanmcp UI"
- User needs "dashboard for MCP tools"
- User wants "visual interface for AI tools"
- User mentions "@leanmcp/ui" or "@UIApp"
- User asks for "ChatGPT App" or "GPT App"
What is an MCP App?
MCP Apps extend MCP servers to deliver interactive UIs. Apps run in sandboxed iframes and communicate via JSON-RPC over postMessage. When a tool is called, the UI can be displayed alongside the tool response.
Core Architecture
- Service file (
mcp/*/index.ts) - Tool methods decorated with@ToolAND@UIApp - Component file (
mcp/*/*.tsx) - React component using@leanmcp/uicomponents - CLI handles the rest -
leanmcp devbuilds and wraps components with AppProvider
Required Dependencies
{
"dependencies": {
"@leanmcp/core": "^0.4.7",
"@leanmcp/ui": "^0.3.7",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"dotenv": "^16.5.0"
},
"devDependencies": {
"@leanmcp/cli": "latest",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"@types/node": "^20.0.0",
"typescript": "^5.6.3"
}
}
TypeScript Configuration
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"jsx": "react-jsx",
"jsxImportSource": "react",
"outDir": "./dist",
"strict": true
},
"include": ["**/*.ts", "**/*.tsx", "mcp/**/*.ts", "mcp/**/*.tsx"]
}
Project Structure
my-mcp-app/
├── main.ts # Entry point
├── mcp/
│ └── products/
│ ├── index.ts # Service with @Tool + @UIApp
│ └── ProductsDashboard.tsx # React component
├── package.json
└── tsconfig.json
Two Import Paths (Critical)
| Import | Use In | Contents |
|--------|--------|----------|
| @leanmcp/ui/server | Service files (index.ts) | UIApp, GPTApp decorators only |
| @leanmcp/ui | Component files (.tsx) | React components, hooks, styles |
@UIApp Decorator
CRITICAL: @UIApp is a METHOD decorator, not a class decorator!
Correct Usage
// mcp/products/index.ts
import { Tool } from '@leanmcp/core';
import { UIApp } from '@leanmcp/ui/server'; // Server import!
export class ProductsService {
@Tool({ description: 'List products' })
@UIApp({ component: './ProductsDashboard' }) // On method with @Tool
async listProducts(args: { page?: number }) {
return { products: [...], total: 100 };
}
}
WRONG - Never do this
// WRONG: @UIApp on class
@UIApp({ component: './Dashboard' }) // WRONG!
export class MyService { ... }
@UIApp Options
@UIApp({
component: './ComponentName', // Required: relative path (string, not import)
uri: 'ui://custom/path', // Optional: custom URI
title: 'Page Title', // Optional
styles: 'body { ... }' // Optional: additional CSS
})
React Component Development
Basic Component
// mcp/products/ProductsDashboard.tsx
import { RequireConnection, ToolDataGrid } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';
export function ProductsDashboard() {
return (
<RequireConnection loading={<div>Loading...</div>}>
<div style={{ padding: '20px' }}>
<h1>Products</h1>
<ToolDataGrid
dataTool="listProducts"
columns={[
{ key: 'name', header: 'Name', sortable: true },
{ key: 'price', header: 'Price' }
]}
transformData={(result) => ({
rows: result.products,
total: result.total
})}
pagination
/>
</div>
</RequireConnection>
);
}
Critical Rules
- NEVER wrap with AppProvider - CLI does this automatically
- Always use RequireConnection - Shows loading until host connects
- Import styles -
import '@leanmcp/ui/styles.css' - Export matching name - File
Foo.tsxexportsfunction Foo() - Use string paths -
'./Component'not direct import
Core Components
ToolButton
Button that calls a tool with loading state and confirmation:
import { ToolButton } from '@leanmcp/ui';
<ToolButton
tool="delete-item"
args={{ id: item.id }}
confirm={{
title: 'Delete Item?',
description: 'This action cannot be undone.'
}}
variant="destructive"
resultDisplay="toast"
onToolSuccess={() => refetch()}
>
Delete
</ToolButton>
Props:
tool- Tool name to callargs- Arguments to passconfirm- Confirmation dialog configvariant-default,destructive,outline,ghostresultDisplay-toast,inline,noneonToolSuccess/onToolError- Callbacks
ToolForm
Form that submits to a tool:
import { ToolForm } from '@leanmcp/ui';
<ToolForm
toolName="create-product"
fields={[
{ name: 'name', label: 'Product Name', type: 'text', required: true },
{ name: 'price', label: 'Price', type: 'number', min: 0 },
{ name: 'category', label: 'Category', type: 'select', options: [
{ value: 'electronics', label: 'Electronics' },
{ value: 'clothing', label: 'Clothing' }
]},
{ name: 'inStock', label: 'In Stock', type: 'switch' }
]}
submitText="Create Product"
showSuccessToast
resetOnSuccess
onSuccess={(result) => console.log('Created:', result)}
/>
Field types: text, number, email, textarea, select, switch, slider, date
ToolDataGrid
Server-paginated table with sorting and row actions:
import { ToolDataGrid } from '@leanmcp/ui';
<ToolDataGrid
dataTool="listProducts" // NOT 'toolName'!
columns={[
{ key: 'name', header: 'Name', sortable: true }, // NOT 'field'/'headerName'!
{ key: 'price', header: 'Price', render: (v) => `$${v.toFixed(2)}` }
]}
transformData={(r) => ({ rows: r.products, total: r.total })}
pagination
pageSize={20}
refreshInterval={30000} // NOT 'autoRefresh'!
rowActions={[
{
label: 'Edit',
tool: 'edit-product',
getArgs: (row) => ({ id: row.id })
},
{
label: 'Delete',
tool: 'delete-product',
getArgs: (row) => ({ id: row.id }),
variant: 'destructive',
confirm: { title: 'Delete?', description: 'Cannot undo' }
}
]}
/>
Common Mistakes to Avoid:
- Use
dataToolnottoolName - Use
key/headernotfield/headerName - Use
refreshIntervalnotautoRefresh - Always provide
transformData
ToolSelect
Select dropdown with tool-based options:
import { ToolSelect } from '@leanmcp/ui';
<ToolSelect
optionsTool="list-categories"
transformOptions={(r) => r.categories.map(c => ({ value: c.id, label: c.name }))}
onSelectTool="set-category"
argName="categoryId"
placeholder="Select category"
/>
ResourceView
Display MCP resources with auto-refresh:
import { ResourceView } from '@leanmcp/ui';
<ResourceView
uri="config://settings"
refreshInterval={5000}
render={(data) => (
<pre>{JSON.stringify(data, null, 2)}</pre>
)}
/>
Core Hooks
useTool
Call MCP tools programmatically:
import { useTool } from '@leanmcp/ui';
function MyComponent() {
const { call, result, loading, error } = useTool('get-data', {
retry: 3,
transform: (r) => r.data
});
useEffect(() => {
call({ filter: 'active' });
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{result?.value}</div>;
}
useResource
Read MCP resources:
import { useResource } from '@leanmcp/ui';
function ConfigView() {
const { data, loading, refresh } = useResource('config://settings', {
refreshInterval: 5000
});
return (
<div>
{loading && <p>Loading...</p>}
{data && <pre>{JSON.stringify(data, null, 2)}</pre>}
<button onClick={refresh}>Refresh</button>
</div>
);
}
useHostContext
Access host environment (theme, viewport):
import { useHostContext } from '@leanmcp/ui';
function ThemedComponent() {
const { theme, viewport } = useHostContext();
return (
<div className={theme === 'dark' ? 'dark-mode' : 'light-mode'}>
Content
</div>
);
}
GPT Apps SDK Hooks
For ChatGPT Apps integration:
useToolOutput
Access structuredContent from tool response:
import { useToolOutput } from '@leanmcp/ui';
function ChannelsView() {
const toolOutput = useToolOutput<{ channels: Channel[] }>();
if (!toolOutput?.channels) return <div>No channels</div>;
return (
<ul>
{toolOutput.channels.map(ch => <li key={ch.id}>{ch.name}</li>)}
</ul>
);
}
useWidgetState
Persistent state across sessions:
import { useWidgetState } from '@leanmcp/ui';
function FilteredView() {
const [state, setState] = useWidgetState<{ filter: string }>({ filter: 'all' });
return (
<select
value={state.filter}
onChange={(e) => setState({ filter: e.target.value })}
>
<option value="all">All</option>
<option value="active">Active</option>
</select>
);
}
@GPTApp Decorator
For ChatGPT Apps specifically:
import { Tool } from '@leanmcp/core';
import { GPTApp } from '@leanmcp/ui/server';
export class SlackService {
@Tool({ description: 'Compose Slack message' })
@GPTApp({
component: './SlackComposer',
name: 'slack-composer'
})
async composeMessage() {
return { channels: [...] };
}
}
Complete Example
Service File
// mcp/dashboard/index.ts
import { Tool, Resource, SchemaConstraint } from '@leanmcp/core';
import { UIApp } from '@leanmcp/ui/server';
class CreateItemInput {
@SchemaConstraint({ description: 'Item name', minLength: 1 })
name!: string;
@SchemaConstraint({ description: 'Item value', minimum: 0 })
value!: number;
}
export class DashboardService {
private items: any[] = [];
@Tool({ description: 'View dashboard with items' })
@UIApp({ component: './Dashboard' })
async viewDashboard() {
return { items: this.items, total: this.items.length };
}
@Tool({ description: 'Create a new item', inputClass: CreateItemInput })
async createItem(input: CreateItemInput) {
const item = { id: crypto.randomUUID(), ...input, createdAt: new Date().toISOString() };
this.items.push(item);
return { success: true, item };
}
@Tool({ description: 'Delete an item' })
async deleteItem(args: { id: string }) {
this.items = this.items.filter(i => i.id !== args.id);
return { success: true };
}
@Resource({ description: 'Dashboard statistics' })
async stats() {
return {
contents: [{
uri: 'dashboard://stats',
mimeType: 'application/json',
text: JSON.stringify({
total: this.items.length,
totalValue: this.items.reduce((sum, i) => sum + i.value, 0)
})
}]
};
}
}
Component File
// mcp/dashboard/Dashboard.tsx
import { RequireConnection, ToolDataGrid, ToolForm, ToolButton, useResource } from '@leanmcp/ui';
import '@leanmcp/ui/styles.css';
export function Dashboard() {
const { data: stats, refresh } = useResource('dashboard://stats');
return (
Truncated for display — read the full file on GitHub.
Related Skills
claude-mem
102.6kPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More
Agent-Reach
101.4kGive your AI agent eyes to see the entire internet. Read & search Twitter, Reddit, YouTube, GitHub, Bilibili, XiaoHongShu — one CLI, zero API fees.
Understand-Anything
88.0kGraphs that teach > graphs that impress. Turn any code into an interactive knowledge graph you can explore, search, and ask questions about. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, and more.
headroom
77.7kCompress tool outputs, logs, files, and RAG chunks before they reach the LLM. 20% fewer tokens for coding agents, 60-95% fewer tokens for JSON, same answers. Library, proxy, MCP server.
Languages
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
