TeleMCP
README.md
# TeleMCP
Telegram intelligence and action layer for the Model Context Protocol.
TeleMCP turns Telegram conversations, groups, channels, files and links into a
searchable, auditable knowledge layer that an AI agent can query — and, with a
human in the loop, act on.
- **Telegram Bot API** (webhook) and **MTProto** personal mode, cleanly separated
- **40 MCP tools**, 7 resources, 4 prompts
- **Hybrid search**: PostgreSQL full text + trigram + `pgvector` semantic search
- **Inbox AI**: remember a message, get it back by meaning months later
- **Human approval** for everything that writes; reinforced approval for deletes
- **Full audit trail**, per-chat policy, deny by default, multi-tenant isolation
- **Fly.io** for compute, **Neon PostgreSQL** for durable state
## How it behaves
Three rules explain most of the design:
1. **Reading is free, writing is not.** Every WRITE tool returns
`APPROVAL_REQUIRED` on its first call, with an approval id. A human approves;
the agent calls again with the same arguments plus `approval_id`. Changing a
single character invalidates the approval. Approvals are single-use and
expire in minutes.
2. **Chats are deny-by-default.** Connecting an account indexes nothing. A chat
is read only after it is explicitly allowlisted.
3. **Message content is data, never instruction.** Retrieved text is fenced
before it reaches a model and flagged when it looks like prompt injection.
The policy engine never reads content at all, which is what makes the
defence real rather than advisory.
## Quick start
```bash
git clone <your-fork> && cd telemcp
cp .env.example .env # then fill it in — see "Configuration"
uv sync # or: pip install -e ".[dev,files]"
```
Bring up PostgreSQL with the required extensions:
```bash
docker run -d --name telemcp-db \
-e POSTGRES_USER=telemcp -e POSTGRES_PASSWORD=telemcp -e POSTGRES_DB=telemcp \
-p 5432:5432 pgvector/pgvector:pg16
```
Apply the schema and start the two processes:
```bash
python scripts/migrate.py upgrade
uvicorn app.main:app --host 0.0.0.0 --port 8080 # web
python -m app.jobs.worker # worker
```
Check it is alive:
```bash
curl localhost:8080/healthz # {"status":"ok"}
curl localhost:8080/readyz # database, extensions, migration revision
```
### Connect a Telegram bot
1. Create the bot with `@BotFather` and copy its token into `TELEGRAM_BOT_TOKEN`.
2. Generate a webhook secret and set `TELEGRAM_WEBHOOK_SECRET` plus
`TELEGRAM_WEBHOOK_BASE_URL` (your public HTTPS base URL).
3. Register the webhook:
```bash
python scripts/create_bot_webhook.py
```
4. Send `/start` to the bot. It replies with your Telegram user id; put it in
`TELEGRAM_OWNER_IDS` and restart. Ownership is declared, never inferred —
otherwise anyone in a group could change what gets indexed.
5. Add the bot to a chat and send `/allowchat`. Nothing is indexed before that.
`/privacy` shows exactly what is stored.
### Connect the MCP endpoint
The endpoint is `https://<host>/mcp` (Streamable HTTP) and accepts two kinds of
credential, because clients differ in what they can send.
**A shared bearer token**, for clients that support custom headers — Claude
Code among them:
```bash
claude mcp add --transport http telemcp https://telemcp.fly.dev/mcp \
--header "Authorization: Bearer $MCP_AUTH_SECRET"
```
**OAuth 2.1**, for clients that only speak OAuth — ChatGPT's custom connectors
among them. Add the server URL in the client; it discovers the authorization
server, registers itself, and sends you to a consent page. Approve it there
with your `MCP_AUTH_SECRET` and the client receives a token of its own.
Both paths land on the same policy engine: an OAuth client is constrained by
the scopes it was granted, and every write still needs human approval.
### Personal mode (optional)
Personal mode connects *your* Telegram account over MTProto so history can be
backfilled. It never bypasses Telegram's permissions — it sees only what your
account can already see, and only in allowlisted chats.
```bash
python scripts/create_personal_session.py # prompts for phone, code, 2FA
```
The session string is sealed with AES-256-GCM under `SESSION_ENCRYPTION_KEY`
before it touches the database, is never logged, and can be destroyed at any
time with the `telegram_revoke_personal_session` tool.
## The approval flow, concretely
```text
agent: telegram_send_message(chat_id, text)
↓
{"ok": false, "error": "APPROVAL_REQUIRED",
"approval": {"id": "…", "summary": "send message · chat -100… · “…”"}}
↓
human: POST /approvals/{id}/approve
↓
agent: telegram_send_message(chat_id, text, approval_id="…")
↓
{"ok": true, "data": {"message_id": "…", "sent_at": "…"}}
```
`GET /approvals` lists what is waiting. Every step is audited.
## Tool catalogue
| Group | Tools |
|---|---|
| Messages | `send_message`, `reply_message`, `get_message`, `get_recent_messages`, `search_messages`, `search_semantic`, `search`, `get_context`, `forward_message`, `delete_message` |
| Chats | `get_chat`, `list_known_chats`, `get_group_messages`, `get_channel_posts`, `get_thread` |
| Files | `download_file`, `send_file`, `send_photo`, `list_files`, `find_documents` |
| Updates / Polls | `get_updates`, `create_poll` |
| Intelligence | `summarize_chat`, `summarize_channel`, `summarize_thread`, `find_links`, `find_mentions`, `find_decisions`, `extract_tasks`, `extract_people`, `extract_topics` |
| Inbox AI | `remember_message`, `save_item`, `list_saved_items`, `search_saved_items`, `forget_item` |
| Admin | `sync_chat`, `reindex_chat`, `get_sync_status`, `revoke_personal_session` |
All names are prefixed `telegram_`. Approval requirements are listed in
[SECURITY.md](SECURITY.md).
Resources: `telegram://chat/{id}`, `…/recent`, `telegram://message/{chat}/{id}`,
`telegram://thread/{chat}/{id}`, `telegram://channel/{id}`,
`telegram://file/{id}/metadata`, `telegram://saved/{id}`.
Prompts: `telegram_chat_digest`, `telegram_decision_review`,
`telegram_action_items`, `telegram_research_digest`.
## Configuration
Everything is environment-driven; `.env.example` documents every key. The ones
that matter most:
| Variable | Meaning |
|---|---|
| `DATABASE_URL` | Neon **pooled** connection, used by web and worker |
| `DATABASE_URL_DIRECT` | Neon **direct** connection, used by migrations |
| `MCP_AUTH_SECRET` | Bearer token for `/mcp` and `/approvals` (≥32 chars) |
| `TELEGRAM_WEBHOOK_SECRET` | Verified on every webhook call |
| `SESSION_ENCRYPTION_KEY` | base64 32 bytes; seals MTProto sessions |
| `CHAT_DEFAULT_POLICY` | `deny` (keep it) |
| `MCP_ALLOWED_HOSTS` | Hostnames the MCP endpoint answers to; required in production |
| `OAUTH_ISSUER` | Public HTTPS base URL, used by OAuth discovery and consent |
| `TELEGRAM_OWNER_IDS` | Telegram ids allowed to run `/allowchat` |
| `EMBEDDING_PROVIDER` | `hash` (local, no key) or `openai` |
| `LLM_PROVIDER` | `heuristic` (local, no key) or `openai` |
### Running without any API keys
`EMBEDDING_PROVIDER=hash` and `LLM_PROVIDER=heuristic` are the defaults, and
they are not stubs: hashed embeddings give a real, deterministic vector space,
and the intelligence tools fall back to deterministic extraction. Every tool
works end to end with no external AI provider. Each summary reports which path
produced it in its `engine` field. Switch to `openai` when you want
model-quality semantics.
## Deploying to Fly.io
```bash
fly launch --no-deploy
fly secrets set \
DATABASE_URL="…" DATABASE_URL_DIRECT="…" \
MCP_AUTH_SECRET="…" TELEGRAM_BOT_TOKEN="…" \
TELEGRAM_WEBHOOK_SECRET="…" SESSION_ENCRYPTION_KEY="…"
fly deploy
fly scale count web=1 worker=1
fly status && fly checks list
```
`fly.toml` runs migrations as the release command, so a deploy cannot serve
traffic against an unmigrated schema. The worker is only required for
embeddings, enrichment and personal mode — but without it, semantic search
stays empty.
### Neon setup
1. Create the project and a `telemcp` database. Pick a region near your Fly
region — `gru` pairs with São Paulo. Leave **Neon Auth off**: TeleMCP
authenticates through its own bearer credential and provisions its own
users, so Neon Auth would add unused tables.
2. Copy both connection strings (pooled and direct).
3. `CREATE EXTENSION vector; CREATE EXTENSION pg_trgm; CREATE EXTENSION pgcrypto;`
4. `python scripts/migrate.py upgrade`
Verified against PostgreSQL 16. Newer majors work provided `pgvector` is
available; if `CREATE EXTENSION vector` fails, recreate the project on 17.
Connect the application as a **non-superuser** role. Superusers bypass row
level security, and RLS is what enforces tenant isolation at the database
level. See [SECURITY.md](SECURITY.md#row-level-security).
## Operating it
```bash
python scripts/manage_policies.py list # the allowlist
python scripts/manage_policies.py allow -100123456 # index a chat
python scripts/manage_policies.py set -100123456 --write # permit sending
python scripts/reindex.py # rebuild the index
```
Bot commands: `/start`, `/help`, `/status`, `/privacy`, `/remember`, `/search`,
`/recent`, `/summary`, `/decisions`, `/tasks`, `/saved`, and for the owner
`/allowchat`, `/denychat`, `/link`, `/unlink`, `/reindex`.
`/metrics` exposes Prometheus counters and histograms: tool calls and errors,
messages ingested, search latency, queue depth, approval requests.
## Architecture
```text
MCP client ──HTTP──▶ web process ──▶ Neon PostgreSQL ◀── worker process
├ MCP server ├ job runner
├ Telegram webhook ├ embeddings
└ approval API └ MTProto
```
Fly.io runs compute; Neon holds all durable state. The web process is
stateless, so it scales and restarts freely.
Every tool call takes exactly one path:
```text
authenticate → resolve principal → tool policy → scope → rate limit
→ chat policy → approval → execute → audit
```
A tool implementation never re-implements a check, and no tool can skip one.
## Development
```bash
pip install -e ".[dev,files]"
ruff check .
pytest tests/unit tests/contract tests/security -q # no services needed
docker run -d --name telemcp-test -p 5433:5432 \
-e POSTGRES_USER=telemcp -e POSTGRES_PASSWORD=telemcp \
-e POSTGRES_DB=telemcp_test pgvector/pgvector:pg16
export TEST_DATABASE_URL=postgresql+asyncpg://telemcp:telemcp@localhost:5433/telemcp_test
pytest -q # everything
```
262 tests: unit, contract (every tool's schema, class and annotations),
security (injection, SSRF, webhook spoofing, replay, tenant isolation) and
integration against a real PostgreSQL with pgvector.
## Project status
V0.1. See [STATUS.md](STATUS.md) for the Definition of Done, item by item, and
what is deliberately deferred.
## License
Apache-2.0.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues