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>
90 lines
2.6 KiB
Python
90 lines
2.6 KiB
Python
"""FastMCP server exposing bridge tools to claude.ai."""
|
|
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
from fastmcp import FastMCP
|
|
|
|
from .db import Database
|
|
from .config import get_group_chat_id
|
|
|
|
# Will be initialized in __main__ with shared db instance
|
|
db: Database | None = None
|
|
|
|
mcp = FastMCP(
|
|
name="homelab-bridge",
|
|
instructions=(
|
|
"This MCP server bridges claude.ai to a homelab Telegram group chat. "
|
|
"Use pull_updates to read conversation history (supports cursor-based pagination). "
|
|
"Use send_message to post messages to the group (attributed as [claude.ai]). "
|
|
"Use queue_status for a quick summary."
|
|
),
|
|
)
|
|
|
|
|
|
def init(database: Database):
|
|
"""Set the shared database instance."""
|
|
global db
|
|
db = database
|
|
|
|
|
|
@mcp.tool()
|
|
def send_message(message: str) -> str:
|
|
"""Send a message to the homelab Telegram group chat.
|
|
|
|
The message will be posted with [claude.ai] attribution so participants
|
|
know the message came from claude.ai.
|
|
|
|
Args:
|
|
message: The text to send to the group chat.
|
|
"""
|
|
chat_id = get_group_chat_id()
|
|
outbound_id = db.queue_outbound(chat_id, message)
|
|
return json.dumps({"sent": True, "id": outbound_id})
|
|
|
|
|
|
@mcp.tool()
|
|
def pull_updates(since_id: int = 0, since: str | None = None, limit: int = 50) -> str:
|
|
"""Pull conversation messages from the Telegram group.
|
|
|
|
Returns messages from all participants (Mikkel, homelab bot, MCP bot).
|
|
Supports cursor-based pagination: use the returned 'cursor' value as
|
|
'since_id' in the next call to get only new messages.
|
|
|
|
Args:
|
|
since_id: Return messages with id > this value. Use cursor from previous response.
|
|
since: ISO 8601 timestamp. Alternative to since_id — returns messages after this time.
|
|
limit: Maximum number of messages to return (default 50, max 200).
|
|
"""
|
|
limit = min(limit, 200)
|
|
|
|
if since:
|
|
messages = db.get_messages_since_timestamp(since, limit)
|
|
else:
|
|
messages = db.get_messages_since_id(since_id, limit)
|
|
|
|
# Enrich with attachment info
|
|
for msg in messages:
|
|
if msg["has_attachment"]:
|
|
msg["attachments"] = db.get_attachments_for_message(msg["id"])
|
|
else:
|
|
msg["attachments"] = []
|
|
del msg["has_attachment"]
|
|
|
|
cursor = messages[-1]["id"] if messages else since_id
|
|
|
|
return json.dumps({
|
|
"messages": messages,
|
|
"cursor": cursor,
|
|
"count": len(messages),
|
|
})
|
|
|
|
|
|
@mcp.tool()
|
|
def queue_status() -> str:
|
|
"""Get current status of the bridge.
|
|
|
|
Returns message counts, last activity, and pending outbound messages.
|
|
"""
|
|
status = db.get_status()
|
|
return json.dumps(status)
|