implementing-mcp-ui-apps について
MCP ツールの結果と並べて表示する、インタラクティブな React ビューを構築する PostHog エンジニアリング向けスキルです。詳細、リスト、カスタムアプリとその設定を扱います。
主な機能
- 01React の詳細・リストビュー
- 02カスタム UI アプリのエントリーポイント
- 03YAML によるアプリ設定
- 04ツールとアプリの関連付け
- 05生成アプリの検証
活用シーン
- 01ツール結果をテーブルやチャートで表示
- 02リストから詳細へのドリルダウン追加
- 03カスタム MCP アプリの設定
MCP 対応
ツール結果とともに MCP クライアントが表示する UI アプリの構築方法と、アプリの登録および MCP ツールとの関連付けを説明します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ posthog/ posthog --skill implementing-mcp-ui-appsインストール後、Claude Code で/implementing-mcp-ui-appsを入力して読み込まれたか確認します。
詳細
MCP UI アプリの実装
MCP UI アプリは、MCP クライアント(例: Claude Desktop)でツールの結果と並んでレンダリングされるインタラクティブな React ビジュアライゼーションです。これらは Quill コンポーネントライブラリで構築され、Cloudflare Workers Static Assets を介して配信されます。
完全なリファレンス: services/mcp/CONTRIBUTING.md。
クイックワークフロー
# 1. Create view components in your product's mcp/apps/ directory
# (see "View components" below)
# 2. Add ui_apps entries to your product's mcp/tools.yaml
# (see "YAML configuration" below)
# 3. Link tools to apps with ui_app: <key> in the tools section
# 4. Generate entry points + registry, then build
pnpm --filter=@posthog/mcp run generate:ui-apps
pnpm --filter=@posthog/mcp run build
UI アプリを追加すべきタイミング
MCP ツールが、視覚的な表示が役立つ構造化データ(テーブル、詳細ビュー、チャート、ステータスバッジなど)を返す場合です。UI アプリがない場合、ツールの結果はチャット内でプレーンテキストまたは JSON として表示されます。
アーキテクチャ
products/{product}/mcp/
apps/ # React view components (you write these)
EntityView.tsx # Detail view
EntityListView.tsx # List view (uses ListDetailView from @posthog/mcp-ui)
index.ts # Barrel exports
tools.yaml # YAML config: ui_apps + tools
services/mcp/
src/ui-apps/apps/
generated/ # Auto-generated entry points (don't edit)
debug.tsx # Custom/manual entry points
query-results.tsx
src/resources/
ui-apps.generated.ts # Auto-generated: URI constants, UiAppKey, URI_MAP, UI_APPS
ui-apps.ts # Hand-authored: withUiApp(), registerUiAppResources()
scripts/
generate-ui-apps.ts # The generator — reads YAML, writes entry points + registry
yaml-config-schema.ts # Zod schemas for YAML validation (source of truth for field definitions)
ビューコンポーネント
ビューコンポーネントは products/{product}/mcp/apps/ に配置します。
詳細ビュー — 単一のエンティティをレンダリングします:
import { type ReactElement } from 'react'
import { DescriptionList } from '@posthog/mcp-ui'
import { Card, CardHeader, CardTitle, CardContent } from '@posthog/quill'
export interface MyEntityData {
id: number
name: string
// ... fields from the API response
}
export function MyEntityView({ data }: { data: MyEntityData }): ReactElement {
return (
<Card>
<CardHeader>
<CardTitle>{data.title}</CardTitle>
</CardHeader>
<CardContent>
<DescriptionList items={[{ label: 'ID', value: String(data.id) }]} />
</CardContent>
</Card>
)
}
リストビュー — リストから詳細へのステートマシンには @posthog/mcp-ui の ListDetailView を使用します:
import { type ReactElement, type ReactNode } from 'react'
import { DataTable, type DataTableColumn, ListDetailView } from '@posthog/mcp-ui'
import { MyEntityView, type MyEntityData } from './MyEntityView'
export interface MyEntityListData {
results: MyEntityData[]
_posthogUrl?: string
}
export interface MyEntityListViewProps {
data: MyEntityListData
onMyEntityClick?: (entity: MyEntityData) => Promise<MyEntityData | null>
}
export function MyEntityListView({ data, onMyEntityClick }: MyEntityListViewProps): ReactElement {
return (
<ListDetailView<MyEntityData>
onItemClick={onMyEntityClick}
backLabel="All entities"
getItemName={(e) => e.name}
renderDetail={(e) => <MyEntityView data={e} />}
renderList={(handleClick) => {
const columns: DataTableColumn<MyEntityData>[] = [
{
key: 'name',
header: 'Name',
sortable: true,
render: (row): ReactNode =>
onMyEntityClick ? (
<button onClick={() => handleClick(row)} className="text-link underline ...">
{row.name}
</button>
) : (
row.name
),
},
]
return (
<div className="p-4">
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
{data.results.length} entit{data.results.length === 1 ? 'y' : 'ies'}
</span>
</div>
<DataTable<CohortData>
columns={columns}
data={data.results}
pageSize={10}
defaultSort={{ key: 'name', direction: 'asc' }}
emptyMessage="No entities found"
/>
</div>
</div>
)
}}
/>
)
}
バレルエクスポート (index.ts):
export { MyEntityView, type MyEntityData } from './MyEntityView'
export { MyEntityListView, type MyEntityListData, type MyEntityListViewProps } from './MyEntityListView'
YAML 設定
products/{product}/mcp/tools.yaml の ui_apps セクションで UI アプリを定義します。各キーはアプリ識別子になります(URI、定数、withUiApp 呼び出しで使用されます)。
アプリの種類は detail、list、custom の 3 つです。
type: detail — 単一エンティティビュー
AppWrapper でラップされたビューコンポーネントを使用して、1 つのエンティティをレンダリングします。
必須フィールド:
| Field | Description |
|---|---|
type | 'detail' である必要があります。 |
view_prop | ビューコンポーネントに渡される React のプロパティ名(例: data、action、flag)。導出できません — コンポーネントのプロパティと一致させる必要があります。 |
任意フィールド(省略時は規約により導出されます):
| Field | Default | Description |
|---|---|---|
app_name | "PostHog " + titleCase(key) | MCP クライアントに表示される表示名。例: キー error-details → "PostHog Error Details"。 |
description | titleCase(key) + " detail view" | MCP リソースレジストリ用の短い説明。 |
component_import | products/{product}/mcp/apps | ビューコンポーネントのインポートパス。プロダクトディレクトリ内の YAML ファイルの場所から自動導出されます。 |
data_type | PascalCase(key) + "Data" | ツール結果の TypeScript 型。例: キー error-details → ErrorDetailsData。 |
view_component | PascalCase(key) + "View" | React コンポーネント名。例: キー error-details → ErrorDetailsView。 |
最小の例:
ui_apps:
action:
type: detail
view_prop: action
オーバーライドを使用する例(規約が実際のコードと一致しない場合):
ui_apps:
llm-costs:
type: detail
view_prop: data
data_type: LLMCostsData # convention would produce LlmCostsData
view_component: LLMCostsView # convention would produce LlmCostsView
type: list — ドリルダウン付きリスト
リストコンポーネントをレンダリングします。項目がクリックされると、app.callServerTool() を介して詳細ツールを呼び出し、詳細ビューをインラインで表示します。MCP クライアントがアプリからのツール呼び出しをサポートしていない場合は、チャットメッセージにフォールバックします。
必須フィールド:
| フィールド | 説明 |
|---|---|
type | 'list' である必要があります。 |
detail_tool | リスト項目がクリックされたときに呼び出すツール名(例: 'action-get'、'cohorts-retrieve')。任意の YAML ファイルの tools セクションで定義された有効なツール名である必要があります。 |
動作のデフォルト値を持つオプションフィールド:
| フィールド | デフォルト | 説明 |
|---|---|---|
detail_args | '{ id: item.id }' | detail_tool に渡される引数の JS 式。変数 item はクリックされたリスト項目を参照します。ツールが異なるパラメータ名を使用する場合は上書きしてください(例: '{ flagId: item.id }')。 |
item_name_field | 'name' | 読み込み状態やフォールバックのチャットメッセージでの表示に使用される、項目オブジェクトのフィールド。項目が name 以外で識別される場合は上書きしてください(例: フィーチャーフラグの場合は key)。 |
click_prop | 'on' + PascalCase(singularKey) + 'Click' | リストコンポーネントに渡されるクリックハンドラーのプロパティ名。単数形のキーは -list サフィックスを取り除いて導出されます。例: キー action-list → onActionClick。コンポーネントがより短い名前を使用する場合は上書きしてください(例: onFeatureFlagClick の代わりに onFlagClick)。 |
entity_label | 単数形キーのケバブケースをスペースに変換したもの | フォールバックのチャットメッセージ(「{entity_label} の詳細を表示して...」)で使用される人間が読めるラベル。例: キー error-issue-list → error issue。 |
規約に基づくデフォルト値を持つオプションフィールド(詳細アプリと同じパターン):
| フィールド | デフォルト | 説明 |
|---|---|---|
app_name | "PostHog " + titleCase(key) | 表示名。 |
description | titleCase(key) + " view" | 簡単な説明。 |
component_import | products/{product}/mcp/apps | インポートパス。 |
list_data_type | PascalCase(singularKey) + "ListData" | リストレスポンスの TypeScript 型。例: キー action-list → ActionListData。 |
item_data_type | PascalCase(singularKey) + "Data" | 単一アイテムの TypeScript 型。例: キー action-list → ActionData。 |
view_component | PascalCase(key) + "View" | React コンポーネント名。例: キー action-list → ActionListView。 |
最小限の例:
ui_apps:
action-list:
type: list
detail_tool: action-get
オーバーライドを使用した例:
ui_apps:
feature-flag-list:
type: list
detail_tool: feature-flag-get-definition
detail_args: '{ flagId: item.id }' # tool expects flagId, not id
item_name_field: key # flags are identified by key, not name
click_prop: onFlagClick # component uses onFlagClick, not onFeatureFlagClick
type: custom — 手書きのエントリーポイント
完全にカスタムなロジックが必要なアプリ向けです(例: debug.tsx、query-results.tsx)。
ジェネレーターはエントリーポイントを作成しません。services/mcp/src/ui-apps/apps/{key}.tsx で
手動で管理します。レジストリエントリは常に生成されます。
カスタムアプリが再利用可能なビューコンポーネントを公開する場合、オプションの render_ui
設定によって render-ui アンブレラツールにも追加されます。
必須フィールド:
| フィールド | 説明 |
|---|---|
type | 'custom' である必要があります。 |
app_name | 表示名。これを導出するための規約がないため必須です(カスタムアプリは命名パターンに従わない場合があります)。 |
description | 簡単な説明。同じ理由で必須です。 |
オプションの render_ui フィールド:
| フィールド | 説明 |
|---|---|
component_import | 再利用可能なビューコンポーネントのインポートパス。src/ui-apps/generated からの相対パス。 |
view_component | ツール結果をレンダリングする React コンポーネント名。 |
view_prop | ツール結果を受け取るプロップ名。 |
data_type | ツール結果の TypeScript 型。コンポーネントが unknown を受け入れる場合は省略します。 |
例:
ui_apps:
query-results:
type: custom
app_name: Query Results
description: Interactive visualization for PostHog query results
render_ui:
component_import: ../components/Component
view_component: Component
view_prop: data
スキーマの場所
これらの YAML フィールドを検証する Zod スキーマは services/mcp/scripts/yaml-config-schema.ts にあります。 各フィールドには、その目的とデフォルトを説明する JSDoc コメントがあります。
アプリタイプに新しいフィールドを追加するには:
- 該当する Zod スキーマ(
DetailUiAppSchema、ListUiAppSchema、またはCustomUiAppSchema)に デフォルトがある場合は.optional()を付けて追加します - 対応する
Resolved*インターフェース(ResolvedDetailUiApp、ResolvedListUiApp、またはResolvedCustomUiApp)に追加します generate-ui-apps.tsのresolveDetailApp()、resolveListApp()、またはresolveCustomApp()にデフォルトの導出を追加します- エントリーポイントまたはディスパッチジェネレーターで解決済みの値を使用します
すべてのスキーマは .strict() を使用しています — 不明なキーはビルド時に拒否され、タイプミスを検出できます。
ツールを UI アプリにリンクする
同じ YAML ファイルの tools セクションで、ui_app を使用して ui_apps キーを参照します:
tools:
my-entity-get:
operation: my_entities_retrieve
enabled: true
ui_app: my-entity # references ui_apps.my-entity
my-entity-list:
operation: my_entities_list
enabled: true
ui_app: my-entity-list # references ui_apps.my-entity-list
ジェネレーターは、すべての ui_app の値が、すべての YAML ファイル内のいずれかの ui_apps セクションに存在するキーを指していることを検証します。
手書きのツール(YAML で生成されていないもの)の場合は、TypeScript で withUiApp を使用します:
import { withUiApp } from '@/resources/ui-apps'
import { withPostHogUrl, type WithPostHogUrl } from '@/tools/tool-utils'
import type { Context, ToolBase } from '@/tools/types'
type Result = WithPostHogUrl<MyEntityData>
export default (): ToolBase<typeof schema, Result> =>
withUiApp('my-entity', {
name: 'my-entity-get',
schema,
handler: async (context, params) => {
const projectId = await context.stateManager.getProjectId()
const data = await fetchEntity(context, params)
return await withPostHogUrl(context, data, `/my-entities/${data.id}`)
},
})
appKey パラメーターは、生成された UiAppKey ユニオンに対して型チェックされます — 無効なキーはコンパイル時のエラーになります。
CI 検証
CI は、ci-mcp.yml と ci-mcp-ui-apps の両方で、生成されたファイルが最新であることをチェックします。
YAML の ui_apps セクションを変更した場合は、pnpm --filter=@posthog/mcp run generate:ui-apps を実行し、その結果をコミットしてください。