mcpField
I
implementing-mcp-ui-apps

implementing-mcp-ui-apps

github.com/posthog开发40.5k168 次安装今天更新
代码仓库

关于 implementing-mcp-ui-apps

面向 PostHog 工程开发的技能,用于构建与 MCP 工具结果一同呈现的交互式 React 视图,涵盖详情、列表和自定义应用及其配置。

主要功能

  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 应用是交互式 React 可视化组件,会在 MCP 客户端(例如 Claude Desktop)中与工具结果一起渲染。它们基于 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。

type: detail——单实体视图

使用包裹在 AppWrapper 中的视图组件渲染一个实体。

必需字段:

FieldDescription
type必须为 'detail'。
view_prop传递给视图组件的 React prop 名称(例如 data、action、flag)。无法推导——必须与组件的 props 匹配。

可选字段(省略时按约定推导):

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'用于在加载状态和回退聊天消息中显示的 item 对象字段。当 item 由 name 以外的内容标识时覆盖此项,例如功能标志使用 key。
click_prop'on' + PascalCase(singularKey) + 'Click'传递给列表组件的点击处理函数的 prop 名称。单数 key 通过去除 -list 后缀得出。例如:key action-list → onActionClick。当你的组件使用更短的名称时覆盖此项,例如使用 onFlagClick 而非 onFeatureFlagClick。
entity_label单数 key 的 kebab 转空格用于回退聊天消息的人类可读标签("Show me the details for {entity_label} ...")。例如:key error-issue-list → error issue。

带有约定默认值的可选字段(与 detail 应用相同的模式):

字段默认值描述
app_name"PostHog " + titleCase(key)显示名称。
descriptiontitleCase(key) + " view"简短描述。
component_importproducts/{product}/mcp/apps导入路径。
list_data_typePascalCase(singularKey) + "ListData"列表响应的 TypeScript 类型。示例:key action-list → ActionListData。
item_data_typePascalCase(singularKey) + "Data"单个条目的 TypeScript 类型。示例:key action-list → ActionData。
view_componentPascalCase(key) + "View"React 组件名称。示例:key 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接收工具结果的 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.yml 中检查生成的文件是否为最新。 如果你修改了 YAML 的 ui_apps 部分,请运行 pnpm --filter=@posthog/mcp run generate:ui-apps 并提交结果。

评论

支持 Markdown