Skip to content

Messaging Channels

Reactive Agents can send and receive messages on Signal and Telegram using MCP servers. No custom adapter code needed — the framework’s built-in .withMCP() and .withGateway() capabilities handle everything.

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). The Telegram MCP server uses chigwell/telegram-mcp. Both run in hardened Docker containers. The gateway heartbeat polls for messages; the agent uses MCP tools to read and respond.

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

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

Terminal window
./scripts/telegram-session.sh

Save the output to .env.telegram:

Terminal window
TELEGRAM_API_ID=12345678
TELEGRAM_API_HASH=abc123...
TELEGRAM_SESSION_STRING=1BVtsO...
const agent = await ReactiveAgents.create()
.withName("telegram-agent")
.withProvider("anthropic")
.withReasoning()
.withTools()
.withGuardrails()
.withKillSwitch()
.withMCP([{
name: "telegram",
transport: "stdio",
command: "docker",
args: [
"run", "-i", "--rm",
"--cap-drop", "ALL",
"--no-new-privileges",
"--memory", "128m",
"--user", "1000:1000",
"--env-file", ".env.telegram",
"ghcr.io/reactive-agents/telegram-mcp",
],
}])
.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();

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

All Docker flags in the examples enforce 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
  • Never pass secrets as MCP tool arguments — they’d appear in agent context
  • Use --env-file for Telegram credentials
  • 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
  • Check heartbeat interval (default: 15s)
  • Verify daily token budget isn’t exhausted
  • Check ProactiveActionSuppressed events for policy blocks
  • Ensure MCP containers are running: docker ps