Ida-Telegram
by L8teNever
README.md
# Ida-Telegram
Ein eigenständiger MCP-Server (Model Context Protocol), getrennt von
Ida-Untis und ohne Verbindung dazu. Zwei Dinge in einem Container:
1. Zwei MCP-Werkzeuge für Claude: einer fest konfigurierten Person eine
Telegram-Nachricht schicken (`nachricht_senden`), und lesen, was sie
gerade geschrieben hat (`neue_nachrichten_abrufen`) -- Fotos kommen dabei
als echter Bildinhalt mit, den die Routine wirklich "sehen" kann.
2. Ein Hintergrund-Loop, der neue Telegram-Nachrichten erkennt (Text, Fotos,
Sprachnachrichten) und dafür eine **claude.ai Routine** triggert -- die
Routine liest die Nachricht dann selbst über die MCP-Tools oben und
antwortet.
Läuft als Docker-Container und wird über einen bestehenden Cloudflare
Tunnel unter einer eigenen Domain erreichbar gemacht.
## Architektur
```
claude.ai Routine (Cloud-Agent)
| ^
(per API-Trigger) (MCP: nachricht_senden,
| neue_nachrichten_abrufen)
| |
| v
Claude (MCP-Client) --https--> Cloudflare Tunnel (öffentliche Domain)
|
v
127.0.0.1:4567 auf deinem Server
|
v
Docker-Container "ida-telegram-mcp"
^
| (long-polling, ausgehend)
Telegram Bot HTTP-API
^
Telegram-Nutzer schreibt dem Bot
```
Wichtig: **Dieser Container ruft selbst nie eine Claude-API auf.** Er macht
nur zwei Dinge -- Telegram per Long-Polling nach neuen Nachrichten fragen,
und bei neuen Nachrichten eine claude.ai Routine über deren eigenen
API-Trigger anstoßen. Die eigentliche "Intelligenz" (Nachricht lesen,
Antwort formulieren) läuft komplett in der Routine bei Anthropic, die sich
dafür ganz normal als MCP-Client mit diesem Server verbindet -- genau wie
Claude Code oder claude.ai es auch tun.
Der Container published seinen Port **nur auf `127.0.0.1`** -- von außen
nicht direkt erreichbar, nur über den bereits laufenden `cloudflared`-Prozess.
Zusätzlich verlangt der Server bei jeder Anfrage ein geheimes Token
(`MCP_AUTH_TOKEN`).
**Wichtigste Design-Entscheidung:** Es gibt keinen Empfänger-Parameter.
`TELEGRAM_CHAT_ID` in der `.env` legt die einzige Person fest, an die dieser
Server jemals schreiben kann -- weder Claude noch sonst jemand kann über die
Tools eine andere chat_id angeben. Eine Nachricht erreicht eine echte Person
und lässt sich nicht zurückholen, deshalb ist der Empfänger bewusst fest
verdrahtet statt frei wählbar.
## Voraussetzungen
- Docker + Docker Compose auf dem Server
- Ein bereits eingerichteter und verbundener Cloudflare Tunnel auf diesem Server
- Ein Telegram-Bot (in wenigen Minuten selbst erstellt, siehe unten)
- Ein claude.ai-Account, um die Routine anzulegen
## 1. Telegram-Bot erstellen
1. In Telegram den Chat mit **@BotFather** öffnen.
2. `/newbot` senden, Namen vergeben -- du bekommst einen Token wie
`123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`.
3. Die Person, die die Nachrichten empfangen soll, schreibt dem neuen Bot
einmal eine beliebige Nachricht (z.B. "hi"). **Wichtig:** Ein Bot kann
niemanden anschreiben, der ihm nicht vorher selbst geschrieben hat.
4. Im Browser aufrufen (mit echtem Token):
`https://api.telegram.org/bot<TOKEN>/getUpdates`
Im JSON nach `"chat":{"id": ...}` suchen -- das ist die `chat_id`.
## 2. Einrichten, bauen, starten
```bash
git clone https://github.com/<dein-user>/Ida-Telegram.git
cd Ida-Telegram
cp .env.example .env
```
`.env` erstmal mit `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID` und
`MCP_AUTH_TOKEN` ausfüllen (siehe Tabelle unten) -- `ROUTINE_ID`
und `ROUTINE_API_KEY` folgen in Schritt 5, dafür muss der Server erst
erreichbar sein.
Image bauen lassen: Bei jedem Push auf `main` baut
`.github/workflows/docker-publish.yml` das Image automatisch nach
`ghcr.io/<dein-user>/ida-telegram:latest`. Einmalig auf öffentlich stellen
(GitHub -> Profil -> **Packages** -> `ida-telegram` -> Package settings ->
Change visibility -> Public), damit `docker compose` es ohne Login ziehen
kann.
```bash
docker compose pull
docker compose up -d
docker compose logs -f
```
(Mit `AUTOREPLY_ENABLED=true` als Standard startet der Container zunächst
mit Fehler, weil `ROUTINE_ID`/`ROUTINE_API_KEY` noch fehlen --
das ist normal, kommt in Schritt 5. Alternativ jetzt schon `AUTOREPLY_ENABLED=false`
setzen und später wieder auf `true`.)
## 3. An den bestehenden Cloudflare Tunnel anbinden
Analog zu Ida-Untis, nur mit eigenem Hostname und Port 4567:
```yaml
ingress:
- hostname: telegram.deine-domain.de
service: http://localhost:4567
- service: http_status:404
```
(Bzw. im Zero-Trust-Dashboard unter Public Hostname eintragen.) Danach
`cloudflared` neu laden.
## 4. Als claude.ai Connector hinzufügen
Genau wie bei Ida-Untis: claude.ai -> Einstellungen -> Connectors -> Add
custom connector -> als URL
`https://telegram.deine-domain.de/mcp?token=<MCP_AUTH_TOKEN>` eintragen.
## 5. Routine anlegen (der eigentliche Auto-Antwort-Teil)
1. Auf [claude.ai/code/routines](https://claude.ai/code/routines) -> **Neue
Routine**.
2. **Name:** z.B. "Ida Telegram Autoreply".
3. **Anweisungen:** (setzt voraus, dass die Routine sowohl den
Ida-Telegram- als auch den [Ida-Memory](https://github.com/L8teNever/Ida-Memory)-Connector
hat, siehe Schritt 5)
> Du bist der Telegram-Assistent fuer die eine fest konfigurierte Person.
>
> 1. Neue Nachricht lesen: Ruf ueber den Ida-Telegram-Connector
> `neue_nachrichten_abrufen` auf. Das koennen Text, Fotos (die du dir
> direkt anschauen kannst) oder Hinweise auf Sprachnachrichten sein.
> Liefert das Tool nichts, mach nichts und brich ab.
>
> 2. Kontext holen: Ruf zusaetzlich `chat_verlauf` auf, um die letzten
> Nachrichten (beide Richtungen) zu sehen -- hilft z.B. zu verstehen,
> worauf sich eine kurze Nachricht bezieht.
>
> 3. Gedaechtnis pruefen, BEVOR du antwortest -- aber nur wenn noetig:
> Wenn du die Antwort schon aus der Nachricht selbst oder aus
> `chat_verlauf` kennst, ueberspring diesen Schritt. Sonst nutze den
> Ida-Memory-Connector: `search_nodes` mit den wichtigsten Stichworten aus
> der Nachricht (Namen, Projekte, Themen). Bei relevanten Treffern mit
> `open_nodes` die genauen Eintraege nachladen. Kein `read_graph`
> benutzen -- das laedt unnoetig den kompletten Bestand. Findest du nichts
> und weisst es auch sonst nicht -- sag ehrlich, dass du es nicht weisst,
> statt etwas zu erfinden.
>
> 4. Antworten: Kurze, freundliche, hilfreiche Antwort auf Deutsch ueber
> `nachricht_senden`. Relevante Infos aus Schritt 2/3 einbauen, wenn sie
> zur Nachricht passen. Bei Sprachnachrichten zuerst
> `sprachnachricht_transkribieren(voice_id)` mit der id aus
> `neue_nachrichten_abrufen` aufrufen, um den Inhalt zu verstehen.
>
> 5. Essensplan-Sonderfall: Wenn ein Foto ein Essensplan fuer die Woche
> ist, merke dir NUR das Mittagessen pro Tag (z.B. als Entity "Essensplan
> KW<Nummer>" mit einer observation pro Tag, "Montag: Spaghetti
> Bolognese"). Fruehstueck und Abendessen auf demselben Bild ignorierst
> du, auch wenn sie draufstehen. Wird spaeter nach dem Essen an einem
> bestimmten Tag gefragt: wie in Schritt 3 im Gedaechtnis nachschauen.
>
> 6. Gedaechtnis aktualisieren, NACH der Antwort: Enthaelt die Nachricht
> sonst einen dauerhaft nuetzlichen Fakt (Vorliebe, laufendes Projekt,
> wiederkehrende Info, Korrektur zu etwas Gespeichertem) -- ueber
> Ida-Memory speichern: `create_entities` nur fuer neue Personen/Projekte/
> Themen, `add_observations` fuer neue Fakten zu bestehenden Entities
> (nur an die, zu der sie wirklich gehoeren), `create_relations` fuer
> dauerhafte Zusammenhaenge. NICHT jede Kleinigkeit speichern -- im
> Zweifel lieber nichts speichern als zu viel.
4. **Trigger:** "API" auswählen (nicht Zeitplan). claude.ai zeigt dir danach
einmalig einen **API-Token** an (`sk-ant-oat01-...`) -- sofort notieren,
er wird danach nicht mehr im Klartext angezeigt.
5. Bei **Konnektoren** den gerade hinzugefügten `Ida-Telegram`-Connector
auswählen.
6. Routine speichern.
7. Die Routine-ID aus der URL ablesen, die claude.ai beim Bearbeiten der
Routine anzeigt (`claude.ai/code/routines/trig_...` -- der Teil ab `trig_`
ist die ID), und zusammen mit dem Token in `.env` eintragen:
```bash
ROUTINE_ID=<trig_...>
ROUTINE_API_KEY=<der-notierte-api-token>
```
Technischer Hintergrund: der Server ruft dafür
`POST https://api.anthropic.com/v1/claude_code/routines/<ROUTINE_ID>/fire`
auf (offizieller Endpunkt für Routinen-Trigger, siehe
[Doku](https://platform.claude.com/docs/en/api/claude-code/routines-fire)) --
`ROUTINE_ID` und `ROUTINE_API_KEY` sind alles, was dafür gebraucht wird.
8. Neu starten: `docker compose up -d`
Ab jetzt: schreibt die konfigurierte Person dem Telegram-Bot, triggert der
Container die Routine, die Routine liest die Nachricht über MCP und
antwortet über MCP zurück auf Telegram.
## Verfügbare MCP-Tools
| Tool | Zweck |
|---|---|
| `nachricht_senden(text)` | Schickt `text` an die fest konfigurierte Person. Stoppt dabei automatisch die "tippt..."-Anzeige und trägt die Antwort in `chat_verlauf` ein |
| `neue_nachrichten_abrufen()` | Gibt zurück, was den aktuellen Routine-Lauf ausgelöst hat (jeweils nur einmal): Text als String, Fotos als echten Bildinhalt, Bildunterschriften als eigener Text, Sprachnachrichten als Hinweistext mit `voice_id` |
| `sprachnachricht_transkribieren(voice_id)` | Transkribiert eine zwischengespeicherte Sprachnachricht zu Text -- läuft lokal in diesem Container (faster-whisper), keine Audiodaten verlassen die eigene Infrastruktur |
| `chat_verlauf()` | Letzte `CHAT_HISTORY_LENGTH` Nachrichten (Standard 5, beide Richtungen) als leichtgewichtiger Text -- fürs Gesprächsgedächtnis über den aktuellen Lauf hinaus. Nicht destruktiv, beliebig oft abrufbar. Fotos nur als `[Foto]`-Platzhalter, keine Bilddaten |
| `bot_status()` | Prüft nur, ob Token/Bot erreichbar sind (sendet nichts) |
Während ein Routine-Lauf auf eine Antwort wartet, zeigt der Bot in Telegram
automatisch "tippt..." an (aktualisiert alle 4s, damit es nicht ausblendet) --
kein eigenes Tool dafür nötig, das läuft im Hintergrund mit.
Persistentes Gedächtnis (über einzelne Routine-Läufe hinweg, gemeinsam
nutzbar von mehreren KIs/Connectors) liegt bewusst **nicht** hier, sondern
im separaten [Ida-Memory](https://github.com/L8teNever/Ida-Memory)-Projekt
-- der Routine dafür zusätzlich diesen Connector geben.
**Unterstützte Nachrichtentypen:**
| Typ | Verhalten |
|---|---|
| Text (auch formatiert, z.B. **fett**) | Wird 1:1 als Text an die Routine weitergegeben |
| Foto | Wird heruntergeladen und als echter Bildinhalt weitergegeben -- die Routine kann es tatsächlich "sehen" (Claude-Vision über MCP-Bildinhalte) |
| Sprachnachricht | Wird heruntergeladen und zwischengespeichert (Hinweis mit `voice_id`). Keine automatische Transkription -- die Routine ruft bei Bedarf gezielt `sprachnachricht_transkribieren(voice_id)` auf (lokal per Whisper, siehe unten) |
| Sticker, Videos, Dokumente | Werden aktuell ignoriert |
## Lokale Sprachnachrichten-Transkription (Whisper)
`sprachnachricht_transkribieren` läuft **direkt in diesem Container** --
kein externer Dienst, keine Audiodaten verlassen die eigene Infrastruktur.
Technisch: [faster-whisper](https://github.com/SYSTRAN/faster-whisper)
(CTranslate2-Engine statt des originalen openai-whisper/PyTorch-Stacks --
deutlich schnellere und speicherschonendere CPU-Inferenz, wichtig auf einem
VPS ohne GPU).
- **Auf Abruf, nicht automatisch:** Eine Sprachnachricht wird beim Empfang
nur heruntergeladen und mit einer `voice_id` zwischengespeichert (die
letzten 20, älteste fällt raus). Transkribiert wird erst, wenn die Routine
das Tool tatsächlich aufruft -- spart Rechenzeit für Sprachnachrichten,
die z.B. gar nicht beantwortet werden.
- **Modell wird lazy geladen:** Nicht beim Containerstart, sondern beim
ersten tatsächlichen Transkriptions-Aufruf -- der ist dadurch spürbar
langsamer (Download + Laden), jeder weitere Aufruf nutzt das bereits
geladene Modell und ist deutlich schneller.
- **Ressourcen:** `WHISPER_MODEL=base` (Standard) ist ein Kompromiss aus
Geschwindigkeit/Genauigkeit für eine CPU-VPS ohne GPU. Bei sehr
begrenztem RAM `tiny` probieren, bei Bedarf an Genauigkeit `small`. Das
Modell wird nach dem ersten Download im `/data`-Volume gecacht.
- **Bekanntes Whisper-Verhalten:** Auf sehr kurzen/leisen/inhaltsleeren
Aufnahmen "halluziniert" das Modell manchmal plausibel klingenden, aber
falschen Text (ein dokumentiertes Verhalten aller Whisper-Modelle, kein
Bug dieses Servers) -- bei zweifelhaften Ergebnissen im Zweifel nachfragen.
## Wie der Auto-Antwort-Loop funktioniert
Wenn `AUTOREPLY_ENABLED=true` (Standard) läuft im Container ein
Hintergrund-Thread:
1. Fragt Telegram per Long-Polling nach neuen Nachrichten der konfigurierten
Person (`TELEGRAM_CHAT_ID`) -- Nachrichten von anderen werden ignoriert.
2. Kommen mehrere Nachrichten schnell hintereinander, wartet der Server
`AUTOREPLY_DEBOUNCE_SECONDS` auf weiteren Nachschub und bündelt alles --
die Routine wird dann **einmal** getriggert statt einmal pro Nachricht.
3. Schickt einen `POST` mit `Authorization: Bearer $ROUTINE_API_KEY` an den
Routinen-Endpunkt (`ROUTINE_ID` in der URL) -- der gebündelte Text geht als
`text`-Feld direkt mit (sofortiger Kontext für die Routine), zusätzlich
liefert `neue_nachrichten_abrufen` denselben Text noch einmal ab, falls
die Routine ihn lieber über MCP nachlesen will.
Kein Doppelt-Antworten: Telegrams `getUpdates`-Offset-Mechanismus sorgt von
selbst dafür, dass jede Nachricht genau einmal in den Zwischenspeicher
wandert, auch nach einem Neustart des Containers; `neue_nachrichten_abrufen`
liefert jede Nachricht ebenfalls nur einmal aus.
**Kosten:** Jeder Routine-Lauf verbraucht claude.ai-Nutzung auf deinem
Account (Cloud-Agent-Sitzung), nicht eine separate API-Rechnung.
## Lokal testen ohne Cloudflare
```bash
docker compose up -d
curl -H "Authorization: Bearer $MCP_AUTH_TOKEN" http://127.0.0.1:4567/healthz
```
## Troubleshooting
- **Container startet nicht**: `docker compose logs` -- meist fehlt eine
Pflicht-Variable in `.env` (z.B. `ROUTINE_ID`/`ROUTINE_API_KEY`
fehlen, obwohl `AUTOREPLY_ENABLED=true` ist).
- **`Telegram-API-Fehler: chat not found`**: Die Zielperson hat dem Bot noch
nie geschrieben (siehe Schritt 1.3), oder die `chat_id` ist falsch.
- **`Telegram-API-Fehler: Unauthorized`**: `TELEGRAM_BOT_TOKEN` falsch/abgelaufen.
- **Claude bekommt 401**: Token in Client-Konfiguration und `.env` vergleichen.
- **Routine wird nicht getriggert**: `docker compose logs -f` prüfen -- Zeile
"Telegram-Autoreply-Loop gestartet" sollte beim Start erscheinen, und bei
neuer Nachricht "Neue Nachricht(en) erhalten, triggere Routine...". Bei
einem HTTP-Fehler danach: `ROUTINE_ID`/`ROUTINE_API_KEY` prüfen (401 =
Token falsch/gehört nicht zu dieser Routine, 404 = `ROUTINE_ID` falsch).
- **Routine läuft, antwortet aber nicht**: In claude.ai unter Routinen die
letzte Sitzung öffnen und den Verlauf prüfen -- meist fehlt der
Ida-Telegram-Connector bei den Konnektoren der Routine, oder
`neue_nachrichten_abrufen` liefert eine leere Liste (Race Condition sehr
unwahrscheinlich, aber möglich bei extrem kurzem `AUTOREPLY_DEBOUNCE_SECONDS`).
- **`Conflict: terminated by other getUpdates request`**: Der Bot-Token wird
gleichzeitig noch woanders per `getUpdates` abgefragt (oder es ist ein
Webhook für den Bot gesetzt) -- ein Telegram-Bot-Token kann immer nur von
einem Prozess gleichzeitig per Long-Polling abgefragt werden.
- **`sprachnachricht_transkribieren` dauert beim ersten Aufruf sehr lange**:
normal -- das Modell wird dann erst heruntergeladen/geladen. Danach
deutlich schneller. Bei dauerhaft sehr langsamer Transkription ein
kleineres `WHISPER_MODEL` (z.B. `tiny`) probieren.
- **"Keine zwischengespeicherte Sprachnachricht ... gefunden"**: entweder
eine falsche/erfundene `voice_id`, oder es sind seither mehr als 20 neue
Sprachnachrichten eingegangen (älteste fallen aus dem Zwischenspeicher).
- **Container braucht spürbar mehr RAM als vorher**: durch
`faster-whisper` + geladenes Modell erwartet -- bei sehr begrenztem RAM
`WHISPER_MODEL=tiny` setzen oder `WHISPER_ENABLED=false`.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues