# Telegram Personal Mcp

> Safe Telegram personal-account MCP with recoverable sends and hash-bound file replies

- **Type:** MCP server
- **Install:** `agentstack add mcp-stranmor-telegram-personal-mcp`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Stranmor](https://agentstack.voostack.com/s/stranmor)
- **Installs:** 0
- **Category:** [Communication](https://agentstack.voostack.com/c/communication)
- **Latest version:** 0.1.0
- **License:** Apache-2.0
- **Upstream author:** [Stranmor](https://github.com/Stranmor)
- **Source:** https://github.com/Stranmor/telegram-personal-mcp

## Install

```sh
agentstack add mcp-stranmor-telegram-personal-mcp
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# Telegram Personal MCP

[](https://github.com/Stranmor/telegram-personal-mcp/actions/workflows/ci.yml)
[](https://github.com/Stranmor/telegram-personal-mcp/releases)
[](#license)

A deterministic MCP server for Telegram personal accounts: find conversations,
recover exact message context, move attachments through a local workspace, and
send replies through a host-owned approval flow.

The server does not contain an AI agent and does not route work between agents.
The MCP host's agent or model supplies reasoning and invokes this server through
its MCP client. The human remains the approval authority; this server supplies a
bounded, typed Telegram interface over stdio.

```text
Human approval
        |
        v
Host agent / model
        |
        v
MCP client
        |
        | MCP over stdio
        v
telegram-personal-mcp ---- account-scoped attachment cache
        |                  prepared-send ledger + private file snapshots
        | MTProto
        v
     Telegram
```

## Live Codex Demo

[](docs/assets/telegram-personal-mcp-demo.mp4)

This recording runs a real `codex exec` session against
`telegram-personal-mcp --demo`. The terminal view renders the live Codex event
stream rather than replaying a prewritten tool transcript: the host agent finds
the contract message, reads its context, downloads the PDF, creates a writable
sibling, binds its exact SHA-256, replies to the source message, and verifies the
reply with another context read.

The deterministic fixture uses no Telegram credentials, network connection, or
real message delivery. The source [asciinema cast](docs/assets/telegram-personal-mcp-demo.cast)
and [reproducible renderer](scripts/render_terminal_demo.sh) are included in the
repository.

The implementation uses the official Rust MCP SDK, pinned to `rmcp 1.7.0`,
and targets the implemented stable MCP `2025-11-25`. The pin deliberately
avoids claiming or negotiating MCP `2026-07-28` before that contract is
implemented here. A regression test requests that version and verifies the
server falls back to `2025-11-25`. All 14 tools have input/output schemas, MCP
annotations, and a stable structured result envelope after argument
deserialization.

`v0.1.0` is the first public prerelease. Release evidence is deterministic
and credential-free: CI verifies the real binary, package, stdio transport, and
demo/fake workflow, while live Telegram authentication and delivery remain an
explicit prerelease boundary.

## Try It Without Telegram

Demo mode runs the real MCP binary against a deterministic local fixture. It
does not need Telegram credentials or a session and does not contact Telegram.

The fastest end-to-end proof launches the compiled binary as a child process,
connects an `rmcp` client over real OS stdio, finds the contract message, reads
its context, downloads the fixture PDF, creates a modified copy, and sends that
copy as a reply with an exact SHA-256 binding:

```bash
cargo test --test stdio_demo
```

The test also emits two compact proof lines. The hashes below are stable for
the checked-in fixture and show both the negotiated protocol and the exact
downloaded/replied bytes:

```text
protocol-proof requested=2026-07-28 negotiated=2025-11-25
demo-proof tools=14 message_id=11 downloaded_sha256=8a9e0a420df39699e56b1cc65a12505d8300394566f3d7be014bc20d62a4901c file_reply_sha256=909a3dbb487550bd8d00c5d769649fe98f0212f0d7b6124f2ff839c4132ae60f
```

To explore the same fixture from an MCP host:

```bash
cargo build --release
```

Register the resulting executable as a stdio MCP server with the single
argument `--demo`, using that host's documented configuration format. MCP host
configuration formats are not universal, so this README does not present one
host's syntax as a portable standard.

A useful human prompt for the host agent is:

> Find Alex's contract message, read the surrounding context, download the PDF,
> save a modified copy beside the original, then show me the exact recipient,
> reply target, caption, and SHA-256 before asking whether to send it.

`TELEGRAM_MCP_DEMO=true` is equivalent to `--demo`. Demo mode starts the stdio
server directly and cannot be combined with `doctor`, `session`, or `auth`
subcommands. Its tools can still write fixture downloads and prepared-send
state to the configured local cache/state paths.

## What It Enables

The main workflow is deliberately practical:

1. Find the right dialog and message.
2. Read the exact message plus a bounded window around it.
3. Inspect and download its attachment into a managed local cache.
4. Let the calling agent modify the file with its normal local tools.
5. After the MCP host obtains approval for the exact destination and payload,
   reply with the resulting file.

No Telegram message text is treated as an instruction by the server. There are
no deletion tools, no single-call unbounded history export, and no background
update feed.

## Engineering Depth

The project is designed around failure modes that a thin Telegram wrapper
usually leaves to its caller:

| Failure mode | Mechanism |
| --- | --- |
| A misspelled send field silently changes behavior | Closed input schemas reject every unknown field at the MCP boundary |
| A process dies around a text send | Prepared drafts persist state, Telegram `random_id`, payload hash, and final receipt across restarts |
| The configured session is replaced before dispatch | `send_prepared` revalidates the live Telegram account against the account bound at prepare time |
| A local file changes after approval | `send_file` snapshots exact bytes, verifies `approved_sha256`, then uploads the immutable snapshot |
| A caller selects an arbitrary or cross-account file | Upload paths must resolve inside the selected account's canonical cache root |
| An SDK advertises a protocol contract this server does not implement | The SDK generation and server version are pinned, with a raw-stdio negotiation regression test |

These mechanisms do not replace host-side human approval. They make the
deterministic boundary beneath that approval inspectable, recoverable, and hard
to call incorrectly.

## Demo Workflow

These are MCP tool calls made by the client, not shell commands:

```text
search_dialogs({ query: "Alex" })
  -> chat_ref: "@alex"

search_messages({ chat_id: "@alex", query: "contract.pdf", limit: 20 })
  -> message_id: 11

read_context({ chat_id: "@alex", message_id: 11, before: 5, after: 5 })
  -> target message plus nearby messages

list_attachments({ chat_id: "@alex", message_id: 11 })
  -> contract.pdf, application/pdf

download_attachment({ chat_id: "@alex", message_id: 11 })
  -> local_path: ".../attachments/2a1c.../7b9f.../-contract.pdf"
     bytes_written, sha256

# The downloaded original is immutable. The agent creates a new sibling file
# with tools outside this MCP, for example:
# ".../attachments/2a1c.../7b9f.../contract-updated.pdf"
# The host then obtains human approval for the exact chat, file, caption,
# reply target, delivery mode, and SHA-256.

send_file({
  chat_id: "@alex",
  file_path: ".../attachments/2a1c.../7b9f.../contract-updated.pdf",
  caption: "Updated contract",
  reply_to_message_id: 11,
  mode: "document",
  approved_sha256: ""
})
```

The in-process MCP integration test exercises the fuller shape, including
message search, context recovery, a PDF download, prepared text-send replay, a
file reply, and rejection of a file outside the managed upload root:

```bash
cargo test --test mcp_integration
```

## Tools

| Tool | Purpose | Important behavior |
| --- | --- | --- |
| `health` | Verify the selected Telegram account | Calls `get_me`; returns account metadata without exposing the local session path; does not read dialogs or messages |
| `list_dialogs` | List a bounded dialog slice | Default 30, maximum 100 |
| `search_dialogs` | Find dialogs by title, username, reference, or numeric ID | Preview-text matching is opt-in; scans at most 500 dialogs |
| `read_chat` | Read a bounded recent message page | Default 30, maximum 50; supports `offset_id` |
| `search_messages` | Search within one chat or across Telegram | In-chat search is pageable; global search is first-page-only |
| `get_message` | Fetch one exact message | Requires a chat reference and positive message ID |
| `read_context` | Read one message with nearby messages | Defaults to 5 before and 5 after; maximum 25 each |
| `list_attachments` | Inspect downloadable media on one message | Returns typed attachment metadata |
| `download_attachment` | Download message media to the managed cache | Streams into an opaque attachment directory, stops at the hard byte limit, removes partial files, and publishes an immutable content-addressed original with SHA-256 |
| `prepare_message` | Persist an immutable text-send draft | Binds account, chat, text, and optional reply target into a payload hash |
| `get_prepared` | Recover one prepared text draft | Returns the exact persisted payload and hash after a restart; may initialize or migrate the local ledger |
| `send_prepared` | Send a prepared text draft | Verifies the selected session is still the prepared account; repeated calls with the same `draft_id` are idempotent |
| `send_message` | Send text directly | Compatibility path; non-idempotent and not recommended for recoverable workflows |
| `send_file` | Upload and send a managed local file | Requires the exact approved SHA-256, captures a private immutable snapshot before upload, rejects cross-account/outside/symlink-escape paths, and remains non-idempotent |

`search_messages` returns `completeness` as `complete`, `page`, or
`first_page_only`. When `chat_id` is omitted, Ferogram does not expose the full
Telegram global-search cursor needed for reliable pagination, so the result is
intentionally labelled `first_page_only` and has no claimed next page. Use
dialog discovery followed by in-chat search when complete traversal matters.

### Canonical peer references

Every reference returned for later tool calls is round-trippable:

- positive numeric IDs identify users;
- negative numeric IDs identify basic groups;
- `-100...` IDs identify channels and supergroups;
- a dialog's `chat_ref` uses `@username` when one exists, otherwise its
  `canonical_chat_id`;
- message `chat_ref` and `sender_ref` fields use canonical numeric peer
  references.

The internal Ferogram dialog ID is not exposed through MCP. Tool inputs accept a
username or one of the canonical numeric forms above; phone numbers, Telegram
links, and invite links are rejected rather than guessed.

## Structured Results

Once arguments have deserialized, domain-level success and failure use the same
declared output schema:

```json
{
  "data": { "...": "tool-specific result" }
}
```

```json
{
  "error": {
    "code": "MESSAGE_NOT_FOUND",
    "message": "...",
    "retryable": false
  }
}
```

This lets an MCP client branch on typed domain error codes instead of parsing
prose. Malformed or unknown input fields are rejected by the MCP protocol
boundary itself and may be returned in the SDK's standard invalid-params error
shape rather than the tool outcome envelope.

## Quick Start

### 1. Requirements

- a current stable Rust toolchain;
- an MCP client that can launch a stdio server.

Demo mode needs nothing else. Real Telegram mode also needs Telegram `api_id`
and `api_hash` from the official
[`my.telegram.org` application page](https://my.telegram.org/apps);
`auth login` can create the authorized Ferogram `.session` file. Telegram
account signup and Desktop `tdata` import are not implemented.

### 2. Install

Install the current prerelease from its exact Git tag:

```bash
cargo install --locked \
  --git https://github.com/Stranmor/telegram-personal-mcp.git \
  --tag v0.1.0
```

The project is not published to crates.io. For a source checkout or local
development:

```bash
git clone https://github.com/Stranmor/telegram-personal-mcp.git
cd telegram-personal-mcp
cargo install --locked --path .
```

### 3. Create or select a Telegram session

Inject the API hash through a secret store or an ephemeral environment rather
than committing it to an MCP configuration file:

```bash
export TELEGRAM_API_ID=12345
read -rsp "Telegram API hash: " TELEGRAM_API_HASH; echo
export TELEGRAM_API_HASH
export TELEGRAM_MCP_SESSION_PATH="$HOME/.local/share/telegram-personal-mcp/account.session"
```

`.env.example` inventories the application-specific runtime settings, but the
binary does not automatically load `.env` files.

Create or refresh the selected session from an interactive terminal:

```bash
telegram-personal-mcp auth login
```

The command prompts for a phone number, requests a Telegram login code, and
handles optional two-step verification. The login code and 2FA password are read
without terminal echo and cannot be supplied through command arguments or a
pipe. On success it prints account metadata and the saved session path as JSON.

When no explicit path is set, `auth login` reuses the only session candidate or
creates `default.session` inside the resolved XDG data session directory when
the cache is empty. Multiple candidates remain fail-closed; select one explicitly
before retrying. An already-authorized selected session returns
`already_authorized` without requesting another code. On Unix, the session
parent directory must be owner-only; newly created parents use mode `0700` and
session/lock files use `0600`.

Inspect the resolved session without connecting to Telegram:

```bash
telegram-personal-mcp session list
```

Then verify the selected session and account with a real `get_me` call:

```bash
telegram-personal-mcp doctor
```

If more than one `.session` file is visible in the session cache, `doctor` and
Telegram tool calls fail with `AMBIGUOUS_SESSION` before opening an arbitrary
account. Select one explicitly with `TELEGRAM_MCP_SESSION_PATH` or
`--session-path`.

### 4. Register the stdio server

MCP client configuration formats are not universal. Configure your host to:

1. launch `telegram-personal-mcp` with no subcommand;
2. inherit `TELEGRAM_API_ID` and the optional session selection variables;
3. inject `TELEGRAM_API_HASH` from its secret store or process environment
   rather than embedding the value in repository or host configuration;
4. optionally inject `TELEGRAM_MCP_PROXY` through the environment when a
   SOCKS5 proxy is required.

Protocol messages use stdout; diagnostics use stderr. `TELEGRAM_API_HASH` and
`TELEGRAM_MCP_PROXY` intentionally have no command-line flags so secrets do
not appear in process arguments.

## Recommended Text-Send Flow

Use `prepare_message` followed by `send_prepared` instead of calling
`send_message` directly:

1. Call `prepare_message` with the exact chat, text, and optional reply target.
2. Present the returned account, chat, text, reply target, and `payload_sha256`
   to the user, then obtain approval for that exact payload and destination in
   the current session. If the process restarted, recover the same record with
   `get_prepared(draft_id)` instead of reconstructing it from chat memory.
3. Call `send_prepared` with its `draft_id` and the exact returned hash as
   `approved_payload_sha256`. A mismatch fails with
   `PAYLOAD_HASH_MISMATCH` before dispatch. If the session now belongs to a
   different Telegram account, dispatch fails with `ACCOUNT_MISMATCH`.
4. If the result is `DELIVERY_UNCERTAIN`, retry `send_prepared` with the same
   `draft_id` and `approved_payload_sha256`; do not create a new draft.

```text
prepare_message({ chat_id: "@alex", text: "Approved reply", reply_to_message_id: 418 })
  -> draft_id: "...", payload_sha256: "8d..."

get_prepared({ draft_id: "..." })
  -> exact persisted payload and payload_sha256: "8d..."

send_prepared({
  draft_id: "...",
  approved_payload_sha256: "8d..."
})
```

The SQLite ledger preserves the Telegram `random_id` and final receipt across
process restarts. Replaying a sent draft returns its recorded message ID. This
reduces duplica

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Stranmor](https://github.com/Stranmor)
- **Source:** [Stranmor/telegram-personal-mcp](https://github.com/Stranmor/telegram-personal-mcp)
- **License:** Apache-2.0

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-stranmor-telegram-personal-mcp
- Seller: https://agentstack.voostack.com/s/stranmor
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
