Skip to Content
Telegram MCPPlan & Phases1 — Minimal Telegram Bridge

Phase 1 — Minimal Telegram MCP Bridge

Goal

Telegram message arrives → server logs it, sends a hardcoded reply. No MCP, no DB beyond the user table stub, no auth beyond hardcoded admin. The goal of this phase is to prove the HTTP + Telegram + Postgres + systemd pipeline end-to-end before adding any real protocol surface.

Status (2026-05-14)

All scope and tests are landed. Definition of Done items 1–4 are met in code; final manual verification of items 1–3 (live /start round-trip) is pending the next VPS deploy.

Scope / Deliverables

Scaffolding:

  • npm init, TypeScript + strict mode, tsconfig, Vitest, ESLint, Prettier
  • Fastify HTTP server bound to 127.0.0.1:PORT
  • config.ts — Zod-parsed env loader; fail-fast on missing required vars
  • pino logger with requestId plugin
  • GET /healthz returning { ok: true }

Telegram side:

Persistence (minimal):

Ops:

Design adjustments made during implementation

These are deviations from the original scope, noted for traceability:

  • TelegramEvent envelope enriched. The phase doc listed chatId, userId, text for text events and chatId, userId, callbackData for callbacks. The implementation also carries username, firstName, and messageId on every event kind. Rationale: the users table already has columns for username/first_name, and messageId is needed by Phase 7’s answerCallbackQuery flow. Documented in components/telegram-normalize.md.
  • Webhook returns 200 even when the handler throws. Intentional — Telegram redelivers on non-2xx, and a deterministic downstream bug would otherwise loop forever. Transient failures will be retried via Inngest in Phase 5. Documented in components/http-telegramWebhook.md.
  • allowed_updates whitelist sent during webhook registration restricts Telegram to message and callback_query deliveries, so normalize()’s drop paths exist mostly for defense-in-depth.
  • Architecture-as-built documented at architecture/phase-1-inbound.md with the full request lifecycle and boot order.

Automated Unit Testing

All landed and green (npm test41 passed, 6 skipped; the 6 skipped are DB-gated and run when Postgres is reachable):

  • config.test.ts — Zod env loader: rejects missing required vars with a clear error; accepts a valid env; applies PORT=3000 default; coerces numeric vars correctly. (file)
  • normalize.test.ts — Telegram update fixtures map correctly:
    • text update → TelegramEvent { kind: 'text', text, chatId, userId, username, firstName, messageId }
    • photo update → TelegramEvent { kind: 'photo', fileId of largest size, dimensions }
    • document update → TelegramEvent { kind: 'document', fileId, mimeType, size }
    • callback query → TelegramEvent { kind: 'callback', callbackData, callbackQueryId, chatId, userId }
    • drop paths: edited_message, channel_post, sticker, poll, game callback, inaccessible callback, inline-mode callback (file)
  • webhook.test.tsPOST /telegram/webhook:
    • missing X-Telegram-Bot-Api-Secret-Token → 401, no side effects
    • mismatched secret → 401, attempt is logged
    • matching secret → 200, normalized event forwarded
    • ignored update kind → 200, handler not invoked
    • handler throws → still 200 + error log (file)
  • healthz.test.tsGET /healthz returns { ok: true } with content-type application/json. (file)
  • users.repo.test.ts (transactional, against the docker-compose DB):
    • first inbound update from an unknown telegram_user_id inserts a row with status='pending', role='user'
    • a second update for the same user is idempotent (no duplicate inserts)
    • bootstrapAdmin inserts ADMIN_TELEGRAM_USER_ID with status='active', role='admin'; re-running is a no-op; promotes a pre-existing pending row in place (file)
  • logger.test.ts — every request emits one summary log line containing {requestId, route, durationMs, outcome}; child logs share the same requestId. (file)
  • dispatch.test.ts (added during implementation — composition test for src/telegram/dispatch.ts): db-then-next ordering, null name passthrough, non-text events still touch the table, DB failure short-circuits next. (file)

Integration scope (deferred to integration suite): full setWebhook round-trip via a captured Telegram dev bot is out of scope for unit tests — exercised manually via npm run register-webhook.

Definition of Done

  • Send /start from your personal account to the bot → reply arrives within 2s. (pending next VPS deploy — code path is unit-tested + dispatch wiring matches the runbook)
  • User row exists in DB with status='pending'. (pending VPS deploy)
  • Admin user row exists with status='active'. (pending VPS deploy)
  • All requests logged with requestId correlation. (logger.test.ts proves the contract end-to-end via Fastify’s onResponse hook.)