Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Testing Mcp Tools Locally

ASecurity

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.

39,909 stars
0 votes
0 copies
2 views
Added 9/20/2026
developmentpythonrustgoshellbashdjangodockertestingdebuggingapi

Works with

cliapimcp

Security Analysis

A100/100

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add PostHog/posthog --skill testing-mcp-tools-locally --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Testing Mcp Tools Locally?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Testing Mcp Tools Locally
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/posthog-testing-mcp-tools-locally-558edc7b/badge)](https://www.skillsdirectory.com/skills/posthog-testing-mcp-tools-locally-558edc7b)

More formats (shields.io, HTML) on the badges page.

Download with Pro
Files
SKILL.md
---
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](../../../products/managed_migrations/docs/support-mcp-tools.md).

## 1. Start the dev environment

```bash
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

```bash
hogli migrations:run
```

ClickHouse migration failures are fine — batch imports only need Postgres.

## 3. Verify DB connectivity from the Django shell

```bash
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.

```python
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.

```python
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

```bash
# 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:

```bash
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:

```bash
# 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:

| key                                                         | tools visible                    |
| ----------------------------------------------------------- | -------------------------------- |
| staff user, `batch_import_support:read` + `user:read`       | both                             |
| staff user, `*` only                                        | none                             |
| staff user, `batch_import_support:read` without `user:read` | none (staff lookup fails closed) |
| non-staff user, both scopes                                 | none (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:

```bash
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.

Attribution

PostHogPostHog
View sourceMore from PostHog →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

284972 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2192 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

10311 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →