Skip to main content
Glama
s4relok

tg-mcp

by s4relok

tg-mcp

CI

Telegram digest MCP server that logs into a Telegram user account and works only with selected Telegram chats and channels. Read access is the default; source management is an explicit authenticated capability.

Current shape

  • Node.js 22 ESM service.

  • Express + MCP Streamable HTTP endpoint at /mcp.

  • Optional OAuth 2.1 protected MCP endpoint at /tg-mcp/oauth-mcp.

  • REST/OpenAPI fallback under /tg-mcp/api and /tg-mcp/openapi.json.

  • MongoDB storage.

  • GramJS-based Telegram user account sync CLI.

  • Optional OpenAI Audio Transcriptions pipeline for Telegram voice/audio messages.

  • Optional read-only Telegram slash bot.

  • MCP prompts:

    • daily_telegram_digest

    • search_telegram

  • Read-only MCP tools:

    • list_sources

    • get_sync_status

    • get_audio_transcription_status

    • get_daily_digest

    • get_period_summary

    • search_telegram_messages

    • get_message_context

    • get_action_items

    • get_source_summary

  • Optional authenticated owner media tools:

    • transcribe_source_audio

    • get_telegram_audio

    • list_source_images

    • get_telegram_images

  • Optional authenticated owner MCP tools:

    • send_telegram_message

    • enable_source

    • disable_source

    • set_source_tags

    • sync_source

    • get_source_settings

    • update_source_settings

Related MCP server: telegram-user-mcp

Local development

npm ci
npm run cli -- setup-env
npm test
npm start

Health:

curl http://127.0.0.1:3010/health

REST/OpenAPI fallback:

curl http://127.0.0.1:3010/tg-mcp/openapi.json
curl "http://127.0.0.1:3010/tg-mcp/api/sync/status"
curl "http://127.0.0.1:3010/tg-mcp/api/transcriptions/status"
curl "http://127.0.0.1:3010/tg-mcp/api/digest/daily?timelineLimit=80"
curl "http://127.0.0.1:3010/tg-mcp/api/sources/<sourceId>/summary?date=2026-07-09"
curl "http://127.0.0.1:3010/tg-mcp/api/digest/daily?sourceQuery=project"
curl "http://127.0.0.1:3010/tg-mcp/api/digest/daily?refresh=true"
curl "http://127.0.0.1:3010/tg-mcp/api/search?query=release"

If APP_AUTH_TOKEN is set, pass Authorization: Bearer <token> for REST and MCP calls.

When NODE_ENV=production, the HTTP service refuses to start without APP_AUTH_TOKEN unless ALLOW_UNAUTHENTICATED=true is set explicitly. Keep that override only for private tests.

ChatGPT Web developer-mode test

ChatGPT Web developer-mode apps do not accept a custom static bearer token for MCP. Authenticated MCP apps should implement OAuth 2.1. For a private read-only test, keep the main /mcp endpoint protected and expose a second no-auth endpoint with a long random path:

CHATGPT_MCP_PATH=/tg-mcp/chatgpt-mcp-<long-random-slug>

Then paste this URL into ChatGPT Web as the developer-mode MCP server URL:

https://celticspear.com/tg-mcp/chatgpt-mcp-<long-random-slug>

Do not publish that URL. Anyone who knows it can call the read-only MCP tools for the selected Telegram sources. The no-auth path never exposes disabled source metadata or source-management tools, even when owner management is enabled elsewhere. For a shared or published app, use the OAuth endpoint below.

OAuth 2.1 MCP endpoint

tg-mcp can act as an OAuth protected resource server for ChatGPT. It deliberately does not implement login, consent, token issuance, client registration, or refresh tokens: use an established external identity provider that supports authorization code + PKCE and publishes OAuth/OIDC discovery metadata.

Configure the resource server:

OAUTH_ENABLED=true
OAUTH_MCP_PATH=/tg-mcp/oauth-mcp
OAUTH_RESOURCE=https://celticspear.com/tg-mcp/oauth-mcp
OAUTH_ISSUER=https://<your-idp-issuer>
OAUTH_JWKS_URL=https://<your-idp-issuer>/<jwks-path>
OAUTH_JWT_ALGORITHMS=RS256,ES256
OAUTH_ALLOWED_SUBJECTS=<your-exact-idp-sub>
OAUTH_RESOURCE_DOCUMENTATION=https://<your-docs-url>

The IdP must echo the OAuth resource parameter and issue an expiring JWT access token with an audience exactly equal to OAUTH_RESOURCE. Tokens must contain sub, scope or scp, and preferably client_id or azp. Configure ChatGPT's callback URL shown in the app/connector management UI and let the IdP expose its authorization/token endpoints through discovery.

Available scopes:

  • telegram:read: enabled-source lists, digests, search, summaries, status, original audio delivery when MCP_AUDIO_TOOLS_ENABLED=true, and owner image List/Get when MCP_IMAGE_TOOLS_ENABLED=true.

  • telegram:sources:read: disabled-source catalog and source settings.

  • telegram:sources:manage: enable/disable, tags, and settings mutations.

  • telegram:sync:run: exact bounded manual sync and manual audio transcription.

  • telegram:messages:send: send a plain-text or Rich Text message to Saved Messages.

The OAuth transport always requires telegram:read. Each privileged tool checks its additional scopes against the current request token, including after a session has been initialized. Missing scopes return an MCP mcp/www_authenticate challenge so ChatGPT can request authorization again.

Discovery metadata is published at both:

https://celticspear.com/.well-known/oauth-protected-resource
https://celticspear.com/.well-known/oauth-protected-resource/tg-mcp/oauth-mcp

MCP_SOURCE_MANAGEMENT_ENABLED=true is required before privileged source tools are registered. Manual transcription, original audio delivery, and image delivery are controlled independently by MCP_MANUAL_TRANSCRIPTION_ENABLED, MCP_AUDIO_TOOLS_ENABLED, and MCP_IMAGE_TOOLS_ENABLED. Keep APP_AUTH_TOKEN for admin, REST, CLI setup, and the legacy /mcp endpoint; OAuth protects only OAUTH_MCP_PATH. See the implementation and IdP rollout checklist in docs/oauth-scopes-plan.md.

Telegram setup

Fill these values in .env or /srv/tg-mcp/shared/.env:

TELEGRAM_API_ID=
TELEGRAM_API_HASH=
TELEGRAM_SESSION_FILE=/srv/tg-mcp/shared/sessions/telegram.session
ALLOWED_SOURCE_IDS=

Create or update the env file:

npm run cli -- setup-env --set TELEGRAM_API_ID=<api_id> --set TELEGRAM_API_HASH=<api_hash>

For the VPS layout:

export TELEGRAM_API_ID=<api_id>
export TELEGRAM_API_HASH=<api_hash>
npm run cli -- setup-env --production --env-path /srv/tg-mcp/shared/.env --from-env TELEGRAM_API_ID --from-env TELEGRAM_API_HASH

Existing secrets and safety allowlists (ALLOWED_SOURCE_IDS, OAUTH_ALLOWED_SUBJECTS) are preserved unless you pass a new value with --set or --from-env. The command writes mode 0600, creates a backup before overwriting an existing file, and generates APP_AUTH_TOKEN when it is missing.

List available sources:

npm run cli -- login
npm run cli -- doctor --telegram
npm run cli -- list-sources

Save the source list into MongoDB, then enable selected chats/channels:

npm run cli -- refresh-sources
npm run cli -- find-sources project
npm run cli -- select-source "Project Alpha" --tag work
npm run cli -- sync

Send one plain-text message to the authorized account's Saved Messages:

npm run cli -- send-message "Text for Saved Messages"

Send an interactive Rich Text checklist from the authorized user account:

npm run cli -- send-message $'- [ ] Open task\n- [x] Completed task' --rich-text

send-message uses the existing non-interactive Telegram session, does not connect to MongoDB, and currently has no option for selecting another chat or group. Plain text is sent literally without Markdown parsing and must contain between 1 and 4096 characters. --rich-text sends Telegram Rich Markdown using the account's user session and supports up to 32768 characters. Rich Text is a Telegram Premium feature; checklist rows use - [ ] and - [x].

Useful variants:

npm run cli -- disable-source <id>
npm run cli -- set-source-tags <id> --tag work --tag project-x
npm run cli -- enable-source <id> --tag work
npm run cli -- get-source-settings <id>
npm run cli -- update-source-settings <id> --sync-interval-seconds 900 --history-depth-days 30 --priority 80
npm run cli -- update-source-settings <id> --include-media false --include-replies true --include-forwarded-posts false
npm run cli -- update-source-settings <id> --priority inherit
npm run cli -- db-sources --include-disabled
npm run cli -- sync --source-id <id> --limit 100
npm run cli -- backfill --days 7 --limit 1000
npm run cli -- transcription-status --source-id <id>
npm run cli -- transcribe-audio --source-id <id> --limit 1
npm run cli -- retry-failed-transcriptions --source-id <id> --limit 10

tg_sources.enabled is the operational source of truth. If ALLOWED_SOURCE_IDS is set, it is an additional hard server ceiling: a DB-enabled source outside that list cannot be synchronized, including through CLI, admin, or MCP manual sync. Run refresh-sources before enabling a newly discovered source.

Tags are normalized to lowercase. set-source-tags replaces tags by default; use --mode add or --mode remove for incremental changes.

Disabling a source stops future sync and excludes it from normal search/digests, but it does not delete already stored messages. To delete them, first disable the source and then run the CLI-only destructive operation:

npm run cli -- purge-source-data <id> --force

login is interactive and writes the Telegram session file. Later commands reuse that session and should not prompt unless Telegram requires reauthorization.

Check readiness:

npm run cli -- doctor
npm run cli -- doctor --telegram
npm run cli -- doctor --env-path /srv/tg-mcp/shared/.env

doctor returns machine-readable checks plus nextSteps with the next safe commands for the current setup state. doctor --telegram also performs a non-interactive authorization check with the existing session file.

Every CLI command that reads runtime config accepts --env-path PATH, and the service also honors TG_MCP_ENV_FILE. This is useful on the VPS because production secrets live in /srv/tg-mcp/shared/.env, outside the release checkout.

Admin operations

Authenticated admin endpoints are available for operations that should not be exposed as MCP tools:

curl -X POST http://127.0.0.1:3010/admin/sources/refresh \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>"

curl -X POST http://127.0.0.1:3010/admin/sources/select \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"query":"Project Alpha","tags":["work"]}'

curl -X POST http://127.0.0.1:3010/admin/sync \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"sourceIds":["<sourceId>"],"limit":100}'

Use {"backfillDays":7} with /admin/sync to bypass the incremental cursor for a historical import. Source management endpoints also support /admin/sources/<sourceId>/enable, /admin/sources/<sourceId>/disable, /admin/sources/<sourceId>/tags, and GET/PATCH /admin/sources/<sourceId>/settings. Endpoints that talk to Telegram use the existing session file and never prompt.

Example settings patch with optimistic concurrency:

curl -X PATCH http://127.0.0.1:3010/admin/sources/<sourceId>/settings \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"settings":{"syncIntervalSeconds":900,"priority":80},"expectedVersion":2}'

All successful source mutations are appended to tg_source_audit with actor, action, before/after state, and timestamp.

Background sync

The HTTP service can run a safe background sync loop after the Telegram session file exists:

TELEGRAM_SYNC_ENABLED=true
TELEGRAM_SYNC_INTERVAL_SECONDS=300
TELEGRAM_SYNC_ON_START=true
SOURCE_DEFAULT_SYNC_INTERVAL_SECONDS=300
SOURCE_DEFAULT_HISTORY_DEPTH_DAYS=30
SOURCE_SCHEDULER_POLL_INTERVAL_SECONDS=30

If credentials, session, or selected sources are missing, the worker logs a warning and waits for the next interval. It never prompts from the systemd service.

The scheduler polls for due sources, orders them by nextSyncAt and per-source priority, and uses a lease so background, admin, CLI, and MCP sync cannot overlap for the same source. TELEGRAM_SYNC_INTERVAL_SECONDS remains a compatibility fallback for SOURCE_DEFAULT_SYNC_INTERVAL_SECONDS.

Normal sync is incremental: each source tracks lastSyncedMessageId and later runs request only newer Telegram messages. backfill --days N intentionally bypasses that cursor for historical imports, but is clamped to the source's historyDepthDays setting.

With background sync enabled, a persistent MTProto listener also applies UpdateMessageReactions events to already stored messages. MCP search, context, and digest message objects expose reactions (emoji, custom emoji, or paid reaction aggregates) and reactionCount. Reaction changes invalidate cached digests for that source. Run a bounded historical backfill once after upgrading to populate reactions that existed before the listener was started.

Per-source settings:

  • syncIntervalSeconds: 60..604800, or null/CLI inherit for the server default.

  • historyDepthDays: 1..3650, or inherit. This limits import depth and is not retention.

  • includeMedia: keeps supported audio and image metadata and permits supported audio transcription jobs; text captions remain searchable when media metadata is disabled.

  • includeReplies: imports or skips reply messages.

  • includeForwardedPosts: imports or skips forwarded messages.

  • priority: 0..100; higher values run first when multiple sources are due.

To expose owner write tools on the bearer-protected MCP endpoint, explicitly enable:

MCP_SOURCE_MANAGEMENT_ENABLED=true
MCP_MESSAGE_SENDING_ENABLED=true
MCP_MANUAL_TRANSCRIPTION_ENABLED=true
MCP_AUDIO_TOOLS_ENABLED=true
MCP_IMAGE_TOOLS_ENABLED=true

These flags have no effect on the no-auth CHATGPT_MCP_PATH; that endpoint remains read-only. On the OAuth endpoint, source tools additionally require telegram:sources:read plus telegram:sources:manage, manual sync and transcription require telegram:sync:run, and send_telegram_message requires telegram:messages:send.

When enabled, send_telegram_message accepts text and an optional format. The default plain_text sends text literally. rich_text sends Telegram Rich Markdown from the authenticated user account, so task-list rows such as - [ ] Open and - [x] Done are owned by that user and remain interactive. The tool is non-idempotent: clients must not automatically retry an ambiguous failure. Selecting another chat or group is not supported yet.

Check data freshness:

curl "http://127.0.0.1:3010/tg-mcp/api/sync/status?staleAfterHours=24"

The MCP tool get_sync_status exposes the same source freshness state to ChatGPT so it can say when data is missing or stale before summarizing.

Audio transcription

Telegram voice notes and audio files are synced as messages even when they do not have a text caption. The sync stores audio metadata in MongoDB and queues them with transcription.status=pending. Once transcribed, the transcript is stored as transcriptText on the same tg_messages document, included in Mongo text search, message context, daily digests, and action detection.

Enable the OpenAI Audio Transcriptions worker:

OPENAI_API_KEY=<openai_api_key>
OPENAI_TRANSCRIPTION_ENABLED=true
OPENAI_TRANSCRIPTION_MODEL=gpt-4o-mini-transcribe
AUDIO_TRANSCRIPTION_SOURCE_IDS=<saved_messages_source_id>
AUDIO_TRANSCRIPTION_INTERVAL_SECONDS=3600
AUDIO_TRANSCRIPTION_BATCH_SIZE=1
AUDIO_TRANSCRIPTION_WORK_DIR=/srv/tg-mcp/shared/audio-work

The background worker only claims jobs from explicitly configured transcription sources: AUDIO_TRANSCRIPTION_SOURCE_IDS or AUDIO_TRANSCRIPTION_SOURCE_TAGS. If neither is set, it will not process pending audio from arbitrary enabled Telegram chats. When background sync stores at least one audio/voice message, the service immediately runs one bounded transcription pass; AUDIO_TRANSCRIPTION_INTERVAL_SECONDS remains a safety polling interval. Keep AUDIO_TRANSCRIPTION_BATCH_SIZE=1 to cap each pass to one OpenAI request.

gpt-4o-mini-transcribe is the default for clean personal recordings. Use gpt-4o-transcribe when recordings are noisy, terminology-heavy, or require the highest available transcription quality.

The worker downloads Telegram media into the work directory, submits it to the OpenAI Audio Transcriptions API, stores only the transcript/metadata by default, and removes the temporary audio file. Completed items have transcriptText plus transcription.status=done, so later runs do not claim the same file again. Files larger than AUDIO_TRANSCRIPTION_MAX_FILE_BYTES are split with ffmpeg when AUDIO_TRANSCRIPTION_SPLIT_LARGE_FILES=true.

Manual operations:

npm run cli -- transcription-status
npm run cli -- transcribe-audio --limit 1
npm run cli -- transcribe-audio --source-id <saved_messages_source_id> --limit 1
npm run cli -- retry-failed-transcriptions --limit 10

Authenticated admin endpoints:

curl http://127.0.0.1:3010/admin/transcriptions/status \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>"

curl -X POST http://127.0.0.1:3010/admin/transcriptions/run \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"sourceIds":["<saved_messages_source_id>"],"limit":1}'

curl -X POST http://127.0.0.1:3010/admin/transcriptions/retry-failed \
  -H "Authorization: Bearer <APP_AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"limit":10}'

The read-only REST endpoint /tg-mcp/api/transcriptions/status and MCP tool get_audio_transcription_status expose counts for selected sources. Search and digest tools automatically use completed transcripts.

Manual MCP transcription

When MCP_MANUAL_TRANSCRIPTION_ENABLED=true, authenticated owner/OAuth MCP clients can call:

transcribe_source_audio(sourceId="<exact-enabled-source-id>", limit=5)

The command processes only pending voice/audio messages from that exact source. It does not call Telegram sync, change source settings, or add the source to AUDIO_TRANSCRIPTION_SOURCE_IDS / AUDIO_TRANSCRIPTION_SOURCE_TAGS. Call sync_source first when fresh or historical audio is required.

OAuth requires telegram:read telegram:sync:run. The command is never registered on the no-auth CHATGPT_MCP_PATH.

Original Telegram audio

When MCP_AUDIO_TOOLS_ENABLED=true, authenticated owner/OAuth MCP clients can request only explicitly selected voice/audio messages:

get_telegram_audio(
  sourceId="<exact-enabled-source-id>",
  messageIds=[75606]
)

The contract intentionally matches get_telegram_images: one exact enabled sourceId and 1..MCP_AUDIO_GET_MAX_ITEMS exact positive messageIds. There is no wildcard, date-range export, semantic audio search, or source-wide download. The service verifies the stored message, media.kind=voice|audio, an audio MIME allowlist, the live Telegram message, and per-file/total limits. It also applies the configured ALLOWED_SOURCE_IDS ceiling when non-empty.

MCP_AUDIO_TOOLS_ENABLED=true
MCP_AUDIO_GET_MAX_ITEMS=3
MCP_AUDIO_MAX_FILE_BYTES=10485760
MCP_AUDIO_MAX_TOTAL_BYTES=20971520

Each successful item produces a small text metadata block followed by MCP audio content:

{
  "type": "audio",
  "data": "<base64 original bytes>",
  "mimeType": "audio/ogg"
}

The bytes are returned unchanged; no transcoding or transcription occurs. structuredContent includes sourceId, messageId, mimeType, sanitized fileName, byte size, duration, and per-item status, but deliberately omits base64 data. The implementation reuses the transcription pipeline's exact Telegram message resolver, bounded downloader, and private AUDIO_TRANSCRIPTION_WORK_DIR; the temporary file is deleted in finally and is never placed under a web root or retained as a second cache. Audio bytes and transcripts are not logged. The tool requires telegram:read on OAuth and is never registered on the no-auth CHATGPT_MCP_PATH.

After enabling the flag and deploying a committed revision, run the bounded smoke test without writing the returned audio to disk or printing MCP content:

SOURCE_ID=5509770803 \
MESSAGE_ID=75606 \
EXPECTED_SIZE=2578344 \
ENV_FILE=/srv/tg-mcp/shared/.env \
node ops/smoke-audio.mjs

The script prints only identifiers, MIME, byte length, and SHA-256; it does not print captions, transcripts, or audio data.

Telegram images

Telegram photos plus JPEG, PNG, and WebP documents are stored as message metadata when the source has includeMedia=true. The server does not run OCR, vision models, embeddings, or automatic image tagging.

Enable owner image tools and the fixed-retention file cache:

MCP_IMAGE_TOOLS_ENABLED=true
MCP_IMAGE_LIST_MAX_LIMIT=100
MCP_IMAGE_GET_MAX_ITEMS=5
MCP_IMAGE_MAX_FILE_BYTES=10485760
MCP_IMAGE_MAX_TOTAL_BYTES=26214400
MCP_IMAGE_CACHE_MAX_ITEMS_PER_SYNC=100
IMAGE_CACHE_DIR=/srv/tg-mcp/shared/image-cache
IMAGE_CACHE_RETENTION_DAYS=30
IMAGE_CACHE_CLEANUP_INTERVAL_SECONDS=3600

IMAGE_CACHE_RETENTION_DAYS is clamped to 1..30. A cache entry expires at a fixed time calculated when the file is written; List/Get calls do not extend it. The startup/periodic janitor removes expired files and Mongo cache metadata. Image bytes live only in IMAGE_CACHE_DIR, outside the web root. MongoDB stores the Telegram reference, relative path, MIME type, size, SHA-256, cachedAt, and expiresAt.

Manual workflow:

sync_source(
  sourceIds=["<exact-enabled-source-id>"],
  backfillDays=30,
  cacheImages=true,
  imageLimit=100
)

list_source_images(
  sourceId="<exact-enabled-source-id>",
  limit=20
)

get_telegram_images(
  sourceId="<exact-enabled-source-id>",
  messageIds=[123, 124]
)

sync_source does not download image bytes unless cacheImages=true. get_telegram_images reads unexpired local cache entries first and downloads only cache misses from Telegram. It returns a text context block followed by real MCP image content for each successful item. The model using the MCP app can then inspect the pixels itself.

Images without captions cannot be found semantically by this server. Use list_source_images with a bounded date range and pass selected message ids to get_telegram_images. Albums retain Telegram groupedId.

If an image is removed from Telegram after it was cached, the local copy remains available only until its fixed expiry. There is no permanent archive. Both image tools require telegram:read, exact enabled sources, and the ALLOWED_SOURCE_IDS ceiling when configured. They are never registered on the no-auth CHATGPT_MCP_PATH.

Permanent chat backups

An owner can explicitly protect individual chats. Each enable operation accepts one exact sourceId; repeat it for a second chat. No global selection or tag expansion occurs. Every protected chat keeps its own permanent archive on this server. An optional second copy supplements the local archive.

BACKUP_DIR=/srv/tg-mcp/shared/chat-archive
BACKUP_REPLICA_DIR=
BACKUP_INTERVAL_SECONDS=60
BACKUP_REPLICA_INTERVAL_SECONDS=86400
BACKUP_PAGE_SIZE=100
BACKUP_MEDIA_BATCH_SIZE=20
BACKUP_MAX_FILE_BYTES=2147483648
BACKUP_MIN_FREE_BYTES=536870912
MCP_BACKUP_TOOLS_ENABLED=true

BACKUP_DIR must be outside release checkouts, image cache, audio work directories and the web root. It has no automatic retention or purge. Do not change this path without moving the complete archive while the service is stopped. Metadata and SHA-256 addressed blobs are authoritative local files; Mongo remains the mutable operational index. Archive search rebuilds an in-memory projection from the versioned journal and works without Mongo or Telegram. This first implementation keeps that projection in memory; size the server for the selected chat histories.

npm run cli -- backup-source <sourceId> --pages 5
npm run cli -- backup-status <sourceId>
npm run cli -- run-source-backup <sourceId> --pages 5
npm run cli -- pause-source-backup <sourceId>
npm run cli -- resume-source-backup <sourceId>
npm run cli -- search-source-backup <sourceId> --query "meeting"
npm run cli -- backup-message <sourceId> --message-id 123
npm run cli -- backup-media <sourceId> --message-id 123
npm run cli -- verify-source-backup <sourceId>

The first enable preserves the existing index, transcripts and available image cache bytes, then starts a bounded collection pass. The server resumes collection every minute, including all accessible history regardless of normal index depth, reply, forward or media filters. Cursors commit after records and media jobs; large incremental backlogs are paged without jumping over messages. Voice/audio, the largest Telegram photo, and document bytes are saved without transcoding. Legacy cache bytes are retained with original=false because old cache metadata does not prove file version or maximum photo quality. Oversized originals remain blocked_by_limit; they are never replaced with a smaller photo silently.

Observed edits retain previous versions, including A → B → A. Channel deletion updates add markers without deleting content. Private/group deletion updates without a peer are not attributed by guessing; missing messages remain readable. A connected update listener plus periodic full-history reconciliation captures changes. No mechanism can recover unseen messages or originals already deleted before they were saved. Unknown attachment kinds and unavailable originals remain visible gaps. History completion, media completion and replica verification are separate status fields.

Disabling the operational source pauses network collection; owner archive reading still works. pause-source-backup stops collection and preserves the archive. purge-source-data rejects protected sources even while paused. Existing source allowlists still apply; removing access never deletes archived files. Archiving does not automatically purchase transcription for the entire history:

npm run cli -- transcribe-backup-audio <sourceId> --limit 1

That explicit bounded command transcribes saved audio without Telegram. Existing normal transcription jobs also preserve their downloaded originals and append their results to the protected archive. Failed OpenAI requests do not remove the archived original.

Owner admin routes live at /admin/backups/<sourceId>: GET status, POST /enable, /pause, /run, /verify, /replicate, /transcribe; GET /search, /messages/<messageId>, /messages/<messageId>/media. HTTP replication uses only the configured destination; arbitrary filesystem destinations and restore are CLI only. Admin routes require APP_AUTH_TOKEN, including in development.

With MCP_BACKUP_TOOLS_ENABLED=true, owner/OAuth MCP adds get_source_backup_status, search_source_backup, get_backup_message_context, get_backup_media, enable_source_backup, pause_source_backup, run_source_backup. OAuth requires telegram:read and telegram:backup:read or telegram:backup:manage for the corresponding operation; scopes are rechecked per request. No archive tools are exposed on the no-auth MCP path. MCP media delivery is bounded to 25 MiB per call; larger files use the owner admin download or CLI. Only supported image/audio MIME types are embedded in MCP content.

For a second copy, configure BACKUP_REPLICA_DIR as a separately mounted external filesystem (another server or NAS), or provide a destination explicitly:

npm run cli -- export-source-backup <sourceId> --destination /mnt/backups/tg-mcp
npm run cli -- restore-source-backup <sourceId> --snapshot /mnt/backups/tg-mcp/<snapshot> --target /srv/tg-mcp/restored-archive
BACKUP_DIR=/srv/tg-mcp/restored-archive npm run cli -- search-source-backup <sourceId> --query "meeting"

Exports are self-contained full snapshots for one chat: selection, committed journal prefix, blobs and a checksummed manifest. Prior snapshots and the local archive are retained. Each snapshot duplicates its file data; plan destination capacity accordingly. A .partial directory is never a verified snapshot. Restore verifies all hashes, refuses an existing target and starts with capture paused. It does not overwrite the live Mongo database or send anything to Telegram. No storage provider or remote mount is provisioned automatically. A second local directory on the same server is not protection against loss of that server. Object Lock / WORM policies must be configured at the external storage layer; the application does not claim administrator-proof immutability.

Daily server archive without duplicate copies

ops/daily-backups.mjs SOURCE_ID... compares a stable fingerprint of each selected chat's indexed messages, transcripts, source metadata, supplemental data and media cache inventory. Sync timestamps and expiring Telegram file references do not count as changes. With no changes and no pending/retryable media, it returns unchanged without connecting to Telegram or appending archive records. Incomplete history and due media retries are processed even with the same index. The fingerprint is saved only after the updated archive passes verification.

The job maintains the existing single archive at BACKUP_DIR/<sourceId>; it does not create dated server copies. The archive still retains previously saved messages and originals if they disappear from Telegram. Continuous capture should remain paused when using this daily mode. An unchanged operational index cannot reveal Telegram changes which the normal indexer has not observed yet.

Install ops/tg-mcp-backup-daily.service and .timer into /etc/systemd/system/, and set BACKUP_SOURCE_IDS to the space-separated exact selected IDs in /srv/tg-mcp/shared/daily-backups.env. Enable the timer with systemctl enable --now tg-mcp-backup-daily.timer. It checks daily at 03:30 Europe/Chisinau and catches up after downtime. Inspect results with journalctl -u tg-mcp-backup-daily.service. PC copies remain manual.

Copy to this Windows computer

After each successful Windows pull, an offline viewer is generated at E:\backups\tg-mcp\Просмотр\index.html. Open it directly in a browser by double-clicking the file. It includes chat selection, text/transcript search, image previews, audio/video controls, and download links. It uses no network or local web server. Files in Просмотр\media have normal extensions and are independent copies, so editing them does not alter the immutable snapshots. The viewer uses each selected chat's latest complete, verified snapshot. Rebuild it without downloading from Telegram or the server:

node ops/build-backup-viewer.mjs 'E:\backups\tg-mcp' <sourceId1> <sourceId2>

The SSH pull workflow keeps permanent snapshots under E:\backups\tg-mcp (NTFS). It transfers only missing SHA-256 objects, reuses local objects through hard links across snapshots, and verifies the entire journal/files before marking the copy successful. Each snapshot has the same self-contained layout accepted by restore. Do not edit files inside snapshots or .objects: they share immutable data. Server originals are never removed. Only a successful transfer's temporary tar and staging files are cleaned up. Interrupted .downloads/.incoming/.partial directories are not complete backups and are retained for inspection.

.\ops\pull-backups.ps1 -RefreshServer -SourceIds @('<sourceId1>', '<sourceId2>') -DestinationRoot 'E:\backups\tg-mcp'
.\ops\install-backup-task.ps1 -SourceIds @('<sourceId1>', '<sourceId2>') -DestinationRoot 'E:\backups\tg-mcp'
Start-ScheduledTask -TaskName 'tg-mcp chat backups'

The Windows task is manual only, with no daily or logon triggers. Each launch refreshes the selected server archives once, copies them to the PC, and rebuilds the viewer. Paused automatic capture remains paused even if the manual command fails. Use backup-source-once SOURCE_ID for a manual server-only refresh. The task uses the current user's SSH key, strict host-key checking and non-interactive mode; the PC must be on and the user logged in. Concurrent pulls are excluded. Configuration is in pull-config.json and execution output in pull.log under the destination. Node.js, Windows tar, ssh and scp must be installed. A 512 MiB reserve and temporary transfer space are checked on the PC. The service status records the verified PC destination only after local checksum verification succeeds. Neither SSH keys nor Telegram sessions enter snapshots.

Archive status includes free space, media failures and the last verified replica. Downloads and journal appends reserve at least BACKUP_MIN_FREE_BYTES (512 MiB by default); lack of space stops collection instead of deleting previous records. Collection/replication errors are logged and do not delete old copies. Replica errors leave local capture active. For checksum checks use verify-source-backup; periodically exercise restore into a new empty target. A dead writer PID is recovered automatically. If a process dies before creating its lock owner file or during lock recovery, inspect .writer-lock / .lock-recovery only after stopping every server/CLI writer; do not remove a live writer's lock.

Tests include actual file restore after local archive loss, corrupted blobs, isolated sources, versions, pagination and HTTP/MCP/OAuth access. The optional real Mongo integration test drops only its isolated temporary database and restores archive search from the replica:

RUN_MONGO_BACKUP_TESTS=1 node --test test/backupMongo.test.js

Digest cache

Daily, period, and source summaries are cached in tg_digests. The cache key includes the period, timezone, source filters, timeline options, and selected source sync state, so a later Telegram sync naturally invalidates stale summaries.

Use refresh=true in REST calls, or refresh: true in MCP tool arguments, to force recomputation from stored messages.

Optional Telegram slash bot

The HTTP service can also run a small read-only Telegram bot for quick checks against the same selected data:

TELEGRAM_BOT_ENABLED=true
TELEGRAM_BOT_TOKEN=<bot token from BotFather>
TELEGRAM_BOT_ALLOWED_CHAT_IDS=<your chat id or comma-separated ids>
TELEGRAM_BOT_TIMEZONE=Europe/Chisinau

Supported commands:

/digest_today [source]
/digest_week [source]
/search <query>
/actions [source]
/sources [query]

TELEGRAM_BOT_ALLOWED_CHAT_IDS is optional, but recommended. The bot never sends Telegram messages on behalf of the synced user account; it only replies with digests/search results from MongoDB.

VPS quick deploy

The target server already has Apache, MongoDB, and bundled Node.js.

Expected layout:

/srv/tg-mcp/
  repo.git/
  releases/
  shared/
    .env
    audio-work/
    image-cache/
    logs/
    sessions/
    node/
  current -> releases/<release>

Deploy code:

ops/deploy.sh

Run a VPS preflight from a checkout or release directory:

ENV_FILE=/srv/tg-mcp/shared/.env ops/preflight.sh
CHECK_TELEGRAM=true ENV_FILE=/srv/tg-mcp/shared/.env ops/preflight.sh

The preflight checks Node.js, git state, tests, and doctor against the selected env file. CHECK_TELEGRAM=true additionally verifies the existing Telegram session without prompting.

Run a smoke test after the HTTP service is running:

AUTH_TOKEN=<APP_AUTH_TOKEN> BASE_URL=http://127.0.0.1:3010 ops/smoke.sh
AUTH_TOKEN=<APP_AUTH_TOKEN> BASE_URL=https://celticspear.com ops/smoke.sh

The smoke test checks /health, OpenAPI, REST sync status, and an MCP initialize request.

Install systemd service:

sudo ops/install-systemd-service.sh

Install Apache /mcp proxy:

sudo ops/install-apache-proxy.sh

Install log rotation for /srv/tg-mcp/shared/logs/*.log:

sudo ops/install-logrotate.sh

That installer exposes:

https://celticspear.com/mcp
https://celticspear.com/tg-mcp/oauth-mcp
https://celticspear.com/.well-known/oauth-protected-resource
https://celticspear.com/tg-mcp/openapi.json
https://celticspear.com/tg-mcp/api/...

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    A read-only Telegram MCP server that retrieves messages from your DMs, groups, and channels, enabling Claude to generate executive briefings from Telegram conversations.
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    An MCP server that connects to Telegram as your real user account and exposes read-only tools to read and search messages, list chats and folders, inspect group info, and download media.
    11 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Read-only MCP server for Telegram that enables reading messages and transcribing voice, audio, and video notes via the Telegram API for use with Codex and Claude Code.
    Apache 2.0
  • A
    license
    Not graded
    quality
    C
    maintenance
    MCP server that reads Telegram groups and channels via MTProto to produce source-linked summaries with participant contributions, decisions, and risks over specified time ranges.
    90 npm
    MIT