/testing-mcp-tools-locally
Set up the local dev environment, seed data, and API keys to test the staff-only managed migrations MCP tools (managed-migrations-support-list, managed-migrations-support-get) end to end. Use when testing batch import support tooling, debugging MCP tool responses or discovery
$ npx -y skills add posthog/posthog --skill testing-mcp-tools-locally --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
- Slash command
/testing-mcp-tools-locally
Context preview
The summary Claude sees to decide when to auto-load this skill.
Set up the local dev environment, seed data, and API keys to test the staff-only managed migrations MCP tools (managed-migrations-support-list, managed-migrations-support-get) end to end. Use when testing batch import support tooling, debugging MCP tool responses or discovery
SKILL.md
testing-mcp-tools-locally.SKILL.mdname: testing-mcp-tools-locally
description: >
Set up the local dev environment, seed data, and API keys to test the staff-only managed migrations
MCP tools (managed-migrations-support-list, managed-migrations-support-get) end to end.
Use when testing batch import support tooling, debugging MCP tool responses or discovery
(tools not appearing), or verifying the support API before deploying.
Covers the discovery gate: hidden scope, is_staff, user:read, and why wildcard keys and OAuth never work.
Testing managed migrations MCP tools locally
Prerequisites
The dev environment must be running with Docker services healthy. The batch import support API and MCP tools require:
- A staff user (`is_staff = True`)
- A Personal API Key carrying **both** `batch_import_support:read` and `user:read`, explicitly
- Postgres migrations applied (ClickHouse not required)
Why both scopes: the backend accepts `batch_import_support:read` alone, but MCP tool discovery verifies staffness via `/api/users/@me/` and hides the tools (fail-closed) when the key cannot make that call. A `*` wildcard does **not** substitute for either — the discovery gate requires the hidden scope explicitly, and the backend's `INTERNAL` scope handling rejects wildcard keys outright. For the production setup flow, see [docs/support-mcp-tools.md](../../docs/support-mcp-tools.md).
1. Start the dev environment
hogli start -d
hogli wait
If `hogli wait` fails on `migrate-persons-db` or `migrate-behavioral-cohorts`, those are optional separate databases — ignore them. If it fails on `migrate-postgres`, check Docker port forwarding (see troubleshooting below).
2. Run Postgres migrations
hogli migrations:run
ClickHouse migration failures are fine — batch imports only need Postgres.
3. Verify DB connectivity from the Django shell
hogli dev:shell-plus -y -- -c "
from posthog.models import Team, User
print(Team.objects.first(), User.objects.first())
"
If this fails with `connection refused` on port 5432, see troubleshooting below.
4. Seed batch import test data
Use `hogli dev:shell-plus` to create `BatchImport` records in various states. The `secrets` field is an `EncryptedJSONStringField` — empty `{}` serializes to null and violates the NOT NULL constraint; always pass a non-empty dict.
from products.managed_migrations.backend.models.batch_imports import BatchImport
BatchImport.objects.create(
team=team,
created_by_id=user.id,
status=BatchImport.Status.PAUSED,
import_config={
'source': {'type': 's3', 'bucket': 'test', 'region': 'us-east-1', 'prefix': 'data/'},
'data_format': {'type': 'json_lines', 'skip_blanks': True, 'content': {'type': 'mixpanel'}},
'sink': {'type': 'capture'},
},
secrets={'access_key': 'test', 'secret_key': 'test'},
state={'parts': [
{'key': 'part-1', 'current_offset': 50000, 'total_size': 50000},
{'key': 'part-2', 'current_offset': 10000, 'total_size': 50000},
{'key': 'part-3'},
]},
)See `references/seed-data.md` for a full seeding script covering all statuses.
**Important:** the local `batch-import-worker` process will pick up `RUNNING` records and may modify their status (e.g. pausing them due to config validation errors). To keep records stable for testing, either stop the worker or use `COMPLETED`/`FAILED`/`PAUSED` statuses.
5. Make your user staff and mint test keys
Mint **fresh** keys rather than editing scopes on an existing one — the MCP server caches a key's scopes per token, so edited scopes can serve stale results.
from posthog.models import User
from posthog.models.personal_api_key import PersonalAPIKey
from posthog.models.utils import generate_random_token_personal, hash_key_value
me = User.objects.first()
me.is_staff = True; me.save()
def mint(user, scopes):
token = generate_random_token_personal()
PersonalAPIKey.objects.create(user=user, label=str(scopes)[:40], secure_value=hash_key_value(token), scopes=scopes)
return token
print(mint(me, ["batch_import_support:read", "user:read"]))To test the negative cases of the discovery gate, also mint: a `["*"]` key (tools must NOT appear), a `["batch_import_support:read"]` key without `user:read` (tools must NOT appear — staff lookup fails closed), and the full pair on a non-staff user (tools must NOT appear).
6. Test the API directly
# List all batch imports
curl -H "Authorization: Bearer <token>" \
http://localhost:8010/api/managed_migrations_support/ | jq
# Get detail for a specific import
curl -H "Authorization: Bearer <token>" \
http://localhost:8010/api/managed_migrations_support/<uuid>/ | jq7. Test via MCP
**Run the Hono server, not `pnpm run dev`.** The wrangler worker (`pnpm run dev`, port 8787) proxies `/mcp` to **production** `mcp.us.posthog.com` unless `MCP_HONO_URL` is set, so local keys get `401 Invalid API key`. The Hono server serves MCP directly against the local API:
cd services/mcp
cp .dev.vars.example .dev.vars # POSTHOG_API_BASE_URL=http://localhost:8010
pnpm run dev:hono # serves http://localhost:3001/mcp
**Authenticate with the PAT as a Bearer header, never the OAuth flow.** The hidden scope is structurally absent from OAuth — signing in through the inspector's OAuth login can never surface these tools.
The Hono server runs exec mode: `tools/list` returns a single `exec` tool, and real tools are discovered and invoked through it. Test with the MCP Inspector CLI:
# Discovery — should list both support tools for the staff key, none for the others
npx @modelcontextprotocol/inspector --cli http://localhost:3001/mcp \
--header "Authorization: Bearer <token>" \
--method tools/call --tool-name exec --tool-arg "command=search managed-migrations-support"
# Invocation — end-to-end through Django
npx @modelcontextprotocol/inspector --cli http://localhost:3001/mcp
Read more
name: testing-mcp-tools-locally description: > Set up the local dev environment, seed data, and API keys to test the staff-only managed migrations MCP tools (managed-migrations-support-list, managed-migrations-support-get) end to end. Use when testing batch import support tooling, debugging MCP tool responses or discovery (tools not appearing), or verifying the support API before deploying. Covers the discovery gate: hidden scope, is_staff, user:read, and why wildcard keys and OAuth never work.
Testing managed migrations MCP tools locally
Prerequisites
The dev environment must be running with Docker services healthy. The batch import support API and MCP tools require:
- A staff user (`is_staff = True`)
- A Personal API Key carrying **both** `batch_import_support:read` and `user:read`, explicitly
- Postgres migrations applied (ClickHouse not required)
Why both scopes: the backend accepts `batch_import_support:read` alone, but MCP tool discovery verifies staffness via `/api/users/@me/` and hides the tools (fail-closed) when the key cannot make that call. A `*` wildcard does **not** substitute for either — the discovery gate requires the hidden scope explicitly, and the backend's `INTERNAL` scope handling rejects wildcard keys outright. For the production setup flow, see [docs/support-mcp-tools.md](../../docs/support-mcp-tools.md).
1. Start the dev environment
hogli start -d hogli wait
If `hogli wait` fails on `migrate-persons-db` or `migrate-behavioral-cohorts`, those are optional separate databases — ignore them. If it fails on `migrate-postgres`, check Docker port forwarding (see troubleshooting below).
2. Run Postgres migrations
hogli migrations:run
ClickHouse migration failures are fine — batch imports only need Postgres.
3. Verify DB connectivity from the Django shell
hogli dev:shell-plus -y -- -c " from posthog.models import Team, User print(Team.objects.first(), User.objects.first()) "
If this fails with `connection refused` on port 5432, see troubleshooting below.
4. Seed batch import test data
Use `hogli dev:shell-plus` to create `BatchImport` records in various states. The `secrets` field is an `EncryptedJSONStringField` — empty `{}` serializes to null and violates the NOT NULL constraint; always pass a non-empty dict.
from products.managed_migrations.backend.models.batch_imports import BatchImport
BatchImport.objects.create(
team=team,
created_by_id=user.id,
status=BatchImport.Status.PAUSED,
import_config={
'source': {'type': 's3', 'bucket': 'test', 'region': 'us-east-1', 'prefix': 'data/'},
'data_format': {'type': 'json_lines', 'skip_blanks': True, 'content': {'type': 'mixpanel'}},
'sink': {'type': 'capture'},
},
secrets={'access_key': 'test', 'secret_key': 'test'},
state={'parts': [
{'key': 'part-1', 'current_offset': 50000, 'total_size': 50000},
{'key': 'part-2', 'current_offset': 10000, 'total_size': 50000},
{'key': 'part-3'},
]},
)See `references/seed-data.md` for a full seeding script covering all statuses.
**Important:** the local `batch-import-worker` process will pick up `RUNNING` records and may modify their status (e.g. pausing them due to config validation errors). To keep records stable for testing, either stop the worker or use `COMPLETED`/`FAILED`/`PAUSED` statuses.
5. Make your user staff and mint test keys
Mint **fresh** keys rather than editing scopes on an existing one — the MCP server caches a key's scopes per token, so edited scopes can serve stale results.
from posthog.models import User
from posthog.models.personal_api_key import PersonalAPIKey
from posthog.models.utils import generate_random_token_personal, hash_key_value
me = User.objects.first()
me.is_staff = True; me.save()
def mint(user, scopes):
token = generate_random_token_personal()
PersonalAPIKey.objects.create(user=user, label=str(scopes)[:40], secure_value=hash_key_value(token), scopes=scopes)
return token
print(mint(me, ["batch_import_support:read", "user:read"]))To test the negative cases of the discovery gate, also mint: a `["*"]` key (tools must NOT appear), a `["batch_import_support:read"]` key without `user:read` (tools must NOT appear — staff lookup fails closed), and the full pair on a non-staff user (tools must NOT appear).
6. Test the API directly
# List all batch imports
curl -H "Authorization: Bearer <token>" \
http://localhost:8010/api/managed_migrations_support/ | jq
# Get detail for a specific import
curl -H "Authorization: Bearer <token>" \
http://localhost:8010/api/managed_migrations_support/<uuid>/ | jq7. Test via MCP
**Run the Hono server, not `pnpm run dev`.** The wrangler worker (`pnpm run dev`, port 8787) proxies `/mcp` to **production** `mcp.us.posthog.com` unless `MCP_HONO_URL` is set, so local keys get `401 Invalid API key`. The Hono server serves MCP directly against the local API:
cd services/mcp cp .dev.vars.example .dev.vars # POSTHOG_API_BASE_URL=http://localhost:8010 pnpm run dev:hono # serves http://localhost:3001/mcp
**Authenticate with the PAT as a Bearer header, never the OAuth flow.** The hidden scope is structurally absent from OAuth — signing in through the inspector's OAuth login can never surface these tools.
The Hono server runs exec mode: `tools/list` returns a single `exec` tool, and real tools are discovered and invoked through it. Test with the MCP Inspector CLI:
# Discovery — should list both support tools for the staff key, none for the others npx @modelcontextprotocol/inspector --cli http://localhost:3001/mcp \ --header "Authorization: Bearer <token>" \ --method tools/call --tool-name exec --tool-arg "command=search managed-migrations-support" # Invocation — end-to-end through Django npx @modelcontextprotocol/inspector --cli http://localhost:3001/mcp
:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.
Repo: posthog/posthog
Other skills on posthog.
- /analyzing-expensive-users
Analyze the most expensive users in AI observability and explain why they cost so much. Use when the user asks about top spenders, expensive users, per-user LLM cost, user-level cost drivers, or patterns behind high AI observability spend.
Open skill - /creating-online-evaluations
Author continuously-running online evaluations in PostHog AI observability, grounded in real failure modes you've identified. Use when the user wants evaluations that automatically score new generations or whole traces going forward — "create an eval to catch X", "continuously
Open skill - /exploring-ai-failures
Find where an AI/LLM application is failing in production and surface the failure patterns, working from real traces. Use when someone wants to understand what's going wrong with an AI feature, find and categorize failure modes, triage errors, or investigate quality issues
Open skill - /exploring-llm-clusters
Investigate AI observability clusters — understand usage patterns in AI/LLM traffic, compare cluster behavior, compute cost/latency metrics, and drill into individual traces within clusters.
Open skill - /exploring-llm-costs
Investigate LLM spend in PostHog — total cost over time, cost by model, provider, user, trace, or custom dimension, token and cache-hit economics, and cost regressions. Use when the user asks "how much are we spending on LLMs?", "which model / user / feature is most expensive?",
Open skill - /exploring-llm-evaluations
Investigate AI observability evaluations — `hog` (deterministic code-based), `llm_judge` (LLM-prompt-based), and `sentiment` (user-message sentiment). Find existing evaluations, inspect their configuration, run them against specific generations, query individual results, and
Open skill

