Skip to main content
Glama
L8teNever

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`.