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>
8.7 KiB
8.7 KiB
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:
- Runs a Telegram bot in a group chat, logging all messages/files to libsql
- 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)
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:
credentialsfile:MCP_BOT_TOKEN=...,GROUP_CHAT_ID=...,HOMELAB_BOT_ID=...- MCP server binds to
0.0.0.0:8321(accessible via NetBird atmgmt.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)
- Create new Telegram bot via BotFather → get token
- Create Telegram group → add Mikkel + homelab bot + MCP bot
- Get group chat ID (bot will log it on first message)
- Fill in
credentialsfile systemctl --user enable --now mcp-bridge- Add MCP URL
http://mgmt.mg:8321/mcpin claude.ai settings
Future Extensions
- Multi-group support (per-project chats with
targetparameter) - Session tracking (detect agent restarts)
- File content search across attachments
- Message threading/reply chain reconstruction