Skip to Content

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 init in d:/Git/whatsapp-mcp/.
  • Create a private GitHub repository (<org-or-user>/whatsapp-mcp) and git 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.json with pnpm as 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.json with strict: true, ESM ("module": "NodeNext"), paths for @/*.
  • .eslintrc.cjs with @typescript-eslint + a custom rule banning process.env access outside src/config/.
  • .prettierrc, .editorconfig.
  • lefthook.yml pre-commit / pre-push.
  • .gitignore (node_modules, dist, .env, coverage, etc.).
  • .env.example with 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 lefthook

MCP server skeleton

  • src/index.ts — entry point; selects transport from MCP_TRANSPORT.
  • src/server/mcp.ts — server factory; registers tools from the declarative registry.
  • src/transport/stdio.tsStdioServerTransport setup.
  • A ping tool 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 against DATABASE_URL.
  • src/db/schema/index.ts re-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 read process.env. Exports a typed config object.
  • src/config/logger.tspino with redaction list: authorization, *token*, *secret*, password, x-hub-signature-256.

Seeding

  • scripts/seed-owner.ts — inserts the local-owner client with is_owner = true and prints the generated UUID. Operator copies that UUID into LOCAL_OWNER_CLIENT_ID in .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.ts with 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 by pnpm docs:reference. Gitignored; never committed, never hand-edited (a dedicated docs project consumes the TSDoc separately).
  • docs/api/mcp-tools.md — tool reference, currently ping only.

CI (GitHub Actions)

  • .github/workflows/ci.yml — on every PR and push to main:
    • 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 main only, pushed to ghcr.io.
  • .github/dependabot.yml for weekly dependency PRs.

Critical files

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 matches package.json.
  • tests/unit/tools/registry.test.ts — registry catches duplicate tool names and missing fields.
  • tests/integration/db/migrate.test.ts — testcontainers Postgres: db:migrate applies 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/**/*.ts file (per standards.md §2).
  • TSDoc on every exported symbol in src/config/, src/db/, src/tools/, src/server/, src/transport/.
  • docs/architecture/database.md covers the four tenancy tables with rationale.
  • docs/components/mcp-server.md covers the skeleton: transport selection, registry pattern, ping example.
  • docs/api/mcp-tools.md lists ping.
  • docs/reference/ regenerates cleanly via pnpm docs:reference (output is gitignored, not gated in CI).

Acceptance

  1. pnpm install && pnpm db:migrate && pnpm seed:owner && pnpm dev boots the server on stdio.
  2. Pointing Claude Code at the local server (stdio config) lists tools and successfully calls ping.
  3. pnpm test:ci is green; coverage gate passes.
  4. pnpm lint and pnpm typecheck are clean.
  5. pnpm docs:reference regenerates without errors (output is gitignored).
  6. Attempting to read process.env.FOO outside src/config/ produces an ESLint error.
  7. docs/architecture/database.md, docs/components/mcp-server.md, docs/testing/test-strategy.md exist and are non-empty.

Definition of Done

Repo + CI bootstrap

  • git init done; first commit pushed.
  • Private GitHub repo created; origin set; main pushed.
  • wamcp_ prefix registered with GitHub Secret Scanning (custom pattern). (outstanding)
  • Branch protection on main configured.
  • .github/workflows/ci.yml runs lint + typecheck + test:ci + coverage gate. (Docs regen check intentionally removed — docs/reference/ is gitignored per project policy.)
  • .github/dependabot.yml configured (weekly).
  • First CI run green on main.

Project setup

  • package.json with 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.json strict + ESM + paths.
  • eslint.config.mjs (flat config) with custom rule local/no-process-env-outside-config banning process.env outside src/config/.
  • .prettierrc, .editorconfig, lefthook.yml, .gitignore, .env.example committed.

MCP server skeleton

  • src/index.ts selects transport by MCP_TRANSPORT.
  • src/server/mcp.ts builds the server from the tool registry.
  • src/transport/stdio.ts works end-to-end.
  • src/tools/_registry.ts declarative pattern in place.
  • src/tools/ping.ts returns { ok, ts, version }.

Database

  • Drizzle config + migration runner.
  • drizzle/0001_init.sql with full tenancy tables (clients, api_keys, phone_numbers, client_phone_grants).
  • src/db/scoped.ts placeholder exists with TODO for Phase 4.
  • scripts/seed-owner.ts seeds the owner row.

Config + logging

  • src/config/env.ts zod-validates env (the only process.env consumer).
  • src/config/logger.ts pino with redaction list.

Dev infra

  • docker-compose.dev.yml runs Postgres 16.
  • docs/dev/local-setup.md written.

Testing scaffold

  • Vitest config with unit + integration projects.
  • Testcontainers Postgres helper.
  • tests/unit/config/env.test.ts passes.
  • tests/unit/tools/ping.test.ts passes.
  • tests/unit/tools/registry.test.ts passes.
  • tests/integration/db/migrate.test.ts passes.
  • tests/integration/db/seed-owner.test.ts passes.
  • Coverage gate ≥ 80% green.

Documentation

  • docs/architecture/database.md covering tenancy tables.
  • docs/components/mcp-server.md covering skeleton.
  • docs/testing/test-strategy.md, docs/testing/running-tests-locally.md.
  • docs/reference/ regenerates cleanly via pnpm docs:reference (output is gitignored per project policy; not committed, not hand-edited).
  • docs/api/mcp-tools.md lists ping.
  • 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 dev boots stdio MCP.
  • Claude Code lists tools and calls ping successfully. (outstanding — manual end-to-end check pending)
  • pnpm test:ci green; coverage met.
  • pnpm lint and pnpm typecheck clean.
  • pnpm docs:reference regenerates without errors (output gitignored).
  • process.env.FOO outside src/config/ produces ESLint error (rule local/no-process-env-outside-config).

Phase signoff

  • Phase 1 complete. README.md status table updated to ✅. (blocked on the two outstanding items above)