mcpField
I
implementing-mcp-ui-apps

implementing-mcp-ui-apps

github.com/posthog開発40.5k168 回インストール今日更新
リポジトリ

implementing-mcp-ui-apps について

MCP ツールの結果と並べて表示する、インタラクティブな React ビューを構築する PostHog エンジニアリング向けスキルです。詳細、リスト、カスタムアプリとその設定を扱います。

主な機能

  1. 01React の詳細・リストビュー
  2. 02カスタム UI アプリのエントリーポイント
  3. 03YAML によるアプリ設定
  4. 04ツールとアプリの関連付け
  5. 05生成アプリの検証

活用シーン

  1. 01ツール結果をテーブルやチャートで表示
  2. 02リストから詳細へのドリルダウン追加
  3. 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 つのエンティティをレンダリングします。

必須フィールド:

FieldDescription
type'detail' である必要があります。
view_propビューコンポーネントに渡される React のプロパティ名(例: data、action、flag)。導出できません — コンポーネントのプロパティと一致させる必要があります。

任意フィールド(省略時は規約により導出されます):

FieldDefaultDescription
app_name"PostHog " + titleCase(key)MCP クライアントに表示される表示名。例: キー error-details → "PostHog Error Details"。
descriptiontitleCase(key) + " detail view"MCP リソースレジストリ用の短い説明。
component_importproducts/{product}/mcp/appsビューコンポーネントのインポートパス。プロダクトディレクトリ内の YAML ファイルの場所から自動導出されます。
data_typePascalCase(key) + "Data"ツール結果の TypeScript 型。例: キー error-details → ErrorDetailsData。
view_componentPascalCase(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)表示名。
descriptiontitleCase(key) + " view"簡単な説明。
component_importproducts/{product}/mcp/appsインポートパス。
list_data_typePascalCase(singularKey) + "ListData"リストレスポンスの TypeScript 型。例: キー action-list → ActionListData。
item_data_typePascalCase(singularKey) + "Data"単一アイテムの TypeScript 型。例: キー action-list → ActionData。
view_componentPascalCase(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 コメントがあります。

アプリタイプに新しいフィールドを追加するには:

  1. 該当する Zod スキーマ(DetailUiAppSchema、ListUiAppSchema、または CustomUiAppSchema)に デフォルトがある場合は .optional() を付けて追加します
  2. 対応する Resolved* インターフェース(ResolvedDetailUiApp、ResolvedListUiApp、または ResolvedCustomUiApp)に追加します
  3. generate-ui-apps.ts の resolveDetailApp()、resolveListApp()、または resolveCustomApp() にデフォルトの導出を追加します
  4. エントリーポイントまたはディスパッチジェネレーターで解決済みの値を使用します

すべてのスキーマは .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 を実行し、その結果をコミットしてください。

コメント