testing-mcp-tools-locally について
PostHogのmanaged migrations MCPツールをローカルでテストする手順です。環境構築、スコープ付きスタッフ認証情報、ツールの検出、ローカルAPIへの一連の呼び出しを扱います。
主な機能
- 01ローカル開発環境の起動
- 02Postgresとバッチインポート用テストデータの準備
- 03スタッフユーザーとスコープ付きAPIキーの作成
- 04MCP経由のツール検出と呼び出しのテスト
- 05認証と接続の問題の診断
活用シーン
- 01スタッフキーでのサポートツール表示確認
- 02スコープ不足や非スタッフの場合の権限確認
- 03ローカルAPIに対するバッチインポートツールの実行
MCP 対応
Hono MCPサーバーをローカルAPIに接続し、MCP Inspectorでテストする手順です。execモードでのツール検出と呼び出し、スタッフ判定とAPIスコープのゲートを説明します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ posthog/ ai-plugin --skill testing-mcp-tools-locallyインストール後、Claude Code で/testing-mcp-tools-locallyを入力して読み込まれたか確認します。
同梱ファイルreferences/seed-data.md
詳細
管理対象のマイグレーション MCP ツールをローカルでテストする
前提条件
Docker サービスが正常に稼働している状態で、開発環境を起動しておく必要があります。 バッチインポートサポート API と MCP ツールには、以下が必要です。
- スタッフユーザー(
is_staff = True) batch_import_support:readとuser:readの両方を明示的に付与した Personal API Key- 適用済みの Postgres マイグレーション(ClickHouse は不要)
両方のスコープが必要な理由:バックエンドは batch_import_support:read のみでも受け付けますが、MCP ツールの検出では /api/users/@me/ を通じてスタッフ権限を確認します。その呼び出しをキーで実行できない場合、ツールはフェイルクローズで非表示になります。
* ワイルドカードは、どちらのスコープの代わりにもなりません。ツールの検出ゲートでは非公開スコープを明示的に要求し、バックエンドの INTERNAL スコープ処理ではワイルドカードキーが明示的に拒否されます。
本番環境でのセットアップ手順については、docs/support-mcp-tools.md を参照してください。
1. 開発環境を起動する
hogli start -d
hogli wait
hogli wait が migrate-persons-db または migrate-behavioral-cohorts で失敗した場合、これらはオプションの個別データベースなので無視してください。
migrate-postgres で失敗した場合は、Docker のポートフォワーディングを確認してください(下記のトラブルシューティングを参照)。
2. Postgres マイグレーションを実行する
hogli migrations:run
ClickHouse のマイグレーションが失敗しても問題ありません。バッチインポートで必要なのは Postgres のみです。
3. Django シェルから DB 接続を確認する
hogli dev:shell-plus -y -- -c "
from posthog.models import Team, User
print(Team.objects.first(), User.objects.first())
"
ポート 5432 で connection refused が発生して失敗する場合は、下記のトラブルシューティングを参照してください。
4. バッチインポートのテストデータをシードする
hogli dev:shell-plus を使って、さまざまな状態の BatchImport レコードを作成します。
secrets フィールドは EncryptedJSONStringField です。空の {} はシリアライズ時に null となり、NOT NULL 制約に違反します。必ず空でない 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'},
]},
)
すべてのステータスを網羅したシードスクリプトについては、references/seed-data.md を参照してください。
重要: ローカルの batch-import-worker プロセスは RUNNING レコードを処理し、そのステータスを変更する場合があります(例:設定の検証エラーによる一時停止)。
テスト中にレコードの状態を維持するには、ワーカーを停止するか、COMPLETED/FAILED/PAUSED のステータスを使用してください。
5. ユーザーをスタッフにしてテストキーを発行する
既存のキーのスコープを編集するのではなく、新しいキーを発行してください。 MCP サーバーはトークンごとにキーのスコープをキャッシュするため、スコープを編集すると古い結果が返される場合があります。
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"]))
検出ゲートの否定ケースをテストするには、次のキーも発行してください。
["*"] のキー(ツールは表示されてはなりません)、
user:read を含まない ["batch_import_support:read"] のキー(スタッフの照会はフェイルクローズとなるため、ツールは表示されてはなりません)、
およびスタッフではないユーザーに両方のスコープを付与したキー(ツールは表示されてはなりません)。
6. API を直接テストする
# 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. MCP 経由でテストする
pnpm run dev ではなく、Hono サーバーを実行してください。
wrangler worker(pnpm run dev、ポート 8787)は、MCP_HONO_URL が設定されていない場合、/mcp を本番環境の mcp.us.posthog.com にプロキシします。
そのため、ローカルのキーでは 401 Invalid API key が返されます。
Hono サーバーは、ローカル API に直接接続して MCP を提供します。
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
PAT は Bearer ヘッダーで認証し、OAuth フローは使わないでください。 非公開のスコープは OAuth には構造上存在しないため、Inspector の OAuth ログインでサインインしても、これらのツールが表示されることはありません。
Hono サーバーは exec モードで実行されます。tools/list は単一の exec ツールを返し、
実際のツールはそれを通じて検出・呼び出しされます。
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 {}"
想定される検出結果:
| key | 表示されるツール |
|---|---|
スタッフユーザー、batch_import_support:read + user:read | 両方 |
スタッフユーザー、* のみ | なし |
スタッフユーザー、user:read なしの batch_import_support:read | なし(スタッフ確認はフェイルクローズ) |
| 非スタッフユーザー、両方のスコープ | なし(直接 API 呼び出しも 403) |
インタラクティブな Inspector UI(http://localhost:6274)も動作します —
OAuth ログインを使わず、接続設定で PAT を Bearer トークンとして貼り付けてください。
トラブルシューティング
localhost:8787 から 401「Invalid API key」が返される
wrangler worker に接続しています。この worker は /mcp を本番環境にプロキシするため、ローカルのキーはそこで無効です。
ポート 3001 の Hono サーバーを使う(手順 7 を参照)か、.dev.vars に MCP_HONO_URL=http://localhost:3001 を設定してください。
表示されるはずのキーでツールが表示されない
次の順に確認してください。
- キーに
batch_import_support:readが明示的に含まれていること —*は非公開スコープに一致しません。 - キーに
user:read(または*)も含まれていること — 検出時のスタッフ確認では/api/users/@me/を読み取り、フェイルクローズします。 - キーのユーザーが
is_staff = Trueであること。 - キーが最初からこれらのスコープを指定して発行されていること — MCP サーバーはトークンごとにスコープをキャッシュするため、既存のキーを編集するのではなく、新しいキーを発行してください。
ホストからポート 5432 に接続できない
posthog-db-1 Docker コンテナのポートマッピングが古くなっている可能性があります
(現在のポートバインディング設定なしで数日前に作成されたコンテナなど)。
次のコマンドで強制的に再作成してください。
docker compose -f docker-compose.dev.yml -f docker-compose.profiles.yml \
up -d --force-recreate db
確認方法:nc -z 127.0.0.1 5432 が成功するはずです。
secrets={} によって NOT NULL 制約違反が発生する
EncryptedJSONStringField は値を暗号化します — 空の辞書はシリアライズ時に null になります。
必ず空でない辞書を渡してください:secrets={'placeholder': 'true'}。
バッチインポートワーカーがシード済みレコードを変更する
ローカルの batch-import-worker プロセスは RUNNING レコードを自動的に取得します。
設定の検証エラー(例:skip_blanks がない場合)が発生すると、status_message に詳細な Rust バックトレースを記録してインポートを一時停止します。
これを防ぐには、ワーカーを停止するか、RUNNING 以外のステータスでシードしてください。
ゲートの全体像
リクエストは、独立した 2 つのレイヤーを通過します。
- MCP 検出(表示制御):OAuth で非表示のスコープを必要とするツールが表示されるのは、キーにそのスコープが明示的に含まれ、かつ
/api/users/@me/でis_staffが確認された場合のみです — それ以外の場合は非表示となり、フェイルクローズします(services/mcp/src/lib/staff-only-tools.ts)。 - Django による強制(セキュリティ境界):
scope_object = "INTERNAL"およびbatch_import_support:readを条件とするIsAuthenticated+IsStaffUser+APIScopePermission。セッションではスタッフであることだけが必要です。PAT ではスタッフであることに加えて明示的なスコープが必要です。*のみのキーは常に 403 になります。