本文へ移動
cccskills
無料GitHub で公開

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. This skill covers @UIApp decorator, React components with useTool/useResource hooks, ToolDataGrid, ToolForm, ToolButton, and ChatGPT Apps support.

インストール方法を見る

含まれるファイル(7)

  • SKILL.md14.4 KB
  • assets/Dashboard.tsx4.3 KB
  • assets/main.ts346 B
  • assets/mcp-dashboard-index.ts2.1 KB
  • assets/package.json816 B
  • assets/tsconfig.json535 B
  • references/ui-components.md5.5 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

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

  1. Service file (mcp/*/index.ts) - Tool methods decorated with @Tool AND @UIApp
  2. Component file (mcp/*/*.tsx) - React component using @leanmcp/ui components
  3. CLI handles the rest - leanmcp dev builds 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)

ImportUse InContents
@leanmcp/ui/serverService files (index.ts)UIApp, GPTApp decorators only
@leanmcp/uiComponent 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

  1. NEVER wrap with AppProvider - CLI does this automatically
  2. Always use RequireConnection - Shows loading until host connects
  3. Import styles - import '@leanmcp/ui/styles.css'
  4. Export matching name - File Foo.tsx exports function Foo()
  5. 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 call
  • args - Arguments to pass
  • confirm - Confirmation dialog config
  • variant - default, destructive, outline, ghost
  • resultDisplay - toast, inline, none
  • onToolSuccess / 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 dataTool not toolName
  • Use key/header not field/headerName
  • Use refreshInterval not autoRefresh
  • 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 (
    <RequireConnection loading={<div>Connecting...</div>}>
      <div style={{ padding: '20px', fontFamily: 'system-ui' }}>
        <h1>Dashboard</h1>
        
        {/* Stats */}
        {stats && (
          <div style={{ marginBottom: '20px', padding: '10px', background: '#f5f5f5', borderRadius: '8px' }}>
            <p>Total Items: {stats.total}</p>
            <p>Total Value: ${stats.totalValue}</p>
          </div>
        )}

        {/* Create Form */}
        <div style={{ marginBottom: '20px' }}>
          <h2>Create Item</h2>
          <ToolForm
            toolName="createItem"
            fields={[
              { name: 'name', label: 'Name', type: 'text', required: true },
              { name: 'value', label: 'Value', type: 'number', min: 0, required: true }
            ]}
            submitText="Create"
            showSuccessToast
            resetOnSuccess
            onSuccess={() => refresh()}
          />
        </div>

        {/* Items Table */}
        <h2>Items</h2>
        <ToolDataGrid
          dataTool="viewDashboard"
          columns={[
            { key: 'name', header: 'Name', sortable: true },
            { key: 'value', header: 'Value', render: (v) => `$${v}` },
            { key: 'createdAt', header: 'Created', render: (v) => new Date(v).toLocaleDateString() }
          ]}
          transformData={(r) => ({ rows: r.items, total: r.total })}
          rowActions={[
            {
              label: 'Delete',
              tool: 'deleteItem',
              getArgs: (row) => ({ id: row.id }),
              variant: 'destructive',
              confirm: { title: 'Delete item?', description: 'This cannot be undone.' }
            }
          ]}
          pagination
          showRefresh
          getRowKey={(row) => row.id}
        />
      </div>
    </RequireConnection>
  );
}

Theming

Apps automatically adapt to host theme via CSS variables:

:root {
  --color-background-primary: #ffffff;
  --color-text-primary: #171717;
}

.dark {
  --color-background-primary: #0a0a0a;
  --color-text-primary: #fafafa;
}

Access in React:

const { theme } = useHostContext();
const isDark = theme === 'dark';

Editing Guidelines

DO

  • Create .tsx files alongside service index.ts files
  • Use @UIApp on methods paired with @Tool
  • Import from @leanmcp/ui/server in service files
  • Import from @leanmcp/ui in component files
  • Always wrap content in RequireConnection
  • Import @leanmcp/ui/styles.css

DON'T

  • Wrap components with AppProvider
  • Put @UIApp on class
  • Use direct imports in @UIApp({ component: ... })
  • Use wrong prop names (toolName, field, autoRefresh)
  • Add terminal commands in responses

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

Analyze tau2-bench task export files to identify exactly where and why tasks failed. Map failures to specific conversation rounds, show root causes, check whether relevant documentation was available, and cite sources. Use this skill whenever you need to debug a tau2-bench evaluation, understand why an agent made wrong decisions, identify missing knowledge, or review task performance—especially when context length, eligibility criteria, or policy compliance might be at fault.

日本語の概要は準備中です。原文の説明を表示しています。

Leanmcp/gateway-skills2,1122026年10月8日 更新

How to develop, modify, and extend the Claude Code codebase following its established programming style, patterns, and conventions. Use this skill whenever working on the claude-code source — adding tools, commands, utils, components, types, hooks, services, or fixing bugs. Also use when the user asks about claude-code architecture, coding standards, file organization, or wants to understand how the codebase works. This skill ensures all contributions match the existing codebase style exactly.

日本語の概要は準備中です。原文の説明を表示しています。

Leanmcp/gateway-skills2,1122026年10月8日 更新

Generate a complete, beginner-friendly explanation of an entire code repository as a set of markdown files. Maps the repo structure with `treecap` (installing it if missing), studies the actual source, then writes a `CODE_EXPLANATION/` folder containing an `OVERVIEW.md` plus numbered deep-dive files (01_architecture.md, 02_models.md, ...) that explain how the code is built, what it does, and how the pieces fit together — illustrated with real code excerpts. Use this skill whenever the user wants to understand, document, onboard onto, or get a walkthrough of a codebase: phrases like "explain this repo", "explain the code", "code explanation", "walk me through this codebase", "help me understand how this is built", "document the architecture", or "what does this project do" — even if they don't say the word "skill". Prefer this over ad-hoc summaries whenever the goal is a durable, written explanation of a whole repo.

日本語の概要は準備中です。原文の説明を表示しています。

Leanmcp/gateway-skills2,1122026年10月8日 更新

Run inference on Fireworks AI — direct, through the Leanmcp AI Gateway proxy (aigateway.leanmcp.com) for observability, or via LiteLLM / the OpenAI SDK / raw HTTP. Covers the endpoint-and-key matrix for each route, serverless vs dedicated deployments and their cold starts, browsing and filtering the model catalogue, reasoning models that return content:null with the answer in reasoning_content, function calling, concurrency and load testing, and runner scripts where many models share one protocol with a --use-proxy toggle. Use this skill whenever the user mentions Fireworks, FIREWORKS_API_KEY, api.fireworks.ai, fireworks_ai/ model ids, accounts/fireworks/models/..., a Fireworks deployment id, the Leanmcp gateway or LEANMCP_API_KEY, app.leanmcp.com observability, or gpt-oss / kimi / glm / qwen / deepseek served on Fireworks; wants to route LLM calls through a proxy for logging or cost tracking; is debugging empty responses, 404s, scope errors, cold starts, or "the gateway isn't working"; wants to pick a Fireworks model with tool calling; or wants to sweep several models over one benchmark. Reach for it even on vague asks like "why is this model returning nothing" or "set up inference for these models" when Fireworks is in play.

日本語の概要は準備中です。原文の説明を表示しています。

Leanmcp/gateway-skills2,1122026年10月8日 更新

Build, run, debug and ship 3D city games in Godot 4 — procedurally generated cities (block grids, districts, weighted building typologies, MultiMesh batching, facade shaders, terrain, day/night, landmarks), first-person controllers, headless verification, Blender-generated .glb assets, rigged character and animation packs, real OpenStreetMap-derived cities, WebAssembly export, and cloud GPU streaming on GCP. Use this skill whenever the work involves Godot or a .gd/.tscn/.gdshader/project.godot file; whenever someone wants to start, set up, clone onto a new machine, or stand up a 3D game or city generator; whenever they mention procedural city, city generator, block grid, building typology, MultiMesh, facade shader, terrain heightmap, navmesh, Godot export templates, export presets, headless Godot, .glb pipeline, Blender asset generation, character rigs or borrowed animations; and whenever they want to deploy or share a game — web export, SharedArrayBuffer/COOP/COEP, Selkies, pixel streaming, GPU VM, or "let people play this without installing anything." Reach for it even on vague asks like "get this running", "why is my city slow", "my model faces backwards", "make it work on a cloud machine", or "set this up somewhere else too" when a Godot 3D project is in play.

日本語の概要は準備中です。原文の説明を表示しています。

Leanmcp/gateway-skills2,1122026年10月8日 更新

Scaffold a native iOS app (SwiftUI, no dependencies) from nothing, run it in the Simulator, and archive + upload it to TestFlight entirely from the command line. Bundles working scripts for icon generation, simulator run, and signed upload via the App Store Connect API. Use this whenever the user wants to build, run, ship, or debug an iOS/iPhone/iPad app, mentions Xcode, SwiftUI, .xcodeproj, xcodebuild, simctl, provisioning, code signing, App Store Connect, TestFlight, archiving, or "get this on my phone" — and also when an existing iOS build is failing on icons, signing, teams, destinations, or duplicate build numbers.

日本語の概要は準備中です。原文の説明を表示しています。

Leanmcp/gateway-skills2,1122026年10月8日 更新

Leanmcp のスキルをすべて見る

このスキルの問題を報告する