mcpField
I
implementing-mcp-tools

implementing-mcp-tools

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

关于 implementing-mcp-tools

面向 PostHog 工程开发的 MCP 工具实现技能,指导如何从 API 端点生成工具,涵盖后端类型、YAML 配置及工具生命周期。

主要功能

  1. 01后端模式要求
  2. 02YAML 工具定义
  3. 03工具生成流程
  4. 04名称与 CI 校验
  5. 05功能标志门控
  6. 06弃用处理指南

适用场景

  1. 01向 Agent 开放产品 API 端点
  2. 02排查生成工具的模式错误
  3. 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 之前,请确认:

  1. 序列化器具有显式的字段类型和 help_text — 它们会一路传递到生成工具中的 Zod .describe()。 缺少描述 = Agent 只能猜测参数。 使用 ListField(child=serializers.CharField()) 而不是裸的 ListField(), 并在 JSONField 子类上使用 @extend_schema_field(PydanticModel) 以获得带类型的 Zod 输出 (参见 products/alerts/backend/presentation/views/alert.py 中的模式)。
  2. 普通的 ViewSet 方法要有 @extend_schema(request=...) — 否则 drf-spectacular 无法发现请求体, 生成的工具会得到 z.object({})(零参数)。 带有 serializer_class 的 ModelViewSet 没问题;手动验证的普通 ViewSet 则不行。
  3. 查询参数使用 @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 Code64 字符硬性限制;会为工具名称添加前缀 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 将其下线,这会以原子方式交换两者。
弃用工具

重命名或移除分为两个阶段。不要只做完第一个阶段就停止。

  1. 在调用方迁移期间使用别名。 将旧名称注册为一个轻量包装器,它调用当前 处理器并在响应中添加 _deprecation_notice。调用仍然成功,且 Agent 会得知新名称。参见 services/mcp/src/tools/skills/deprecatedAliases.ts,将其展开到 services/mcp/src/tools/index.ts 中的 TOOL_MAP。仅当替代项接受 相同参数时才使用别名。
  2. 删除别名时进行重定向。 在同一次变更中,将旧名称添加到 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 下注册它。

评论

支持 Markdown