New to Claude Skills? Learn how to install them →

Tposthog on GitHub

Testing MCP Tools Locally

Free

Set up and test managed migrations MCP tools with ease.

by posthog37.6k stars on posthog/posthog
1 views
Updated Aug 11, 2026
Get this skill

Free · Opens the source repo

What Testing MCP Tools Locally does

The Testing MCP Tools Locally skill provides a structured approach for developers to set up their local development environment for testing managed migrations tools. This skill is specifically designed for those working with the PostHog platform, allowing users to seed data, configure API keys, and execute tests on staff-only managed migrations tools. By following the outlined steps, users can ensure that their environment is correctly configured to support batch import tooling and verify API responses before deployment.

To get started, users must ensure that their Docker services are running and healthy. The skill outlines the prerequisites, including the need for a staff user with specific API key scopes. This is crucial, as the backend system verifies staff access when discovering tools, preventing unauthorized access. The skill provides detailed commands for starting the development environment, running migrations, and verifying database connectivity, ensuring that users can quickly diagnose and resolve any issues that may arise during setup.

Moreover, the skill includes instructions for seeding test data, which is essential for simulating real-world scenarios when testing the batch import functionality. Users can create BatchImport records with various configurations, allowing them to test different states and ensure that the batch import worker processes records correctly. The skill also emphasizes the importance of minting fresh API keys and testing the API directly, providing users with the tools needed to validate their configurations and troubleshoot any potential issues.

Overall, this skill is an invaluable resource for developers working with PostHog's managed migrations tools, providing a comprehensive guide to setting up a local testing environment and ensuring that all necessary configurations are in place for successful testing and deployment.

When to use it

Use this skill when you need to configure your local environment for testing PostHog's managed migrations tools and verify API responses before deployment.

When not to use it

This skill is not suitable for users who do not have access to the PostHog platform or those who do not need to work with managed migrations tools.

What you can build with it

Setting Up Local Development

Use this skill to configure your local environment for testing PostHog's managed migrations tools.

Seeding Test Data

Quickly seed batch import test data to simulate various scenarios during testing.

Validating API Responses

Test the API directly to ensure that your configurations are correct before deployment.

How to install Testing MCP Tools Locally

View source

1. Install with the skills CLI

npx skills add posthog/posthog/testing-mcp-tools-locally --agent claude-code

2. Or install it manually

Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.

Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs

Inside SKILL.md

Written by posthog

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.

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>/ | jq

7. 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 \
  --header "Authorization: Bearer <token>" \
  --method tools/call --tool-name exec --tool-arg "command=call managed-migrations-support-list {}"

Expected discovery matrix:

keytools visible
staff user, batch_import_support:read + user:readboth
staff user, * onlynone
staff user, batch_import_support:read without user:readnone (staff lookup fails closed)
non-staff user, both scopesnone (and direct API calls 403)

The interactive Inspector UI (http://localhost:6274) also works — paste the PAT as the Bearer token in connection settings instead of using its OAuth login.

Troubleshooting

401 "Invalid API key" from localhost:8787

You're talking to the wrangler worker, which proxies /mcp to production — your local key is invalid there. Use the Hono server on port 3001 (see step 7), or set MCP_HONO_URL=http://localhost:3001 in .dev.vars.

Tools don't appear for a key that should see them

Check, in order:

  1. The key carries batch_import_support:read explicitly* does not match hidden scopes.
  2. The key also carries user:read (or *) — the discovery staff check reads /api/users/@me/ and fails closed.
  3. The key's user has is_staff = True.
  4. The key was minted with those scopes from the start — the MCP server caches scopes per token, so mint a fresh key instead of editing an existing one.

Port 5432 not reachable from host

The posthog-db-1 Docker container may have stale port mappings (container created days ago without the current port binding config). Fix by force-recreating:

docker compose -f docker-compose.dev.yml -f docker-compose.profiles.yml \
  up -d --force-recreate db

Verify: nc -z 127.0.0.1 5432 should succeed.

secrets={} causes NOT NULL violation

EncryptedJSONStringField encrypts the value — an empty dict serializes to null. Always pass a non-empty dict: secrets={'placeholder': 'true'}.

Batch import worker modifies seeded records

The local batch-import-worker process automatically claims RUNNING records. If it encounters a config validation error (e.g. missing skip_blanks), it will pause the import with a detailed Rust backtrace in status_message. Stop the worker or seed with non-RUNNING statuses to prevent this.

The gates, end to end

A request passes through two independent layers:

  1. MCP discovery (presentation): a tool requiring an OAuth-hidden scope surfaces only when the key explicitly carries the scope AND /api/users/@me/ confirms is_staff — otherwise it is hidden, fail-closed (services/mcp/src/lib/staff-only-tools.ts).
  2. Django enforcement (the security boundary): IsAuthenticated + IsStaffUser + APIScopePermission with scope_object = "INTERNAL" and batch_import_support:read. Sessions need staffness only; PATs need staffness plus the explicit scope; *-only keys always 403.

Frequently asked questions about Testing MCP Tools Locally

Similar skills