Phase 1 — Core MCP Server Skeleton (stdio)
Effort: M
Goal
A buildable TypeScript MCP server that exposes a ping tool over stdio, talks to a local Postgres via Drizzle, validates its env on boot, and ships with the full testing + docs scaffolding the rest of the project depends on.
Deliverables
Repo bootstrap (literally the first thing)
git initind:/Git/whatsapp-mcp/.- Create a private GitHub repository (
<org-or-user>/whatsapp-mcp) andgit remote add origin. - Push
main. - Register the
wamcp_secret prefix with GitHub Secret Scanning (custom pattern) so leaked keys get flagged. - Branch protection on
main: require PR, require CI green, require 1 review (later — for v1 the owner is the only reviewer; document the rule but allow self-merge until external clients exist).
Project setup
package.jsonwithpnpmas the package manager, scripts:dev,build,start,db:migrate,db:generate,seed:owner,test,test:integration,test:ci,test:watch,typecheck,lint,format,docs:reference.tsconfig.jsonwithstrict: true, ESM ("module": "NodeNext"),pathsfor@/*..eslintrc.cjswith@typescript-eslint+ a custom rule banningprocess.envaccess outsidesrc/config/..prettierrc,.editorconfig.lefthook.ymlpre-commit / pre-push..gitignore(node_modules, dist, .env, coverage, etc.)..env.examplewith every canonical env var (see architecture.md §9).
Dependencies
runtime:
@modelcontextprotocol/sdk
drizzle-orm, drizzle-kit
postgres (driver)
pino
zod
dotenv
uuidv7
dev:
typescript, @types/node
vitest, @vitest/coverage-v8
testcontainers
msw
eslint, @typescript-eslint/*, prettier
typedoc, typedoc-plugin-markdown
lefthookMCP server skeleton
src/index.ts— entry point; selects transport fromMCP_TRANSPORT.src/server/mcp.ts— server factory; registers tools from the declarative registry.src/transport/stdio.ts—StdioServerTransportsetup.- A
pingtool that returns{ ok: true, ts, version }.
Tool registry (set up the pattern early)
src/tools/_registry.ts— declarative tool list. Each tool exports{ name, scope, inputSchema, handler }. This pattern pays off in Phase 4 (scope check) and audit logging.src/tools/ping.ts— the first tool, using the pattern.
Database (Drizzle)
drizzle.config.ts.src/db/client.ts— Drizzle instance againstDATABASE_URL.src/db/schema/index.tsre-exporting per-table modules.src/db/schema/clients.ts,api-keys.ts,phone-numbers.ts,client-phone-grants.ts— full columns per architecture.md §1. Done up-front so Phase 4 migrations are additive (rate_limit_buckets, audit_log, messages, contacts, media_objects come in later phases).src/db/migrate.ts— migration runner CLI.drizzle/0001_init.sql— initial migration.src/db/scoped.ts— placeholder for the tenant-scoped query module (filled in Phase 4 but the file exists with a TODO so the ESLint rule has somewhere to point).
Config and logging
src/config/env.ts— zod-validated env loader. The only module allowed to readprocess.env. Exports a typedconfigobject.src/config/logger.ts—pinowith redaction list:authorization,*token*,*secret*,password,x-hub-signature-256.
Seeding
scripts/seed-owner.ts— inserts thelocal-ownerclient withis_owner = trueand prints the generated UUID. Operator copies that UUID intoLOCAL_OWNER_CLIENT_IDin.env.
Dev infra
docker-compose.dev.yml— Postgres 16 only (the app runs on the host in dev for fast iteration).docs/dev/local-setup.md— clone, install, env, migrate, seed, run.
Testing scaffold (the standard)
vitest.config.tswith unit + integration projects.tests/integration/_setup.ts— testcontainers Postgres helper.tests/unit/config/env.test.ts— env validation passes/fails cases.tests/unit/tools/ping.test.ts— handler returns expected shape.tests/integration/db/migrate.test.ts— migrations apply on a fresh container.- Coverage gate config wired in.
Docs scaffold
docs/architecture/database.md— first hand-written architecture doc, covering just the tenancy tables (extended in later phases).docs/components/mcp-server.md— first component doc, describing the stdio transport, tool registry, and ping tool.docs/testing/test-strategy.md— copied from standards.md §1, expanded.docs/testing/running-tests-locally.md.docs/reference/— generated on demand bypnpm docs:reference. Gitignored; never committed, never hand-edited (a dedicated docs project consumes the TSDoc separately).docs/api/mcp-tools.md— tool reference, currentlypingonly.
CI (GitHub Actions)
.github/workflows/ci.yml— on every PR and push tomain:- Setup Node 20 + pnpm.
pnpm install --frozen-lockfile.pnpm lint && pnpm typecheck.pnpm test:ci(unit + integration; testcontainers spins up Postgres on the runner — uses Docker-in-Docker, which GitHub-hosted runners support).- Coverage gate: fail if line coverage drops below the per-folder thresholds in standards.md.
- Image build (Phase 7 onwards) on
mainonly, pushed toghcr.io.
.github/dependabot.ymlfor weekly dependency PRs.
Critical files
- src/index.ts — transport selector
- src/server/mcp.ts — server factory + tool registry mounting
- src/transport/stdio.ts
- src/tools/_registry.ts — declarative pattern
- src/tools/ping.ts
- src/config/env.ts — the only
process.envconsumer - src/config/logger.ts
- src/db/client.ts
- src/db/schema/*.ts — tenancy tables in full
- src/db/migrate.ts
- drizzle/0001_init.sql
- scripts/seed-owner.ts
- docker-compose.dev.yml
Tests
tests/unit/config/env.test.ts— env validation: missing required vars throw; valid set parses; bad URL formats rejected.tests/unit/tools/ping.test.ts— handler returns{ ok, ts, version }; version matchespackage.json.tests/unit/tools/registry.test.ts— registry catches duplicate tool names and missing fields.tests/integration/db/migrate.test.ts— testcontainers Postgres:db:migrateapplies cleanly, idempotent on re-run.tests/integration/db/seed-owner.test.ts— seeds the owner row, enforces single-owner partial unique constraint.- Coverage gate green: ≥ 80 % overall on
src/.
Code documentation
- File-level header on every
src/**/*.tsfile (per standards.md §2). - TSDoc on every exported symbol in
src/config/,src/db/,src/tools/,src/server/,src/transport/. docs/architecture/database.mdcovers the four tenancy tables with rationale.docs/components/mcp-server.mdcovers the skeleton: transport selection, registry pattern, ping example.docs/api/mcp-tools.mdlistsping.docs/reference/regenerates cleanly viapnpm docs:reference(output is gitignored, not gated in CI).
Acceptance
pnpm install && pnpm db:migrate && pnpm seed:owner && pnpm devboots the server on stdio.- Pointing Claude Code at the local server (stdio config) lists tools and successfully calls
ping. pnpm test:ciis green; coverage gate passes.pnpm lintandpnpm typecheckare clean.pnpm docs:referenceregenerates without errors (output is gitignored).- Attempting to read
process.env.FOOoutsidesrc/config/produces an ESLint error. docs/architecture/database.md,docs/components/mcp-server.md,docs/testing/test-strategy.mdexist and are non-empty.
Definition of Done
Repo + CI bootstrap
-
git initdone; first commit pushed. - Private GitHub repo created;
originset;mainpushed. -
wamcp_prefix registered with GitHub Secret Scanning (custom pattern). (outstanding) - Branch protection on
mainconfigured. -
.github/workflows/ci.ymlruns lint + typecheck +test:ci+ coverage gate. (Docs regen check intentionally removed —docs/reference/is gitignored per project policy.) -
.github/dependabot.ymlconfigured (weekly). - First CI run green on
main.
Project setup
-
package.jsonwith all required scripts (dev,build,start,db:migrate,db:generate,seed:owner,test,test:integration,test:ci,test:watch,typecheck,lint,format,docs:reference). -
tsconfig.jsonstrict + ESM + paths. -
eslint.config.mjs(flat config) with custom rulelocal/no-process-env-outside-configbanningprocess.envoutsidesrc/config/. -
.prettierrc,.editorconfig,lefthook.yml,.gitignore,.env.examplecommitted.
MCP server skeleton
-
src/index.tsselects transport byMCP_TRANSPORT. -
src/server/mcp.tsbuilds the server from the tool registry. -
src/transport/stdio.tsworks end-to-end. -
src/tools/_registry.tsdeclarative pattern in place. -
src/tools/ping.tsreturns{ ok, ts, version }.
Database
- Drizzle config + migration runner.
-
drizzle/0001_init.sqlwith full tenancy tables (clients,api_keys,phone_numbers,client_phone_grants). -
src/db/scoped.tsplaceholder exists with TODO for Phase 4. -
scripts/seed-owner.tsseeds the owner row.
Config + logging
-
src/config/env.tszod-validates env (the onlyprocess.envconsumer). -
src/config/logger.tspino with redaction list.
Dev infra
-
docker-compose.dev.ymlruns Postgres 16. -
docs/dev/local-setup.mdwritten.
Testing scaffold
- Vitest config with unit + integration projects.
- Testcontainers Postgres helper.
-
tests/unit/config/env.test.tspasses. -
tests/unit/tools/ping.test.tspasses. -
tests/unit/tools/registry.test.tspasses. -
tests/integration/db/migrate.test.tspasses. -
tests/integration/db/seed-owner.test.tspasses. - Coverage gate ≥ 80% green.
Documentation
-
docs/architecture/database.mdcovering tenancy tables. -
docs/components/mcp-server.mdcovering skeleton. -
docs/testing/test-strategy.md,docs/testing/running-tests-locally.md. -
docs/reference/regenerates cleanly viapnpm docs:reference(output is gitignored per project policy; not committed, not hand-edited). -
docs/api/mcp-tools.mdlistsping. - File-level header on every
src/**/*.ts. - TSDoc on every exported symbol in
src/config/,src/db/,src/tools/,src/server/,src/transport/.
Acceptance verified
-
pnpm install && pnpm db:migrate && pnpm seed:owner && pnpm devboots stdio MCP. - Claude Code lists tools and calls
pingsuccessfully. (outstanding — manual end-to-end check pending) -
pnpm test:cigreen; coverage met. -
pnpm lintandpnpm typecheckclean. -
pnpm docs:referenceregenerates without errors (output gitignored). -
process.env.FOOoutsidesrc/config/produces ESLint error (rulelocal/no-process-env-outside-config).
Phase signoff
- Phase 1 complete. README.md status table updated to ✅. (blocked on the two outstanding items above)