SuperAPI AI Directory

implementing-mcp-tools

Agents & models

View repository

Updated 10-16 08:07

About this skill

Implementing MCP tools

Read the full guide at docs/published/handbook/engineering/ai/implementing-mcp-tools.md.

Quick workflow

# 1. Scaffold a starter YAML with all operations disabled.
#    --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. Configure the YAML — enable tools, add scopes, annotations, descriptions
#    Place in products/<product>/mcp/*.yaml (preferred) or services/mcp/definitions/*.yaml
3. 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/ 4. Generate handlers and schemas

hogli build:openapi


## Before you scaffold: fix the backend first

The codegen pipeline can only generate correct tools if the Django backend exposes correct types.
Read the [type system guide](../../../docs/published/handbook/engineering/type-system.md) for the full picture.

Before scaffolding YAML, verify:

1. **Serializers have explicit field types and `help_text`** —
   these flow all the way to Zod `.describe()` in the generated tool.
   Missing descriptions = agents guessing at parameters.
   Use `ListField(child=serializers.CharField())` instead of bare `ListField()`,
   and `@extend_schema_field(PydanticModel)` on `JSONField` subclasses to get typed Zod output
   (see `posthog/api/alert.py` for the pattern).
2. **Plain `ViewSet` methods have `@extend_schema(request=...)`** —
   without it, drf-spectacular can't discover the request body
   and the generated tool gets `z.object({})` (zero parameters).
   `ModelViewSet` with a `serializer_class` is fine; plain `ViewSet` with manual validation is not.
3. **Query parameters use `@validated_request`** or `@extend_schema` with a query serializer —
   otherwise boolean and array query params may produce type mismatches in the generated code.

If a generated tool has an empty or wrong schema, the fix is almost always on the Django side,
not in the YAML config.
For a full audit checklist and before/after examples, use the `improving-drf-endpoints` skill.

## When to add MCP tools

When a product exposes API endpoints that agents should be able to call.
MCP tools are atomic capabilities (list, get, create, update, delete) — not workflows.

If you're adding a new endpoint, check whether it should be agent-accessible.
If yes, add a YAML definition and generate the tool.

## Tool design

Tools should be **basic capabilities** — atomic CRUD operations and simple actions.
Agents compose these primitives into higher-level workflows.

Good: "List feature flags", "Get experiment by ID", "Create a survey".
Bad: "Search for session recordings of an experiment" — bundles multiple concerns.

## Tool naming constraints

Tool names and feature identifiers are validated at build time and in CI.
Violations fail the build.

### Tool names

- **Format**: lowercase kebab-case — only `[a-z0-9-]`, no leading/trailing hyphens
- **Length**: 52 characters or fewer
- **Convention**: `domain-action`, e.g. `cohorts-create`, `dashboard-get`, `feature-flags-list`

### Feature identifiers

- **Format**: lowercase snake*case — only `[a-z0-9*]`, must start with a letter
- **Convention**: should match the product folder name, e.g. `error_tracking`, `feature_flags`

### Why 52 characters?

MCP clients enforce different limits on tool names. The 52-char limit is the safe zone
that works across all known clients:

| Client           | Limit                          | Notes                                                            |
| ---------------- | ------------------------------ | ---------------------------------------------------------------- |
| MCP spec (draft) | 1–128 chars, `[A-Za-z0-9_\-.]` | Official recommendation, not enforced                            |
| Claude Code      | 64 chars                       | Hard limit; prefixes tool names with `mcp____`                   |
| Cursor           | 60 chars combined              | `server_name + tool_name`; tools over this are silently filtered |
| OpenAI API       | `^[a-zA-Z0-9_-]+$`, 64 chars   | No dots allowed                                                  |

With the server name "posthog" (7 chars) plus a separator, tool names must stay at
or below 52 characters to fit within Cursor's 60-char combined limit.

### CI enforcement

- `pnpm --filter=@posthog/mcp lint-tool-names` — validates length and pattern for YAML and JSON definitions
- A vitest test validates all runtime `TOOL_MAP` and `GENERATED_TOOL_MAP` entries

## YAML definitions

YAML files configure which operations are exposed as MCP tools.
See existing definitions for patterns:

- `products/<product>/mcp/*.yaml` — preferred, keeps config close to the code
- `services/mcp/definitions/*.yaml` — fallback for functionality without a product folder

The build pipeline discovers YAML files from both paths.

### Key fields

```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
    scopes:
      - your_product:read
    annotations:
      readOnly: true
      destructive: false
      idempotent: true
    # Optional:
    mcp_version: 1 # 2 for create/update/delete ops, 1 for read/list if available via HogQL
    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
    feature_flag: my-flag-key # gate this tool behind a PostHog feature flag
    feature_flag_behavior: enable # 'enable' (default) or 'disable'

Unknown keys are rejected at build time (Zod .strict()).

Gating tools with feature flags

Add feature_flag to any tool (standard or query wrapper) to gate its exposure on a PostHog feature flag evaluated at MCP init time for the current user.

  • feature_flag_behavior: enable (default) — tool is shown only when the flag is on. Use for rolling out new tools.
  • feature_flag_behavior: disable — tool is hidden when the flag is on. Use for sunsetting old tools.

Reusing the same flag key with both behaviors performs an atomic swap: flag on → new tool visible, old tool hidden; flag off → old tool visible, new tool hidden. Useful for A/B testing tool variations.

Flags are evaluated in parallel at init via evaluateFeatureFlags. If a flag can't be evaluated (service error, missing flag), enable-gated tools are excluded and disable-gated tools are included — fail-closed for new tools, fail-open for existing ones.

Syncing after endpoint changes

pnpm --filter=@posthog/mcp run scaffold-yaml -- --sync-all

Idempotent and non-destructive — adds new operations as enabled: false, removes stale ones.

Serializer descriptions

Descriptions flow through the entire pipeline:

Django serializer field → OpenAPI spec → Zod schema → MCP tool description

These descriptions are what agents read to understand tool parameters.

  • Use help_text on serializer fields — it becomes the OpenAPI description.
  • Use param_overrides in YAML to override generated descriptions with imperative instructions.
  • Be specific about formats, constraints, and valid values.
  • Avoid jargon that an LLM wouldn't understand without context.

HogQL system tables

Every list/get endpoint should have a corresponding HogQL system table in posthog/hogql/database/schema/system.py. This lets agents query data via SQL in v2 of the MCP.

Each system table must include a team_id column for data isolation.

Use mcp_version: 1 on read/list YAML tools when a system table covers the same data — v2 agents use SQL instead.

When adding a system table, also add a model reference file (models-<domain>.md) in products/posthog_ai/skills/querying-posthog-data/references/ and register it in products/posthog_ai/skills/querying-posthog-data/SKILL.md under Data Schema.

Two MCP versions

  • v1 (legacy): all CRUD tools exposed, for clients without skill support.
  • v2 (SQL-first): read/list tools replaced by HogQL, create/update/delete tools kept. For coding agents.

Control per-tool availability with mcp_version: 1/2 in the YAML definition.