关于 implementing-mcp-tools
面向 PostHog 工程开发的 MCP 工具实现技能,指导如何从 API 端点生成工具,涵盖后端类型、YAML 配置及工具生命周期。
主要功能
- 01后端模式要求
- 02YAML 工具定义
- 03工具生成流程
- 04名称与 CI 校验
- 05功能标志门控
- 06弃用处理指南
适用场景
- 01向 Agent 开放产品 API 端点
- 02排查生成工具的模式错误
- 03在端点发布期间门控工具
MCP 相关
提供 PostHog MCP 工具的实现指导,说明如何通过 YAML 定义将 API 操作映射为 MCP 客户端可用的工具。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ posthog/ posthog --skill implementing-mcp-tools安装后在 Claude Code 中输入/implementing-mcp-tools确认已加载。
详细介绍
实现 MCP 工具
请阅读完整指南:docs/published/handbook/engineering/ai/implementing-mcp-tools.md。
快速工作流
# 1. Only for a product with no YAML yet: create one with no tools.
# --product discovers endpoints via their x-product attribution.
# ViewSets in products/<name>/backend/ are auto-attributed via module
# path. ViewSets elsewhere need
# @extend_schema(extensions={"x-product": "<product>"}).
pnpm --filter=@posthog/mcp run scaffold-yaml -- --product your_product \
--output ../../products/your_product/mcp/tools.yaml
# 2. List the product's operations that have no YAML entry, then add the ones agents need.
# --add writes an enabled entry; title and description come from the API unless the YAML sets them.
# Add --file <path> to write to a YAML file other than the product's tools.yaml.
pnpm --filter=@posthog/mcp run scaffold-yaml -- --candidates --product your_product
pnpm --filter=@posthog/mcp run scaffold-yaml -- --add your_product_things_list --product your_product
# 3. Configure the entry — description, and annotations for PATCH/POST/PUT
# (scopes come from the API when omitted)
# Place in products/<product>/mcp/*.yaml (preferred) or services/mcp/definitions/*.yaml
# 4. Add a HogQL system table in posthog/hogql/database/schema/system.py
# and a model reference in products/posthog_ai/skills/querying-posthog-data/references/
# 5. Generate handlers and schemas
hogli build:openapi
# 6. Refresh the tool input schema snapshots (CI unit tests fail on a stale snapshot)
pnpm --filter=@posthog/mcp exec vitest run tests/unit/tool-schema-snapshots.test.ts -u
# A tool behind a new `feature_flag` needs that flag in the test's `featureFlags` map, set to the value that shows the tool:
# true for a plain gate, the variant string for a variant gate, a non-true value for a `disable` gate.
# 7. Only when the YAML uses ui_apps: regenerate the UI apps (CI checks they are current)
pnpm --filter=@posthog/mcp run generate:ui-apps
开始搭建之前:先修好后端
只有当 Django 后端暴露正确的类型时,代码生成流水线才能生成正确的工具。 请阅读类型系统指南了解全貌。
在编写 YAML 之前,请确认:
- 序列化器具有显式的字段类型和
help_text— 它们会一路传递到生成工具中的 Zod.describe()。 缺少描述 = Agent 只能猜测参数。 使用ListField(child=serializers.CharField())而不是裸的ListField(), 并在JSONField子类上使用@extend_schema_field(PydanticModel)以获得带类型的 Zod 输出 (参见products/alerts/backend/presentation/views/alert.py中的模式)。 - 普通的
ViewSet方法要有@extend_schema(request=...)— 否则 drf-spectacular 无法发现请求体, 生成的工具会得到z.object({})(零参数)。 带有serializer_class的ModelViewSet没问题;手动验证的普通ViewSet则不行。 - 查询参数使用
@validated_request或带查询序列化器的@extend_schema— 否则布尔和数组查询参数可能在生成的代码中产生类型不匹配。
如果生成的工具 schema 为空或错误,修复点几乎总是在 Django 一侧,
而不是 YAML 配置中。
如需完整的审计清单和修改前后示例,请使用 improving-drf-endpoints 技能。
何时添加 MCP 工具
当产品暴露了 Agent 应能调用的 API 端点时。 MCP 工具是原子能力(列出、获取、创建、更新、删除)—— 而不是工作流。
如果你要添加新端点,请检查它是否应该对 Agent 可访问。 如果是,就添加 YAML 定义并生成工具。
工具设计
工具应该是基础能力 —— 原子 CRUD 操作和简单动作。 Agent 将这些原语组合成更高层的工作流。
好:「列出功能开关」、「按 ID 获取实验」、「创建问卷」。 不好:「搜索某个实验的会话录制」—— 它捆绑了多个关注点。
工具命名约束
工具名称和功能标识符在构建时和 CI 中校验。 违规会导致构建失败。
工具名称
- 格式:小写 kebab-case —— 仅允许
[a-z0-9-],不能有前导或尾随连字符 - 长度:52 个字符或更少
- 约定:
domain-action,例如cohorts-create、dashboard-get、feature-flags-list
让动作动词远离紧凑的工具域
单次执行提示词用 ToolDomainExtractor 构建其紧凑的域索引。
大型工具族可能在中间段处拆分。
如果不裁剪动作,experiment-freeze-exposure 可能会宣传冗余的域
experiment-freeze,而不是 experiment。
每当你添加或重命名动作工具时,都要在同一次变更中检查
TRAILING_ACTIONS 集合。
如果渲染出的域可能以某个尚未加入的操作动词结尾,就添加该动词。并在
services/mcp/tests/unit/instructions.test.ts 中覆盖它。即使该动词不是完整工具名称的
最后一段,这一点同样适用。
添加诸如 freeze、publish 或 emit 之类的操作动词。不要仅仅为了让提示词更短
而添加 config、logs、stats 或 schedule 之类的资源或能力名词;
它们仍然是有用的发现域。
功能标识符
- 格式:小写 snake_case —— 仅允许
[a-z0-9_],必须以字母开头 - 约定:应与产品文件夹名称匹配,例如
error_tracking、feature_flags
为什么是 52 个字符?
MCP 客户端对工具名称强制不同的长度限制。52 字符限制是一个安全区间,在所有已知客户端中都能正常工作:
| 客户端 | 限制 | 备注 |
|---|---|---|
| MCP spec(草案) | 1–128 字符,[A-Za-z0-9_\-.] | 官方建议,未强制执行 |
| Claude Code | 64 字符 | 硬性限制;会为工具名称添加前缀 mcp____ |
| Cursor | 合计 60 字符 | server_name + tool_name;超过此长度的工具会被静默过滤 |
| OpenAI API | ^[a-zA-Z0-9_-]+$,64 字符 | 不允许使用点号 |
当服务名称为 "posthog"(7 字符)再加上一个分隔符时,工具名称必须保持在 52 个字符或以下,才能符合 Cursor 合计 60 字符的限制。
CI 强制执行
pnpm --filter=@posthog/mcp lint-tool-names— 校验 YAML 和 JSON 定义的长度和模式- 一个 vitest 测试会校验所有运行时的
TOOL_MAP和GENERATED_TOOL_MAP条目
YAML 定义
YAML 文件配置哪些操作会作为 MCP 工具暴露。 参考现有定义了解模式:
products/<product>/mcp/*.yaml— 首选,让配置与代码保持接近services/mcp/definitions/*.yaml— 用于没有产品文件夹的功能的备选位置
构建流水线会从这两个路径发现 YAML 文件。
关键字段
category: Human readable name
feature: snake_case_name # should match the product folder name (used for runtime filtering)
url_prefix: /path # frontend app route, used for enrich_url links
tools:
your-tool-name: # kebab-case
operation: operationId_from_openapi
enabled: true
# Optional:
scopes: # defaults to the scopes the API requires (from the OpenAPI spec)
- your_product:read
annotations: # defaults for GET and DELETE; required for PATCH, POST and PUT
readOnly: true
destructive: false
idempotent: true
title: List things
description: >
Human-friendly description for the LLM.
list: true
enrich_url: '{id}'
param_overrides:
name:
description: Custom description for the LLM
response: # filter response fields (applied per-item on list endpoints)
include: [id, key, name] # keep only these fields (dot-path wildcards supported)
exclude: [filters.groups.*.properties] # remove these fields
# include and exclude are mutually exclusive
selectable: true # add optional `fields` param so the agent picks a subset of `include` per call
# (constrained to the allowlist); omit `fields` to return the full set. Requires `include`.
strip_nulls: true # remove keys whose value is `null`, applied after include/exclude
# Use it on tools that echo a nested serializer schema, where the unset optional fields
# dominate the payload. Rejected with `list: true`, where per-row null removal makes the
# TOON table larger. Use `exclude` to drop the fields on a list tool instead.
feature_flag: my-flag-key # gate this tool behind a PostHog feature flag
feature_flag_behavior: enable # 'enable' (default) or 'disable'
省略 scopes 时,生成器会使用 API 所需的 scopes,因此工具不会与端点产生偏差。
仅当 API 按请求计算 scopes(生成器会失败并提示)或需要更严格地限制工具时,才手动设置 scopes。
如果 scopes 列表遗漏了 API 所需的某个 scope,代码生成会失败,并在 CI 上给出 GitHub 注解。
当 API 按请求选择 scopes 时,请在 viewset 的 request_dependent_scope_actions 中列出该 action。
随后规范会用 x-request-dependent-scopes 标记该操作,代码生成会跳过对它的检查,而其工具必须声明 scopes。
annotations 对于 GET(只读)和 DELETE(破坏性)默认使用 HTTP 方法。
PATCH、POST 和 PUT 差异太大(PATCH 可以是软删除,也可以是非幂等的),因此需要为它们声明 annotations。
当工具需要在请求前后添加自定义逻辑时,请设置 hooks: <src/tools/ 下的路径>,并从该模块默认导出一个包含 beforeRequest、afterResponse 或 onError 的对象,写法为 export default { onError } satisfies ToolHooks<Params>,这样拼写错误会导致类型检查失败(参见 src/tools/tool-hooks.ts 中的 ToolHooks)。
用它可以在写入前读取状态,或将已知错误转换为结果,而不是用手写工具遮蔽生成的工具。
未知键会在构建时被拒绝(Zod .strict())。
使用功能标志对工具进行门控
为任意工具(标准工具或查询包装器)添加 feature_flag,即可根据在 MCP 初始化时针对当前用户评估的 PostHog 功能标志来控制其是否暴露。
feature_flag_behavior: enable(默认)— 工具仅在标志开启时显示。用于逐步发布新工具。feature_flag_behavior: disable— 工具在标志开启时隐藏。用于淘汰旧工具。
使用同一个标志键同时配置这两种行为可实现原子切换:标志开启 → 新工具可见,旧工具隐藏;标志关闭 → 旧工具可见,新工具隐藏。适用于对工具变体进行 A/B 测试。
标志会在初始化时通过 evaluateFeatureFlags 并行评估。如果某个标志无法评估(服务错误、标志缺失),则 enable 门控的工具会被排除,而 disable 门控的工具会被包含——对新工具失败关闭,对现有工具失败开放。
启用或重命名工具
MCP 服务和 Django 分别部署。 如果一个工具在其路由上线之前就到达了客户端,那么每次调用都会返回 404,直到 Django 部署跟上为止。 这会同时影响整个 Agent 集群。
- 先让路由上线。 先发布端点,然后在后续变更中启用该工具。如果一个工具在
全新路由的同一个提交中设置了
enabled: true,那么 MCP 服务一部署,它就会在客户端中生效。 - 或者对它进行门控。 添加
feature_flag和feature_flag_behavior: enable,并在 路由开始提供服务后打开该标志。 - 重命名时保留旧名称。 将之前的工具名称保留在 YAML 中,指向同一个
操作,直到新名称已在所有地方部署。使用
同一个标志键上的
feature_flag_behavior: disable将其下线,这会以原子方式交换两者。
弃用工具
重命名或移除分为两个阶段。不要只做完第一个阶段就停止。
- 在调用方迁移期间使用别名。 将旧名称注册为一个轻量包装器,它调用当前
处理器并在响应中添加
_deprecation_notice。调用仍然成功,且 Agent 会得知新名称。参见services/mcp/src/tools/skills/deprecatedAliases.ts,将其展开到services/mcp/src/tools/index.ts中的TOOL_MAP。仅当替代项接受 相同参数时才使用别名。 - 删除别名时进行重定向。 在同一次变更中,将旧名称添加到
services/mcp/src/tools/exec.ts中的DEPRECATED_TOOL_REDIRECTS。这样调用会失败并返回deprecated_tool错误,错误中会指出替代项,而不是泛化的Unknown tool: "..."。 在文本中说明任何参数变化——参见self-driving-inbox-get条目。
当替代项不是可直接替换时,直接进入阶段 2。一个悄悄丢弃已重命名 参数的别名,比一个失败的调用更糟。
保留重定向条目,直到旧名称不再收到流量。isRecordableToolName 只会为服务拥有的名称记录
$mcp_exec_target_tool:一个在线工具,或一个
DEPRECATED_TOOL_REDIRECTS 键。如果你过早删除该条目,剩余调用在 MCP 分析中
将变得无法归因,你也就无法再判断是否还有任何东西仍在使用旧名称。
功能标志移除的工具是另一种情况。它会保留其定义,在 YAML 中声明
superseded_by,并由 flagGatedToolMessage 应答调用。
有意保持某个操作关闭
工具是选择加入的:没有 YAML 条目的操作不会被暴露,并且不会有任何东西为新端点写入条目。
只有在记录某个决定时才写入 enabled: false 的条目,例如一个被另一个工具取代的工具。
这样的条目需要 disabled_reason: <why>。同步会将没有该字段的 enabled: false 条目视为遗留存根并移除,而代码生成会拒绝已启用工具上的 disabled_reason。
端点变更后同步
pnpm --filter=@posthog/mcp run scaffold-yaml -- --sync-all
幂等,且从不添加条目。它会保留操作仍然存在的每个条目,更新重新编号的 _N 操作 ID,
删除操作已不存在的禁用条目,并在已启用工具的操作已不存在时失败。
序列化器描述
描述贯穿整个流水线:
Django serializer field → OpenAPI spec → Zod schema → MCP tool description
这些描述就是 Agent 阅读以理解工具参数的内容。
- 在序列化器字段上使用
help_text——它会成为 OpenAPI 描述。 - 在 YAML 中使用
param_overrides,用命令式指令覆盖生成的描述。 - 具体说明格式、约束和有效值。
- 避免使用大模型在没有上下文时无法理解的术语。
HogQL 系统表
每个 list/get 端点都应在 posthog/hogql/database/schema/system.py 中有一个对应的 HogQL 系统表。
这样 Agent 就能通过 SQL 查询数据。
每个系统表必须包含 team_id 列,用于数据隔离。
添加系统表时,还要在 products/posthog_ai/skills/querying-posthog-data/references/ 中添加一个模型参考文件(models-<domain>.md),
并在 products/posthog_ai/skills/querying-posthog-data/SKILL.md 的 Data Schema 下注册它。