MCTL
Security model

Security model

Threat model, cryptographic invariants, Communication Agent controls, and known limitations for this deployment.

Not zero-knowledge (hosted mode). MCTL makes outbound MTProto calls to Telegram on your behalf. The running server handles plaintext Telegram data while processing requests and, when Communication Agent is enabled, decrypts encrypted agent message state for listener/queue processing. You are trusting both the deployment operator and the integrity of this code. For a stronger session-storage model, see Local Bridge mode below.

Reporting vulnerabilities

Email [email protected]. Please do not open public GitHub issues for unpatched vulnerabilities. Acknowledgement target: 72 hours. Fixes are disclosed in the CHANGELOG once a release ships.

End-user support

For product help (setup, disconnect, account questions): email [email protected]. Do not use the security address for general support.

Trust boundaries

BoundaryTrusted credential / stateLives
Inbound HTTP (Claude / ChatGPT / MCP clients → /mcp) OAuth JWT issued and verified by this server; legacy mode may use api.mctl.ai Bearer header, per request
Outbound MTProto (gotd/td → Telegram) Encrypted per-user session blob telegram_accounts.session_encrypted
Communication Agent listener and queue Per-user listener profile, encrypted message/event rows, queue jobs, conversation and policy state User-scoped Postgres agent tables; active only for listener-enabled profiles

Every database lookup for user content is intended to be scoped to the owning user. Compromise of one account should not expose another account unless the service, key derivation, or deployment boundary itself is compromised.

What the server sees

Communication Agent safety boundary

The model prompt and model output are not trusted as a security boundary. The server-side listener, state machine, policy engine, approval flow, and Telegram send gate remain authoritative.

Enabling the listener does not bypass the existing ALLOW_SEND, scope, per-account, and per-peer send gates.

Communication Agent storage and retention

Telegram account risk

MCTL Assistant is an unofficial third-party connector using the Telegram API. Telegram's anti-spam system can limit an account (PEER_FLOOD) or freeze it (FROZEN_METHOD_INVALID) when it sends spam-like traffic, whichever app the traffic comes from. On the hosted deployment every session reaches Telegram from the same datacenter under one app id, which gives that traffic a shared profile.

Local Bridge mode

When an account is set to mode='local', the MTProto session it uses lives on the user's machine rather than in the hosted session pool, and this server acts as a relay. Turning it on for a new account is self-service via the mctl-telegram-local activate CLI; migrating an existing hosted account into local mode still goes through an operator (set_account_mode). Setup is documented at /docs/local-bridge.

What logs and audit rows never persist

This guarantee applies to logs and audit rows. Communication Agent message/action tables intentionally store encrypted content as described above.

Known coupling: shared-hmac-legacy mode

AUTH_MODE=shared-hmac-legacy validates JWTs using secret material shared with an external authorization service. It must also be unlocked explicitly with AUTH_ALLOW_SHARED_HMAC_LEGACY=true. The coupling does not apply to local-jwt, the mode tg.mctl.ai runs.

Cryptographic invariants

Send gate (defense in depth)

Every newly connected account starts read-only (send_enabled=false) unless its owner chooses Read + send on the permissions step while connecting. Every write to Telegram — send_message, send_media, edit_message, delete_messages, forward_messages, set_reaction, pin_message and send_reply — requires all of:

  1. Server flag ALLOW_SEND=true. This is the operator kill switch: on for production tg.mctl.ai; set ALLOW_SEND=false on a deployment that must never deliver.
  2. Identity has the tool's scope (telegram:messages:send, or telegram:messages:pin for pins). A tool outside the token's scopes, or outside the connection's tool profile, is not listed and is refused as unknown.
  3. telegram_accounts.send_enabled = true for that account (owner sets this on the permissions step, via set_send_consent or /telegram/connect/manage).
  4. The per-peer send rate limit is not exhausted.

When any condition fails, no write RPC reaches Telegram. send_message and send_media return a dry-run result (sent=false) with a dry_reason; every other write tool — edit_message, delete_messages, forward_messages, set_reaction, pin_message and send_reply — returns a tool error naming the condition, never a preview. pin_message and send_reply additionally require a single-use confirmation issued by prepare_pin_message or draft_reply. A denied media call does not fetch its URL or decode its base64 payload.

Authentication-required mode

Rate limiting

The default per-identity token bucket is 30 requests/minute with the same burst. Write paths additionally use per-peer limits so one recipient cannot be targeted up to the global ceiling. Public and well-known endpoints are limited at the ingress layer.

Session TTL

Tamper-evident audit log

Audit rows form a SHA-256 hash chain using prev_hash and entry_hash. You can ask the server to verify the chain:

GET /api/account/audit/verify
{
  "ok": true,
  "verified": 42
}

A non-OK response includes first_bad_id and a reason. Rows created before hash chaining are skipped.

Prompt-injection content boundary

Read tools wrap Telegram message bodies in <telegram-content origin="telegram" peer="redacted" untrusted="true">…</telegram-content> and escape literal closing tags inside message text. This is a signalling boundary for the MCP client, not a substitute for the server-side Communication Agent policy engine.