Local development setup
Walk-through to go from git clone to a running stdio MCP server with one tool (ping).
1. Prerequisites
- Node 20+.
node --version. - pnpm. Activate via corepack:
corepack enable corepack prepare pnpm@9 --activate - Docker. Used to run Postgres locally and by integration tests (testcontainers).
2. Clone + install
git clone <repo-url> whatsapp-mcp
cd whatsapp-mcp
pnpm install3. Env
cp .env.example .envFor Phase 1 you only need DATABASE_URL. The default in .env.example matches the dev docker compose:
DATABASE_URL=postgres://wa:wa@localhost:5432/wa_mcpEvery other secret (Meta, Inngest, media signing) can be left blank until the phase that needs it.
4. Postgres
docker compose -f docker-compose.dev.yml up -dWait for pg_isready (a few seconds). Then:
pnpm db:migrateThis applies drizzle/0001_init.sql and creates drizzle_migrations to track future migrations.
5. Seed the owner row
pnpm seed:ownerCopy the printed UUID into .env as LOCAL_OWNER_CLIENT_ID. (Phase 4 needs this; Phase 1 doesn’t strictly require it, but you’ll want it set before you forget.)
6. Run the MCP server
pnpm devYou should see mcp:stdio ready on stderr. The server is now reading JSON-RPC from stdin.
7. Talk to it from Claude Code
Add an MCP server entry (location varies by OS — see Claude Code docs):
{
"command": "pnpm",
"args": ["-C", "/absolute/path/to/whatsapp-mcp", "dev"],
"env": { "MCP_TRANSPORT": "stdio" }
}Restart Claude Code. The ping tool should appear; calling it should return { ok, ts, version }.
8. Run tests
pnpm test # unit
pnpm test:integration # spins up Postgres testcontainer
pnpm test:ci # full + coverageTroubleshooting
Invalid environment configuration—pnpm devfailed on env validation. The error lists each invalid key on its own line; fix.envand re-run.ECONNREFUSED 127.0.0.1:5432— Postgres isn’t up.docker compose -f docker-compose.dev.yml ps.- Migrations stall on first run — Postgres may not be ready yet.
docker compose -f docker-compose.dev.yml logs postgresto confirm. - ESLint complains about
process.env— that’s thelocal/no-process-env-outside-configrule firing. Move the read into src/config/env.ts and import the typedconfigobject.