Single-process Python app that: - Runs a Telegram bot in a group chat, logging all messages/files to libsql - Exposes send_message, pull_updates, queue_status MCP tools over HTTP - Downloads and stores file attachments with Telegram file_id + local path - Accessible via NetBird mesh at mgmt.mg:8321 (no auth needed) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
237 lines
8.7 KiB
Markdown
237 lines
8.7 KiB
Markdown
# Nexus MCP Bridge — Design Spec
|
|
|
|
**Date:** 2026-03-30
|
|
**Status:** Approved (user greenlit Approach 1, all design questions resolved)
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
A single-process Python application that:
|
|
1. Runs a **Telegram bot** in a group chat, logging all messages/files to **libsql**
|
|
2. Exposes an **MCP server** (FastMCP over HTTP) on the NetBird mesh for claude.ai to query
|
|
|
|
This lets claude.ai dispatch tasks to the homelab agent and pull conversation updates — without Mikkel being the copy-paste middleman.
|
|
|
|
---
|
|
|
|
## Design Decisions
|
|
|
|
| Question | Decision | Rationale |
|
|
|----------|----------|-----------|
|
|
| Telegram conflict | New bot + group chat | Each bot has own token, no polling conflict |
|
|
| What to capture | Everything (all participants + files) | Full replay capability |
|
|
| File storage | Download to disk + store file_id | Durability + convenience |
|
|
| Auth | None (NetBird mesh is trust boundary) | Only accessible from enrolled peers |
|
|
| Database | libsql embedded (local file) | Single process, no extra service |
|
|
| Scope | Single group chat (homelab) for v1 | Nail core loop, extend later |
|
|
| Bot personality | Attributed relay `[claude.ai] ...` | Clear attribution in group |
|
|
| Architecture | Single process (FastMCP + telegram bot) | Simple, systemd restart covers failures |
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌──────────────┐ HTTP (NetBird) ┌─────────────────────────────┐
|
|
│ claude.ai │ ◄──────────────────── │ MCP Bridge Process │
|
|
│ MCP client │ ────────────────────► │ │
|
|
└──────────────┘ │ ┌─────────┐ ┌──────────┐ │
|
|
│ │ FastMCP │ │ Telegram │ │
|
|
│ │ HTTP │ │ Bot │ │
|
|
│ │ Server │ │ (polling)│ │
|
|
│ └────┬─────┘ └────┬─────┘ │
|
|
│ │ │ │
|
|
│ └──────┬───────┘ │
|
|
│ │ │
|
|
│ ┌─────▼─────┐ │
|
|
│ │ libsql │ │
|
|
│ │ (embed) │ │
|
|
│ └───────────┘ │
|
|
│ │
|
|
│ media/ (downloaded files) │
|
|
└─────────────────────────────┘
|
|
│
|
|
Telegram Group Chat
|
|
┌─────▼─────┐
|
|
│ Mikkel │
|
|
│ Homelab ♦ │ (existing bot)
|
|
│ MCP ♦ │ (new bot)
|
|
└───────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Database Schema (libsql)
|
|
|
|
```sql
|
|
CREATE TABLE messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
telegram_message_id INTEGER NOT NULL,
|
|
chat_id INTEGER NOT NULL,
|
|
sender_type TEXT NOT NULL, -- 'user', 'homelab_bot', 'mcp_bot', 'unknown'
|
|
sender_id INTEGER,
|
|
sender_name TEXT,
|
|
content TEXT, -- message text (nullable for media-only)
|
|
reply_to_message_id INTEGER, -- telegram reply reference
|
|
has_attachment INTEGER DEFAULT 0,
|
|
created_at TEXT NOT NULL, -- ISO 8601
|
|
UNIQUE(chat_id, telegram_message_id)
|
|
);
|
|
|
|
CREATE TABLE attachments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
message_id INTEGER NOT NULL REFERENCES messages(id),
|
|
file_type TEXT NOT NULL, -- 'photo', 'document', 'video', 'voice', 'sticker'
|
|
file_id TEXT NOT NULL, -- telegram file_id
|
|
file_unique_id TEXT NOT NULL, -- telegram file_unique_id
|
|
file_name TEXT, -- original filename
|
|
mime_type TEXT,
|
|
file_size INTEGER,
|
|
local_path TEXT, -- path under media/
|
|
caption TEXT,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE outbound_queue (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
chat_id INTEGER NOT NULL,
|
|
content TEXT NOT NULL,
|
|
attribution TEXT DEFAULT 'claude.ai', -- prefix for the message
|
|
status TEXT DEFAULT 'pending', -- 'pending', 'sent', 'failed'
|
|
created_at TEXT NOT NULL,
|
|
sent_at TEXT
|
|
);
|
|
|
|
-- Index for delta queries
|
|
CREATE INDEX idx_messages_created_at ON messages(created_at);
|
|
CREATE INDEX idx_messages_chat_id ON messages(chat_id);
|
|
CREATE INDEX idx_outbound_status ON outbound_queue(status);
|
|
```
|
|
|
|
---
|
|
|
|
## MCP Tools
|
|
|
|
### send_message
|
|
```
|
|
Send a message to the homelab group chat, attributed as [claude.ai].
|
|
|
|
Input: { "message": "Fix the nexus.mg DNS record to 100.79.65.206" }
|
|
Output: { "sent": true, "id": 42 }
|
|
```
|
|
|
|
### pull_updates
|
|
```
|
|
Get conversation messages since a cursor (message ID or timestamp).
|
|
Returns messages from all participants with attachment metadata.
|
|
|
|
Input: { "since_id": 150 } or { "since": "2026-03-30T01:00:00Z" } or {}
|
|
Output: {
|
|
"messages": [
|
|
{
|
|
"id": 151,
|
|
"sender": "mikkel",
|
|
"sender_type": "user",
|
|
"content": "Can you check the DNS?",
|
|
"attachments": [],
|
|
"created_at": "2026-03-30T01:02:15Z"
|
|
},
|
|
{
|
|
"id": 152,
|
|
"sender": "homelab_bot",
|
|
"sender_type": "homelab_bot",
|
|
"content": "Checking Technitium... record found.",
|
|
"attachments": [{"file_type": "photo", "file_name": "dns-screenshot.png"}],
|
|
"created_at": "2026-03-30T01:02:45Z"
|
|
}
|
|
],
|
|
"cursor": 152
|
|
}
|
|
```
|
|
|
|
### queue_status
|
|
```
|
|
Current state summary.
|
|
|
|
Input: {}
|
|
Output: {
|
|
"total_messages": 152,
|
|
"last_message_at": "2026-03-30T01:02:45Z",
|
|
"last_sender": "homelab_bot",
|
|
"pending_outbound": 0
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Telegram Bot Behavior
|
|
|
|
- **Joins group chat** with Mikkel + existing homelab bot
|
|
- **Logs everything**: text, photos, documents, voice, stickers, replies
|
|
- **Downloads attachments** to `media/<YYYY-MM-DD>/<file_unique_id>_<filename>`
|
|
- **Sends outbound** messages prefixed with `[claude.ai]` in bold
|
|
- **No commands** — this bot is a silent logger + relay, not interactive
|
|
- **Ignores private messages** — only operates in the configured group
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
```
|
|
~/repos/telegram-bot-mcp/
|
|
├── docs/ # Specs and design docs
|
|
├── mcp_bridge/
|
|
│ ├── __init__.py
|
|
│ ├── __main__.py # Entry point: runs both bot + MCP server
|
|
│ ├── config.py # Configuration (env vars, paths)
|
|
│ ├── db.py # libsql database layer
|
|
│ ├── telegram_bot.py # Telegram bot (polling, logging, sending)
|
|
│ ├── mcp_server.py # FastMCP tool definitions
|
|
│ └── models.py # Shared data models
|
|
├── media/ # Downloaded attachments
|
|
├── data/ # libsql database file
|
|
├── credentials # BOT_TOKEN, GROUP_CHAT_ID (created during setup)
|
|
├── requirements.txt
|
|
├── heartbeat.sh
|
|
└── README.md
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
Environment/file based:
|
|
- `credentials` file: `MCP_BOT_TOKEN=...`, `GROUP_CHAT_ID=...`, `HOMELAB_BOT_ID=...`
|
|
- MCP server binds to `0.0.0.0:8321` (accessible via NetBird at `mgmt.mg:8321`)
|
|
- Database at `data/bridge.db`
|
|
- Media at `media/`
|
|
|
|
---
|
|
|
|
## Deployment
|
|
|
|
- **systemd user service** (`mcp-bridge.service`)
|
|
- Uses project-local venv at `.venv/`
|
|
- `Restart=always`, `RestartSec=5`
|
|
- Bind to `0.0.0.0:8321` (NetBird interface)
|
|
|
|
---
|
|
|
|
## Setup Steps (for user)
|
|
|
|
1. Create new Telegram bot via BotFather → get token
|
|
2. Create Telegram group → add Mikkel + homelab bot + MCP bot
|
|
3. Get group chat ID (bot will log it on first message)
|
|
4. Fill in `credentials` file
|
|
5. `systemctl --user enable --now mcp-bridge`
|
|
6. Add MCP URL `http://mgmt.mg:8321/mcp` in claude.ai settings
|
|
|
|
---
|
|
|
|
## Future Extensions
|
|
|
|
- Multi-group support (per-project chats with `target` parameter)
|
|
- Session tracking (detect agent restarts)
|
|
- File content search across attachments
|
|
- Message threading/reply chain reconstruction
|