Skip to content

Messaging Channels

Reactive Agents can send and receive messages on Signal and Telegram using MCP servers wired through .withMCP() and .withGateway(). Signal ships as a hardened Docker image in this repo because there is no maintained third-party MCP with the same behavior. Telegram uses the community chigwell/telegram-mcp project — run it with uvx, a local clone, or your own container; we do not publish a Telegram image from this monorepo.

Gateway heartbeat fires every N seconds
→ Agent calls receive_message MCP tool
→ Processes new messages (with guardrails)
→ Responds via send_message MCP tool

The Signal MCP server is a custom TypeScript implementation (docker/signal-mcp/server/) that spawns signal-cli in persistent jsonRpc mode — a single JVM boot with instant command execution (no cold starts per message). It is the supported way to attach Signal to the gateway.

Telegram: use chigwell/telegram-mcp directly. Plain pip install telegram-mcp / uvx telegram-mcp without --from pointing at chigwell’s sources often resolves to a different PyPI project (hosted relay) that expects TELEGRAM_CHAT_ID — not the Telethon user MCP described here.

The gateway heartbeat (or webhooks) drives when the agent runs; the agent uses MCP tools to read and respond. Signal can also push notifications/message for faster inbound handling; Telegram typically relies on polling tools unless you add a separate relay.

Terminal window
docker build -t signal-mcp:local docker/signal-mcp/

Signal requires a real phone number and a captcha. Run the registration helper:

Terminal window
./scripts/signal-register.sh +1234567890

This will:

  1. Ask you to solve a captcha at https://signalcaptchas.org/registration/generate
  2. Send a verification code to your phone
  3. Store encrypted auth keys in ./signal-data/

The data directory is volume-mounted into Docker on subsequent runs.

const agent = await ReactiveAgents.create()
.withName("signal-agent")
.withProvider("anthropic")
.withReasoning()
.withTools()
.withGuardrails()
.withKillSwitch()
.withMCP([{
name: "signal",
transport: "stdio",
command: "docker",
args: [
"run", "-i", "--rm",
"--cap-drop", "ALL",
"--security-opt", "no-new-privileges",
"--memory", "512m",
"-v", "./signal-data:/data:rw",
"-e", `SIGNAL_USER_ID=${process.env.SIGNAL_PHONE_NUMBER}`,
"signal-mcp:local",
],
}])
.withGateway({
heartbeat: {
intervalMs: 15_000,
policy: "adaptive",
instruction: "Check Signal for new messages using signal/receive_message. Respond to any that need attention.",
},
policies: { dailyTokenBudget: 50_000, maxActionsPerHour: 30 },
})
.build();
ToolDescription
signal/send_message_to_userSend a direct message to a Signal user
signal/send_message_to_groupSend a message to a Signal group
signal/receive_messageReceive pending messages (with timeout)
signal/list_groupsList all Signal groups the account belongs to

There is no docker/telegram-mcp/ image in this repository. Install uv (or follow upstream’s clone + uv sync workflow), then point .withMCP() at the telegram-mcp console entrypoint from chigwell’s sources.

Get API credentials from my.telegram.org/apps, then run:

Terminal window
./scripts/telegram-session.sh

Export the values in your shell (or use a secrets manager). Example:

Terminal window
export TELEGRAM_API_ID=12345678
export TELEGRAM_API_HASH=abc123...
export TELEGRAM_SESSION_STRING=1BVtsO...

Pin a tag or revision you trust (v3.0.4 is an example):

const agent = await ReactiveAgents.create()
.withName("telegram-agent")
.withProvider("anthropic")
.withReasoning()
.withTools()
.withGuardrails()
.withKillSwitch()
.withMCP([{
name: "telegram",
transport: "stdio",
command: "uvx",
args: [
"--from",
"git+https://github.com/chigwell/[email protected]",
"telegram-mcp",
],
env: {
TELEGRAM_API_ID: process.env.TELEGRAM_API_ID ?? "",
TELEGRAM_API_HASH: process.env.TELEGRAM_API_HASH ?? "",
TELEGRAM_SESSION_STRING: process.env.TELEGRAM_SESSION_STRING ?? "",
},
}])
.withGateway({
heartbeat: {
intervalMs: 15_000,
policy: "adaptive",
instruction: "Check Telegram for unread messages using telegram/get_chats. Respond to conversations that need attention.",
},
policies: { dailyTokenBudget: 50_000, maxActionsPerHour: 30 },
})
.build();

Alternatives: run uv run main.py from a checkout of chigwell/telegram-mcp, or wrap upstream in your own Docker image — keep that outside this monorepo unless you want to contribute it as a separate published image.

The Telegram MCP server exposes 70+ tools. Key ones for messaging:

ToolDescription
telegram/send_messageSend a text message to a chat
telegram/get_chatsList chats with unread counts
telegram/search_messagesSearch messages in a chat
telegram/send_fileSend a file or document
telegram/forward_messageForward a message between chats

The Signal Docker example uses strict isolation:

FlagPurpose
--cap-drop ALLRemove all Linux capabilities
--no-new-privilegesPrevent privilege escalation
--memory 512mHard memory limit (Signal needs 512m for JVM)
--pids-limit 30Prevent fork bombs
--user 1000:1000Run as non-root
--read-onlyImmutable root filesystem

Telegram via uvx runs as your host user; apply process isolation separately if you need a sandbox.

  • Never pass secrets as MCP tool arguments — they’d appear in agent context
  • For Telegram with uvx: pass credentials via .withMCP({ env: { ... } }) or your process manager — avoid putting secrets in MCP args
  • Use Docker volumes for Signal auth keys (./signal-data/)
  • Add .env.telegram and signal-data/ to .gitignore

Always enable .withGuardrails() for messaging agents. Inbound messages from external users can contain prompt injection attempts. Guardrails check for injection, PII, and toxicity before the LLM processes the message.

Always enable .withKillSwitch() for autonomous messaging agents. This provides:

  • agent.stop(reason) — graceful shutdown at next phase boundary
  • agent.terminate(reason) — immediate halt
  • Ensure Docker is running
  • Signal requires a CAPTCHA — see the registration script
  • The Docker image requires glibc (not Alpine) for signal-cli’s native library
  • Re-run ./scripts/telegram-session.sh
  • Update .env.telegram with new session string

TELEGRAM_CHAT_ID environment variable required

Section titled “TELEGRAM_CHAT_ID environment variable required”
  • You are running the wrong PyPI telegram-mcp (hosted relay), not chigwell’s Telethon server.
  • Use uvx --from git+https://github.com/chigwell/telegram-mcp.git@<tag> telegram-mcp (or upstream’s documented install), not bare uvx telegram-mcp from PyPI.

BotMethodInvalidError / GetDialogsRequest / “cannot be executed as a bot”

Section titled “BotMethodInvalidError / GetDialogsRequest / “cannot be executed as a bot””
  • chigwell/telegram-mcp is a user-account Telethon client (full dialogs, send as you). It does not work with a @BotFather bot session string.
  • Regenerate TELEGRAM_SESSION_STRING using ./scripts/telegram-session.sh and sign in with your personal Telegram account (SMS / Telegram OTP), not a bot token.
  • Check heartbeat interval (default: 15s)
  • Verify daily token budget isn’t exhausted
  • Check ProactiveActionSuppressed events for policy blocks
  • Ensure the Signal container (if used) is running: docker ps
  • For Telegram, confirm uvx resolves chigwell’s package and that TELEGRAM_* env vars are set for the MCP subprocess