关于 implementing-mcp-ui-apps
面向 PostHog 工程开发的技能,用于构建与 MCP 工具结果一同呈现的交互式 React 视图,涵盖详情、列表和自定义应用及其配置。
主要功能
- 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 应用是交互式 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 中的视图组件渲染一个实体。
必需字段:
| Field | Description |
|---|---|
type | 必须为 'detail'。 |
view_prop | 传递给视图组件的 React prop 名称(例如 data、action、flag)。无法推导——必须与组件的 props 匹配。 |
可选字段(省略时按约定推导):
| 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' | 用于在加载状态和回退聊天消息中显示的 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) | 显示名称。 |
description | titleCase(key) + " view" | 简短描述。 |
component_import | products/{product}/mcp/apps | 导入路径。 |
list_data_type | PascalCase(singularKey) + "ListData" | 列表响应的 TypeScript 类型。示例:key action-list → ActionListData。 |
item_data_type | PascalCase(singularKey) + "Data" | 单个条目的 TypeScript 类型。示例:key action-list → ActionData。 |
view_component | PascalCase(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 注释,说明其用途和默认值。
要向应用类型添加新字段:
- 将其添加到相关的 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.yml 中检查生成的文件是否为最新。
如果你修改了 YAML 的 ui_apps 部分,请运行 pnpm --filter=@posthog/mcp run generate:ui-apps
并提交结果。