Prerequisites
A working MCP workflow that produces the content you want to deliver — see how to build a daily top-movers report for a template.
Credentials for your destination channel: a bot token for Telegram or Discord, or SMTP / transactional email credentials for email.
Setup — Telegram
Create a Telegram bot
Message @BotFather, send
/newbot, and follow the prompts. Save the bot token.
Get your chat ID
Send any message to your Telegram bot, then fetch the URL below. The
chat.idin the response is what you need.
https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates
Test delivery
import requests
TOKEN = "your_bot_token"
CHAT_ID = "your_chat_id"
def send_telegram(text: str) -> None:
requests.post(
f"https://api.telegram.org/bot{TOKEN}/sendMessage",
json={
"chat_id": CHAT_ID,
"text": text,
"parse_mode": "Markdown",
},
).raise_for_status()
send_telegram("Hello from your Cryptohopper MCP workflow")
Telegram supports Markdown, so tables and bold formatting from the MCP render natively. Messages over 4,096 characters must be split — chunk long reports at paragraph boundaries.
Setup — Discord
Create a webhook
In your Discord server, open a channel → gear icon → Integrations → Webhooks → New Webhook. Copy the webhook URL.
Test delivery
import requests
WEBHOOK = "https://discord.com/api/webhooks/..."
def send_discord(text: str) -> None:
requests.post(WEBHOOK, json={"content": text}).raise_for_status()
send_discord("Hello from your Cryptohopper MCP workflow")
Discord's content limit is 2,000 characters per message — split long reports. Discord has no native table support; format tables as aligned code blocks using triple backticks. For richer output, use Discord embeds (title, description, fields) instead of raw content.
Setup — Email
Two options: SMTP via Gmail or similar (simple, free, rate-limited) or a transactional service like SendGrid, Mailgun, or Resend (reliable, preferred for scheduled jobs).
SendGrid example
import os
import requests
API_KEY = os.environ["SENDGRID_API_KEY"]
FROM = os.environ.get("ALERT_FROM_EMAIL", "[email protected]")
TO = os.environ.get("ALERT_TO_EMAIL", "[email protected]")
def send_email(subject: str, body: str) -> None:
requests.post(
"https://api.sendgrid.com/v3/mail/send",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"personalizations": [{"to": [{"email": TO}]}],
"from": {"email": FROM},
"subject": subject,
"content": [{"type": "text/markdown", "value": body}],
},
).raise_for_status()
send_email("Daily top movers", "## Today's movers\n...")
Use Markdown or HTML for emailed reports — plain text quickly becomes unreadable for tabular data.
Choosing the right channel
Channel | Best for | Constraints |
Telegram | Real-time alerts, on your phone, one-glance reading | 4,096 char limit, Markdown-lite |
Discord | Shared team channel, rich formatting, discussion around alerts | 2,000 char limit, no real tables |
Longer digests, archival, anything you'll want to search later | Higher latency, easy to ignore |
Ad-hoc alerts → Telegram. Team-visible output → Discord. Morning digests → email.
The canonical pattern
Keep the MCP step and the delivery step separate. A failure in delivery (network blip, expired token) should not lose the MCP output — you can still recover it from logs.
# 1. Run MCP workflow (produces text output)
report_text = run_mcp_workflow(prompt)
# 2. Deliver
try:
send_telegram(report_text)
except Exception as e:
log(f"Delivery failed: {e}")
Troubleshooting
Telegram getUpdates returns an empty list
You haven't sent a message to the bot yet, or you're querying with a different token. Send any message from your Telegram account to the bot first, then retry.
Discord webhook returns 429
Rate limit hit. Discord webhooks allow roughly 30 messages per minute — respect the Retry-After header. For high-frequency alerts, batch multiple events into one message rather than firing one per event.
Emails are landing in spam
Use a proper From address on a domain you own, with SPF/DKIM/DMARC configured. Avoid all-caps subject lines, trigger words, and attachments. A transactional service handles most of this correctly; plain SMTP from a shared provider often doesn't.
Long MCP reports get truncated in Telegram or Discord
Build a splitter that chunks at paragraph boundaries. Don't split in the middle of a table row — cut after the last complete row that fits, and open the next message with a (cont'd) header.
Delivery silently fails on scheduled runs but works manually
The most common cause is a missing environment variable or expired token in the scheduled environment. Cron runs without your shell's env; GitHub Actions needs secrets configured. Log the full error on every failure so it surfaces immediately.
You only want alerts when something actually flags
Make delivery conditional — don't send "nothing to report" messages, they become noise and you'll start ignoring the channel.
You want to deliver to multiple channels at once
Wrap delivery in a fan-out so each channel is independent and a failure in one doesn't block the others: for ch in [send_telegram, send_discord, send_email]: try ch(text).
