Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ wheels/
# Virtual environments
.venv
.envrc
servers_config.json
agent_config.json
instructions.md
access.json
access.json.lock
.access.pending.json
.coverage
logs/
data/
claude-home/
.claude/
.private-journal/
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ COPY --from=node:22-slim /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s /usr/local/lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx

RUN apt-get update && apt-get install -y --no-install-recommends curl git jq \
&& rm -rf /var/lib/apt/lists/*

COPY . /app
WORKDIR /app

Expand Down
209 changes: 152 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
# agentic-telegram-bot

A simple Telegram bot that uses the [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) to interact with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers.
A Telegram bot powered by [agent-core](https://github.com/John-Lin/agent-core) that interacts with [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers. Supports two interchangeable providers:

See also: [agentic-slackbot](https://github.com/John-Lin/agentic-slackbot) — a similar demo bot for Slack.
- **OpenAI** (default) — via [OpenAI Agents SDK](https://github.com/openai/openai-agents-python); works with OpenAI and Azure OpenAI v1.
- **Claude** — via [claude-agent-sdk](https://pypi.org/project/claude-agent-sdk/).

Switch providers by setting `"provider"` in `agent_config.json`.

See also: [agentic-slackbot](https://github.com/John-Lin/agentic-slackbot) and [agentic-discord-bot](https://github.com/John-Lin/agentic-discord-bot) — similar bots for Slack and Discord.

## Features

- Private chat and group chat support
- Configurable DM policy (pairing / allowlist / disabled)
- Connects to any MCP server via `servers_config.json`
- Supports OpenAI, Azure OpenAI endpoints
- Per-conversation history with automatic truncation
- Group reply chain — after `@mention`, anyone can continue by replying
- Optional local shell via `ShellTool`, controlled by `SHELL_ENABLED` and `SHELL_SKILLS_DIR`
- Connects to any MCP server via `agent_config.json`
- Optional local shell: `ShellTool` (OpenAI, via `provider.shell`) or `Bash`/`Write`/`Edit`/… (Claude, via `provider.allowedTools`). Read-only built-ins (`Read`, `Glob`, `Grep`) are always on for Claude.
- Supports OpenAI, Azure OpenAI v1, and Anthropic Claude

## Install Dependencies

Expand All @@ -28,87 +33,129 @@ uv sync
- Use the command `/setprivacy` in the BotFather chat.
- Select your bot.
- Choose "Disable" to allow the bot to receive all messages in groups.
4. Set the bot token and username in the `.envrc` or `.env` file.
4. Set the bot token and username in the `.env` file.

## Environment Variables

Create a `.envrc` or `.env` file in the root directory:
Create a `.env` file in the root directory. Set the key(s) for the provider you plan to use:

```
# Telegram bot
export BOT_USERNAME="@your_bot_username"
export TELEGRAM_BOT_TOKEN=""
BOT_USERNAME="@your_bot_username"
TELEGRAM_BOT_TOKEN=""

# OpenAI API
export OPENAI_API_KEY=""
# OpenAI provider (default)
OPENAI_API_KEY=""

# Local shell (disabled by default)
# export SHELL_ENABLED=1
# export SHELL_SKILLS_DIR="./skills" # optional; mount skills alongside the shell
# Claude provider
# ANTHROPIC_API_KEY=""

# Optional verbose OpenAI Agents SDK logging
# export AGENT_VERBOSE_LOG=1
```
# Optional: override SQLite path for session storage (in-memory by default)
# SESSION_DB_PATH="./sessions.db"

## Agent Instructions
# Optional: override the path to the instructions file (default ./instructions.md)
# AGENT_INSTRUCTIONS_PATH="./instructions.md"

The bot loads its system prompt from `instructions.md` in the project root.
If the file is missing, the bot fails fast at startup.
# Optional verbose OpenAI Agents SDK logging (OpenAI only)
# AGENT_VERBOSE_LOG=1
```

You can copy `instructions.md.example` as a starting point:
If you are using Azure OpenAI (v1 API):

```bash
cp instructions.md.example instructions.md
```
BOT_USERNAME="@your_bot_username"
TELEGRAM_BOT_TOKEN=""
OPENAI_API_KEY=""
OPENAI_BASE_URL="https://<resource-name>.openai.azure.com/openai/v1/"
```

If you are using Azure OpenAI (v1 API), set these instead:
## Agent Instructions

Create an `instructions.md` file in the project root with the agent system prompt:

```markdown
You are a helpful financial assistant. Help users look up stock data,
news, and market information. Always include ticker symbols.
Respond in the user's language. Keep responses concise.
```
export OPENAI_API_KEY=""
export OPENAI_BASE_URL="https://<resource-name>.openai.azure.com/openai/v1/"
```

## MCP Server Configuration (Optional)
An example is provided in `instructions.md.example`. The bot will fail to start if this file is missing.

## Provider & MCP Server Configuration (Optional)

Create an `agent_config.json` to choose a provider and connect MCP servers. If the file is absent, the bot starts with the default OpenAI provider and no tools.

`provider` is a tagged union keyed by `type` (`"openai"` or `"anthropic"`). `mcp` uses an opencode-style schema keyed by server name, with `type: "local" | "remote"`.

Create a `servers_config.json` file to add your MCP servers. If this file is not provided, the bot starts with no MCP servers configured.
### OpenAI provider (default)

```json
{
"model": "gpt-5.4",
"mcpServers": {
"provider": {
"type": "openai",
"model": "gpt-5.4",
"apiType": "responses",
"historyTurns": 10
},
"mcp": {
"my-server": {
"command": "uvx",
"args": ["my-mcp-server"]
"type": "local",
"command": ["uvx", "my-mcp-server"]
}
}
}
```

`model` is optional and defaults to `gpt-5.4`. Each MCP server also accepts `timeout` (seconds, default `30.0`) and `enabled` (default `true`).
All `provider` fields are optional (`model` defaults to `gpt-5.4`, `apiType` to `"responses"`, `historyTurns` to `10`). Each MCP entry also accepts `timeout` (seconds, default `30.0`) and `enabled` (default `true`).

For HTTP-based MCP servers (Streamable HTTP), use `url`:
### Claude provider

```json
{
"mcpServers": {
"provider": {
"type": "anthropic",
"model": "claude-sonnet-4-6",
"allowedTools": ["WebFetch"]
},
"mcp": {
"my-stdio": {
"type": "local",
"command": ["python", "-m", "srv"],
"environment": {"FOO": "bar"}
},
"my-http": {
"type": "remote",
"url": "https://example.com/mcp",
"headers": {"Authorization": "Bearer x"}
}
}
}
```

Requires `ANTHROPIC_API_KEY`. Read-only built-ins (`Read`, `Glob`, `Grep`) are always on; `allowedTools` extends that set with any tool that can mutate files or run commands (`Bash`, `Write`, `Edit`, `WebFetch`, …). Tool names are case-sensitive and validated by the SDK — an unrecognized name is silently dropped. Billing/rate-limit/`error_max_turns` errors are surfaced to the chat as a readable message via `AgentError`.

### Remote (HTTP) MCP servers

```json
{
"mcp": {
"my-server": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"headers": {
"Accept": "application/json, text/event-stream"
}
"headers": {"Accept": "application/json, text/event-stream"}
}
}
}
```

For local MCP servers, use `uv --directory`:
### Local MCP servers (via `uv --directory`)

```json
{
"mcpServers": {
"mcp": {
"my-server": {
"command": "uv",
"args": ["--directory", "/path/to/my-server", "run", "my-entrypoint"]
"type": "local",
"command": ["uv", "--directory", "/path/to/my-server", "run", "my-entrypoint"]
}
}
}
Expand Down Expand Up @@ -178,26 +225,48 @@ uv run bot access group remove <GROUP_ID>

Group members do not need to pair individually — access is controlled at the group level.

## Conversation History

Each chat maintains its own conversation history. Replying to the bot's message continues the same conversation via the group reply chain.

OpenAI history length is controlled by `provider.historyTurns` in `agent_config.json` (default `10`). Claude history is managed on disk by `claude-agent-sdk` and resumed across restarts via a `chat_id -> session_id` mapping in SQLite (`SESSION_DB_PATH`).

## Local Shell (Optional)

The bot can expose a local `ShellTool`. This is **disabled by default**. Enable it with:
Local shell tools are **disabled by default** and are configured in `agent_config.json` per provider.

```
export SHELL_ENABLED=1
### OpenAI — `provider.shell`

```json
{
"provider": {
"type": "openai",
"shell": {
"enabled": true,
"skillsDir": "./skills"
}
}
}
```

With just `SHELL_ENABLED=1`, the agent gets bare local shell access with no pre-defined skills.
`provider.shell.enabled` must be a bool (strings are rejected). `provider.shell.skillsDir` is optional and mounts a skills directory alongside the `ShellTool`.

### Shell Skills (Optional)
### Claude — `provider.allowedTools`

You can optionally mount a skills directory alongside the shell. Each immediate subdirectory containing a `SKILL.md` file is registered as a skill and exposed to the agent as a hint (skills are advisory metadata — they do **not** sandbox command execution).
Read-only built-ins (`Read`, `Glob`, `Grep`) are always on. Add mutating or exec-capable tools explicitly:

```json
{
"provider": {
"type": "anthropic",
"allowedTools": ["Bash", "Write", "Edit", "WebFetch"]
}
}
```
export SHELL_ENABLED=1
export SHELL_SKILLS_DIR="./skills"
```

`SHELL_SKILLS_DIR` is ignored unless `SHELL_ENABLED` is set. If the directory is missing or contains no valid skills, the bot falls back to a bare shell and logs a warning.
### Shell Skills (OpenAI only)

Each immediate subdirectory of `skillsDir` containing a `SKILL.md` file is registered as a skill and exposed to the agent as a hint (skills are advisory metadata — they do **not** sandbox command execution). If the directory is missing or contains no valid skills, the bot falls back to a bare shell and logs a warning.

The `SKILL.md` file should have YAML frontmatter with `name` and `description` fields:

Expand All @@ -215,26 +284,52 @@ Detailed instructions for the agent...
```bash
docker build -t agentic-telegram-bot .

# OpenAI provider
docker run -d \
--name telegent \
--name agentic-telegram-bot \
-e BOT_USERNAME="@your_bot_username" \
-e TELEGRAM_BOT_TOKEN="" \
-e OPENAI_API_KEY="" \
-v /path/to/instructions.md:/app/instructions.md \
-v /path/to/access.json:/app/access.json \
agentic-telegram-bot

# Claude provider (agent_config.json must set "provider": {"type": "anthropic"})
docker run -d \
--name agentic-telegram-bot \
-e BOT_USERNAME="@your_bot_username" \
-e TELEGRAM_BOT_TOKEN="" \
-e ANTHROPIC_API_KEY="" \
-v /path/to/instructions.md:/app/instructions.md \
-v /path/to/agent_config.json:/app/agent_config.json \
-v /path/to/access.json:/app/access.json \
agentic-telegram-bot
```

To use MCP servers, mount your config file:
To use MCP servers with OpenAI, also mount the config:

```bash
docker run -d \
--name telegent \
--name agentic-telegram-bot \
-e BOT_USERNAME="@your_bot_username" \
-e TELEGRAM_BOT_TOKEN="" \
-e OPENAI_API_KEY="" \
-v /path/to/instructions.md:/app/instructions.md \
-v /path/to/servers_config.json:/app/servers_config.json \
-v /path/to/agent_config.json:/app/agent_config.json \
-v /path/to/access.json:/app/access.json \
agentic-telegram-bot
```

### Docker Compose

A `docker-compose.yml` and `run.sh` are provided for convenience. Both mount `instructions.md`, `agent_config.json`, `access.json`, persist sessions to `./data`, and mount `./skills` as the agent skills directory.

```bash
docker compose up -d --build
```

Or run the container directly:

```bash
./run.sh
```
11 changes: 11 additions & 0 deletions agent_config.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"provider": {
"type": "openai"
},
"mcp": {
"yfmcp": {
"type": "local",
"command": ["uvx", "yfmcp"]
}
}
}
12 changes: 6 additions & 6 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
import sys

from agent_core import OpenAIAgent
from agent_core import build_agent
from agents import enable_verbose_stdout_logging

from bot.auth import add_group
Expand Down Expand Up @@ -35,13 +35,13 @@ async def start_bot() -> None:
_configure_logging()
config = Configuration()

server_config = config.load_config("servers_config.json")
openai_agent = OpenAIAgent.from_dict("Telegram Bot Agent", server_config)
agent_config = config.load_config("agent_config.json")
agent = build_agent("Telegram Bot Agent", agent_config)

tg_bot = TelegramMCPBot(
config.telegram_bot_token,
config.bot_username,
openai_agent,
agent,
)

try:
Expand All @@ -51,10 +51,10 @@ async def start_bot() -> None:
except (KeyboardInterrupt, asyncio.CancelledError):
logging.info("Shutting down...")
except Exception as e:
logging.error(f"Error: {e}")
logging.error(f"Error: {e}", exc_info=True)
finally:
await tg_bot.cleanup()
await openai_agent.cleanup()
await agent.cleanup()


def cmd_pair(args: argparse.Namespace) -> None:
Expand Down
Loading
Loading