Skip to main content
Glama
ulinycoin

Shadow-tg

by ulinycoin
README.md
<!-- badges -->
[![PyPI version](https://img.shields.io/pypi/v/shadow-tg.svg)](https://pypi.org/project/shadow-tg/)
[![PyPI downloads](https://img.shields.io/pypi/dm/shadow-tg.svg)](https://pypi.org/project/shadow-tg/)
[![Python](https://img.shields.io/pypi/pyversions/shadow-tg.svg)](https://pypi.org/project/shadow-tg/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Stars](https://img.shields.io/github/stars/ulinycoin/shadow-tg?style=flat)](https://github.com/ulinycoin/shadow-tg)

# shadow-tg

**Read public Telegram channels from AI agents — no MTProto, no Telethon, no `api_id`.**

Part of the **Shadow** product line: tools that give AI agents eyes on the open web.

MCP server that scrapes Telegram's public web preview (`t.me/s/…`): channel metadata, posts, media URLs, comments, and DuckDuckGo-backed channel search. Stack: **httpx + lxml + FastMCP**. No browser.

```text
get_channel("durov")
# → title, subscribers_raw ("2.54M"), description, avatar…

get_posts("durov", limit=3)
# → recent posts + views_raw + next_before cursor

search_posts("durov", "TON")
# → filter ~100 recent posts client-side (not Telegram search)
```

---

## Pain point

Agents need Telegram signal (channel size, engagement, fresh posts). Official Bot API can't read arbitrary public channels. MTProto/Telethon means app registration, session files, and ban risk.

shadow-tg stays on the **public HTML surface** Telegram already exposes. Same data a browser sees — structured for MCP tools.

---

## Quick install

```bash
pip install shadow-tg
```

From source:

```bash
git clone https://github.com/ulinycoin/shadow-tg.git
cd shadow-tg
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
```

### Cursor

After `pip install shadow-tg` (or `pip install -e .` from source), `shadow-tg` is available as a command:

```json
{
  "mcpServers": {
    "tg": {
      "command": "shadow-tg"
    }
  }
}
```

Or point to the project venv directly:

```json
{
  "mcpServers": {
    "tg": {
      "command": ".venv/bin/python",
      "args": ["-m", "tg_mcp.server"]
    }
  }
}
```

### Claude Code (Anthropic)

After `pip install shadow-tg`, add to `~/.claude/settings.json`:

```json
{
  "mcpServers": {
    "tg": {
      "command": "shadow-tg"
    }
  }
}
```

Claude Code also picks up the included `CLAUDE.md` for project context.

### Hermes (`config.yaml`)

After `pip install shadow-tg`:

```yaml
mcp_servers:
  tg:
    command: shadow-tg
    enabled: true
```

Or module form (pointing to the project venv):

```yaml
mcp_servers:
  tg:
    command: python3
    args: ["-m", "tg_mcp.server"]
    enabled: true
```

---

## Example prompts

Copy-paste these into your AI agent's chat after adding shadow-tg as an MCP server:

```text
Find the top 5 Telegram channels about AI agents,
check their size and engagement, and recommend the best one
```

```text
Read the last 10 posts from @durov and summarize
what he's talking about this week
```

```text
Inspect these channels for quality: @techcrunch, @techmeme, @verge
Drop any that are stale or have inflated subscribers
```

```text
Search for Telegram channels about Rust programming.
Verify their subscriber counts and show me the 3 most active ones
```

---

## Tools

| Tool | What it does |
|------|----------------|
| `resolve_channel(query)` | Exists? title + canonical username |
| `get_channel(channel)` | Metadata: **subscribers / subscribers_raw**, description, avatar |
| `inspect_channels(channels, …)` | Batch size + `median_views` + `last_post_at` + quality flags |
| `get_posts(channel, limit?, before?)` | Feed page; paginate with `next_before` |
| `get_post(channel, post_id)` | Single post (+ media) |
| `get_media(channel, post_id)` | CDN photo/video URLs + document cards |
| `get_comments(channel, post_id, …)` | Comments: history (`before`) / live-tail (`after`) |
| `search_posts(channel, query, limit?)` | Scan ~100 recent posts + filter |
| `search_channels(query, limit?, verify?)` | DDG `site:t.me/s/`; `verify=true` pulls subs + median views |

### Important response fields

- **`requested` / `canonical`** — alias vs real username from `data-post` (e.g. `@durov` stays `@durov`; alias channels like `@some_news` → `@original_name`)
- **`subscribers` / `subscribers_raw`** — int + display string (`2.54M`). Use **raw** for size; don't drop the decimal (`2.54M` ≠ `54M`)
- **`views` / `views_raw`** — **post** views, not channel size
- **`median_views` / `engagement_ratio` / `flags`** — from `inspect_channels` / verified search; `stale` = no posts newer than `stale_after_days` (default 30)
- **`engagement_flags` / `likely_inflated`** — fake-sub signals: `low_engagement`, `low_ratio`, `dead_reach`
- **`has_more` / `next_before`** — history cursor; **`next_after`** — live-tail comments (~3s poll)

---

## Examples (`@durov`)

```text
resolve_channel("durov")
get_channel("durov")
get_posts("durov", limit=3)
get_post("durov", 513)
get_media("durov", 532)
search_posts("durov", "TON")
inspect_channels("durov, telegram")
search_channels("durov ton", limit=5, verify=true)
```

Comments (only if the channel has a discussion group):

```text
get_comments("durov", 513)              # first page → next_before + next_after
get_comments("durov", 513, before=…)    # older
get_comments("durov", 513, after=…)     # live-tail; empty = wait, reuse after
```

Do not pass `before` and `after` together.

---

## How data is fetched

| Need | Source |
|------|--------|
| Posts / channel page | `https://t.me/s/{channel}` |
| Single post / media | `https://t.me/s/{channel}/{id}` (+ `?embed=1` fallback) |
| Comments | discussion widget + `POST t.me/api/method` (`loadComments`) |
| Channel search | DuckDuckGo `site:t.me/s/` |

---

## Limitations

- **Public channels only** (web preview). Private / "Contact" pages → `exists: false`
- No reactions, members list, DMs, or cross-channel user comment history
- `search_posts` is a recent-post filter, not Telegram full-text search
- `search_channels` depends on DuckDuckGo indexing
- Comments require a linked discussion group
- CDN media URLs can expire

---

## Layout

```
src/tg_mcp/
  server.py           # FastMCP tools
  tme_parser.py       # t.me/s posts / channel / media
  comment_parser.py   # discussion widget + loadComments auth
  channel_search.py   # ddgs
  channel_quality.py  # median views + inflation / stale flags
  cache.py            # TTL cache under /tmp/tg-mcp-cache
CLAUDE.md             # Claude Code project context
tests/
  fixtures/           # HTML fixtures
```

## Tests

```bash
PYTHONPATH=src python3 -m pytest tests/ -v
```

## License

MIT. Free for anything.

---

#telegram #mcp #ai-agents #llm-tools #web-scraping #python #channel-analytics #no-mtproto