Forwards inbound Telegram messages into your OMP session automatically.
A DM to your bot lands in the agent turn as if you had typed it in the terminal — no copy-pasting, no hooks.
- OMP ≥ 17.2.7 — the bridge uses the
mcp_notificationextension event andpi.sendUserMessage, both part of the OMP extension API. Verified on 17.2.7. - Bun ≥ 1.3.14 — the Telegram MCP server runs on Bun.
- Telegram MCP server — the
telegramserver fromclaude-plugins-official, configured in~/.omp/agent/mcp.json(see below).
OMP receives notifications/claude/channel notifications from the Telegram MCP server, but it does not inject them into the session. So messages sent to the bot never reach the agent, and you end up pasting them in by hand.
This extension closes that gap using OMP's mcp_notification extension event:
Telegram DM → Telegram MCP server → notifications/claude/channel
→ OMP mcp_notification event → pi.sendUserMessage() → agent turn starts
Two real-world gotchas are handled in the code (both hit us in practice):
event.serveris reported in MCPserver:toolform — OMP sets it totelegram:telegram, nottelegram. The bridge matches the bare name, thename:nameform, or any:<name>suffix.deliverAsis omitted —pi.sendUserMessagestarts a turn when idle and queues as a steer only while streaming whendeliverAsis unset. PassingdeliverAs: "steer"forces steer even when idle, which fails to start a fresh turn after a session was aborted or left idle. Omit it so an idle session reliably starts a turn.
Add the server to ~/.omp/agent/mcp.json. Telegram allows exactly one getUpdates consumer per bot token, so if OMP and Claude Code share a token they will fight over the connection (409 Conflict) and repeatedly kill each other's poller. Use a dedicated bot for OMP.
{
"mcpServers": {
"telegram": {
"type": "stdio",
"command": "bun",
"args": ["run", "--silent", "start"],
"cwd": "/Users/<you>/.claude/plugins/cache/claude-plugins-official/telegram/0.0.6",
"env": {
"TELEGRAM_BOT_TOKEN": "<bot token>",
"TELEGRAM_STATE_DIR": "/Users/<you>/.omp-telegram"
},
"timeout": 60000
}
}
}Windows paths: escape the backslashes, since this is JSON —
"cwd": "C:\\Users\\<you>\\.claude\\plugins\\cache\\claude-plugins-official\\telegram\\0.0.6"Version folder: the
0.0.6above is the installed Telegram plugin version. Check the actual folder name — it changes when the plugin updates.
- Bot token: create one with @BotFather. Use a different bot than the one Claude Code uses.
TELEGRAM_STATE_DIR: where this bot's state lives (access.json, inbox). Keep it separate from Claude Code's.
# Add the repository as a marketplace
omp plugin marketplace add TomLeeLive/omp-telegram-plugin
# Install (user scope)
omp plugin install omp-telegram-bridge@omp-telegram-pluginOr from the TUI:
/marketplace add TomLeeLive/omp-telegram-plugin
/marketplace install omp-telegram-bridge@omp-telegram-plugin
Extension modules load at session start, so restart omp for the bridge to take effect.
After restarting, check the OMP log for the bridge's load line — look in ~/.omp/logs/omp.<timestamp>.log:
[telegram-bridge] loaded; listening on servers: telegram
If you see that line, the extension is active. (If it is missing, the extension did not load — see Troubleshooting.)
DM the bot anything and it replies with a six-character pairing code, recorded under pending in <TELEGRAM_STATE_DIR>/access.json.
OMP is not Claude Code. The
/telegram:access pair <code>command belongs to the Claude Code skill, which reads and writes~/.claude/channels/telegram/access.json— not your OMP bot's<TELEGRAM_STATE_DIR>/access.json. Running it does nothing for an OMP bot. Approve the pairing by editing the OMP bot's state file directly, as below.
The bot stores its state (access control, pending pairing codes, downloaded attachments) in a state directory — a folder on disk that the bot reads and writes to decide who may message it. That folder is referenced as <STATE_DIR> throughout this guide.
The state dir is whatever TELEGRAM_STATE_DIR is set to in ~/.omp/agent/mcp.json. A typical value is ~/.omp-telegram — expand ~ to your home directory (C:\Users\<you>\.omp-telegram on Windows, /Users/<you>/.omp-telegram on macOS). The file to edit is <STATE_DIR>/access.json.
DM the bot a message. The bot replies with a 6-char code and writes a pending entry into <STATE_DIR>/access.json, e.g.:
{
"dmPolicy": "pairing",
"allowFrom": [],
"groups": {},
"pending": {
"c111f4": {
"senderId": "123456789",
"chatId": "123456789",
"createdAt": 1785826860181,
"expiresAt": 1785830460181,
"replies": 2
}
}
}To approve:
- Read
<STATE_DIR>/access.jsonfirst (the bot may have added entries — never clobber it). - Move
senderId(the value inside thepending.<code>object) intoallowFrom(dedupe). - Delete the
pending.<code>entry. - Write the file back, pretty-printed:
{ "dmPolicy": "pairing", "allowFrom": ["123456789"], "groups": {}, "pending": {} } - Create the approval marker file
<STATE_DIR>/approved/<senderId>containing thechatId:The bot pollsecho "123456789" > <STATE_DIR>/approved/123456789approved/every 5s, sendsPaired! Say hi to Claude., then deletes the marker.
The code expires after a short window (expiresAt); if the pending entry is gone, DM the bot again to get a fresh code. If the bot is already in allowFrom, pairing is done — no code is issued because dmPolicy: "allowlist" delivers straight through.
From then on, every message you send the bot is injected into the OMP session.
An inbound message arrives in the agent turn like this:
[Telegram @username (chat 12345 2026-08-04T09:12:34.000Z)] hello
The agent already holds the Telegram reply / react / edit_message tools, so that prefix is all the context it needs to hold a conversation.
The bridge listens on a server named telegram by default. If yours is named differently:
export TELEGRAM_BRIDGE_SERVER="my-telegram" # comma-separated for severalNo [telegram-bridge] loaded line in the log after restart.
- Confirm the extension file is where OMP scans it:
<agent-dir>/extensions/telegram-channel-bridge.ts(default~/.omp/agent/extensions/). If you installed via marketplace, confirm it shows inomp plugin list. - Restart
ompagain — extensions load only at session start.
Messages arrive (log shows MCP notification received ... notifications/claude/channel) but nothing is injected.
- Check the log for
[telegram-bridge] forwarding message from @.... If it is absent, theevent.serverfilter did not match. OMP reports the server inserver:toolform (telegram:telegram), which the bridge handles — but if your MCP server is named differently (e.g.my-telegram), setTELEGRAM_BRIDGE_SERVERaccordingly. - If
[telegram-bridge] forwarding...appears but the agent does not respond, the message was delivered as a user prompt — check that a turn actually started (the OMP session should wake).
Inbound messages are dropped entirely (no MCP notification received line).
- The bot is not polled or the sender is not allowlisted. Check the bot's state directory (
<STATE_DIR>— see Pair your account):allowFrommust contain the sender, ordmPolicymust bepairingwith an approved code.
- Access control is not touched. This extension only injects messages. Pairing approvals and allowlist changes stay with the MCP server and with you.
- Same process as OMP. Extensions run in-process. Install code you trust.
- The sender label is forgeable. The injected
[Telegram @name (chat ...)]prefix is part of the same string as the message body, so an allowlisted sender can type that format into their message and appear to be someone else. The MCP server's allowlist is the real boundary — only allow people you trust. - Messages interrupt work in progress. Delivery omits
deliverAs, so a message that arrives while the agent is mid-turn queues as a steer and cuts in. That is the intent, but sending several messages in a row will repeatedly interrupt whatever the agent is doing.
MIT
Built by the team behind ClawSouls — a sharing platform for portable agent personas.