Operations & Strategy
The operational shape and overall strategic decisions for the project: testing strategy, deployment runbook, tech stack, and the still-open design questions. This document is the authoritative ops reference until ../dev/ and ../testing/ get populated as the code stabilizes.
Testing Strategy
- Unit tests (
vitest): pure utilities —normalize.ts, validation, role/status checks, error mappers, event schema parsing. Per-phase unit-test cases are listed in eachphase-n-*.md. - Integration tests: real local Postgres in a Docker container (or
pg-memfor fast suites). Cover user approval flow, memory upsert/read, job state transitions, chat-history pagination. - MCP contract tests: spin up the MCP server in-process, drive via the official MCP test client; verify each tool’s input/output shape and error codes for happy + sad paths.
- Inngest local dev:
npx inngest-cli devagainst/api/inngest. Trigger events from tests; assert side effects in Postgres. - Manual E2E (Phase 6): MCP Inspector connected to
https://bot.example.com/mcpwith bearer token; exercise every tool against a personal test bot. - CI:
npm testruns unit + integration on every push. Inngest function tests run against a local dev server in CI.
Deployment Runbook
One-time VPS setup
- Ubuntu 24.04 LTS, non-root sudo user; SSH key-only, root login disabled.
- Packages:
nginxandcertbot+python3-certbot-nginxfrom the Ubuntu archive.- Postgres from the PGDG repo (Ubuntu 24.04’s archive ships 16; we run Postgres 18).
- Node.js LTS from NodeSource (currently Node 22).
- Service user:
useradd --system --shell /usr/sbin/nologin --home-dir /var/lib/telegram-mcp --create-home telegram-mcp. - Dirs:
/opt/telegram-mcp(code),/etc/telegram-mcp(env, mode 750, ownedroot:telegram-mcp),/var/lib/telegram-mcp(runtime state if any). - Postgres: create DB
telegram_mcpand a role with password; record DSN in env. - DNS: A record
bot.<domain>→ VPS public IP. - Firewall (
ufw): allow 22, 80, 443. - TLS cert:
sudo certbot --nginx -d bot.<domain>once nginx site config below is in place. Renewal is handled automatically by thecertbot.timersystemd unit installed with the package.
nginx server block (shape, ops/nginx-bot.conf)
# Redirect plain HTTP → HTTPS
server {
listen 80;
listen [::]:80;
server_name bot.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name bot.example.com;
# certbot --nginx fills in the cert + key paths and ssl_* directives;
# the lines below are placeholders showing what to expect.
ssl_certificate /etc/letsencrypt/live/bot.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/bot.example.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
access_log /var/log/nginx/bot.access.log;
error_log /var/log/nginx/bot.error.log;
gzip on;
gzip_types text/plain application/json application/javascript text/css;
# Allow modest webhook payloads (photos/docs flow via file_id, not bytes).
client_max_body_size 2m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# MCP streamable HTTP at /mcp uses long-lived server→client streams.
# Disable response buffering and bump the upstream read timeout so the
# connection survives idle periods between agent messages and pushes.
proxy_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}systemd unit (shape)
[Unit]
Description=Telegram MCP Server
After=network.target postgresql.service
[Service]
Type=simple
User=telegram-mcp
Group=telegram-mcp
WorkingDirectory=/opt/telegram-mcp
EnvironmentFile=/etc/telegram-mcp/env
ExecStart=/usr/bin/node dist/index.js
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetInngest Cloud setup
- Create app; copy
INNGEST_EVENT_KEY+INNGEST_SIGNING_KEYinto/etc/telegram-mcp/env. - In the dashboard, register endpoint
https://bot.example.com/api/inngest. - After first deploy, Inngest auto-syncs the function list on its next request.
Telegram setup
/newbotin BotFather → bot token into env.- Programmatically call
setWebhookwithurl=https://bot.example.com/telegram/webhook&secret_token=$TELEGRAM_WEBHOOK_SECRET. Done by a smallnpm run register-webhookscript in this repo. - Optional:
setMyCommandsto populate the bot menu.
Deploy procedure
ssh vps
cd /opt/telegram-mcp
git pull
npm ci
npm run build
npm run migrate
sudo systemctl restart telegram-mcp
sudo journalctl -u telegram-mcp -fRollback
- Code:
git checkout <prev-tag>; npm ci && npm run build && systemctl restart. - DB migrations: never auto-rollback; new migration that reverses the change if needed.
Tech Stack (Provisional)
| Layer | Technology |
|---|---|
| Runtime | Node.js + TypeScript |
| HTTP framework | Fastify (provisional) |
| Telegram API | telegraf (provisional, see open question) |
| MCP SDK | @modelcontextprotocol/sdk (streamable HTTP transport) |
| Queue / orchestration | Inngest (Inngest Cloud) — no Redis required |
| Database | Postgres |
| Migrations | node-pg-migrate (provisional) |
| Validation | Zod |
| Logging | pino (JSON) → journald |
| Testing | Vitest + MCP Inspector for manual E2E |
| Vector DB | pgvector or Qdrant (post-Phase 4, if needed) |
| Deployment | Ubuntu VPS + nginx (TLS via certbot/Let’s Encrypt) + systemd, on a dedicated subdomain |
Open Questions
To be answered before Phase 1 begins (resolved questions kept here for traceability).
Resolved
- Queue / orchestration — Inngest Cloud. Replaces BullMQ + Redis.
- Notification strategy — MCP server pushes a notification to the agent on
job.completed. - Per-user concurrency — Inngest concurrency key =
userId(one job per user at a time). - Deployment target — Rented Ubuntu VPS with public IP, behind nginx.
- Public HTTPS endpoint — Subdomain of an existing domain, TLS via certbot-managed Let’s Encrypt cert (renewed by
certbot.timer). - Agent location — Separate machine; connects to this server remotely.
- MCP transport — Streamable HTTP, single
/mcpendpoint. - Runtime — Node.js + TypeScript.
- MCP auth — Static bearer token in env (
MCP_AUTH_TOKEN), checked on every/mcprequest. TLS + bearer is the boundary. - Agent identity — Claude via the Anthropic SDK / Claude Agent SDK.
- Existing agent project — None yet; built after this server is usable. Phase 6 uses MCP Inspector for validation until then.
Still open
- Scale — Personal/private bot (few users) or potentially public? Drives Inngest Cloud tier sizing and Postgres choice.
- Event schema ownership — Should Inngest event names/payloads be defined in a shared package the future agent project also consumes, or kept private to this repo and surfaced only via MCP?
- Job state mirror — Do we mirror Inngest run state in Postgres for
get_job_status, or query Inngest’s API on demand? (Mirror = faster, more code; API = simpler, rate-limited.) — currently planned as mirror; revisit at Phase 8. - Postgres location — On the same VPS (Phase 1, cheap) or managed (later, when uptime/backups matter more)?
- Token rotation — Manual redeploy is fine for v1; revisit if multiple operators or a public agent ever needs access.
- Inbound event → agent delivery — The agent connects over MCP; how does it learn about an incoming Telegram message? Options: (a) MCP server pushes a custom
notifications/telegram_eventand the agent reacts; (b) we expose apoll_inboundtool the agent calls; (c) the agent maintains a long-running tool call. Default leaning: (a), same mechanism as job-completion push. - MCP push style — Custom
notifications/job_statusmethod (simple, ad-hoc) vs modelling jobs as MCP resources and usingresources/subscribe(more spec-aligned, more surface). Default leaning: custom notification for v1. - Image source variants —
send_imagesupports{url}immediately. Add{fileId}(cheap) and{bytesBase64}(Phase 7). Any need for{s3Url}/ signed-URL pattern? Depends on where the agent’s generated images actually live. - Telegram library —
telegrafchosen as default (modern, TS-native, middleware). Confirm before locking in;node-telegram-bot-apiis the main alternative. - HTTP framework —
Fastifychosen as default (faster, better TS types than Express). Confirm or swap. - Migration tool —
node-pg-migratechosen as default (plain SQL, no ORM lock-in). Alternative: Drizzle (more TS-integrated, brings an ORM). - Image generation backend —
image.generatejob needs a real provider eventually (OpenAI Images, Replicate, fal.ai, local SD). Phase 5 ships with a stub; pick before this becomes user-visible.