tg-mcp
Provides read-only tools to fetch daily digests, period summaries, search messages, and get message context from specified Telegram chats and channels using the Telegram API via GramJS.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tg-mcpget today's digest from the work channel"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tg-mcp
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/apiand/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_digestsearch_telegram
Read-only MCP tools:
list_sourcesget_sync_statusget_audio_transcription_statusget_daily_digestget_period_summarysearch_telegram_messagesget_message_contextget_action_itemsget_source_summary
Optional authenticated owner media tools:
transcribe_source_audioget_telegram_audiolist_source_imagesget_telegram_images
Optional authenticated owner MCP tools:
send_telegram_messageenable_sourcedisable_sourceset_source_tagssync_sourceget_source_settingsupdate_source_settings
Related MCP server: telegram-user-mcp
Local development
npm ci
npm run cli -- setup-env
npm test
npm startHealth:
curl http://127.0.0.1:3010/healthREST/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 whenMCP_AUDIO_TOOLS_ENABLED=true, and owner image List/Get whenMCP_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-mcpMCP_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_HASHExisting 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-sourcesSave 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 -- syncSend 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-textsend-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 10tg_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> --forcelogin 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/.envdoctor 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=30If 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, ornull/CLIinheritfor 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=trueThese 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-workThe 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 10Authenticated 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=20971520Each 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.mjsThe 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=3600IMAGE_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=trueBACKUP_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 1That 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.jsDigest 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/ChisinauSupported 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.shRun 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.shThe 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.shThe smoke test checks /health, OpenAPI, REST sync status, and an MCP initialize request.
Install systemd service:
sudo ops/install-systemd-service.shInstall Apache /mcp proxy:
sudo ops/install-apache-proxy.shInstall log rotation for /srv/tg-mcp/shared/logs/*.log:
sudo ops/install-logrotate.shThat 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/...This server cannot be deployed
Maintenance
Related MCP Connectors
Unofficial Telegram MCP server — read, search, reply and react in your own Telegram account.
Telegram bridge for your MCP-compatible agent. Bidirectional, no LLM in our stack.
Unified inbox MCP for WhatsApp, Telegram, Email, voice — read/send messages, search, AI agents.
Read-only MCP server for Robinhood Chain token discovery, research, and due diligence via GMGN.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA read-only Telegram MCP server that retrieves messages from your DMs, groups, and channels, enabling Claude to generate executive briefings from Telegram conversations.MIT
- AlicenseNot gradedqualityBmaintenanceAn 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 npmMIT
- AlicenseNot gradedqualityAmaintenanceRead-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
- AlicenseNot gradedqualityCmaintenanceMCP 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 npmMIT