Metadata-Version: 2.4
Name: shadow-tg
Version: 0.1.0
Summary: Telegram channel tools for AI agents — read public channels, search, analyze engagement. No MTProto.
Author-email: ulinycoin <ulinycoin7@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/ulinycoin/shadow-tg
Project-URL: Repository, https://github.com/ulinycoin/shadow-tg
Project-URL: Issues, https://github.com/ulinycoin/shadow-tg/issues
Project-URL: Documentation, https://github.com/ulinycoin/shadow-tg#readme
Keywords: telegram,mcp,ai-agents,llm,channel-search,web-scraping,tme,shadow
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Communications :: Chat
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: lxml>=5.1.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp>=1.2.0
Requires-Dist: ddgs>=0.6.0
Provides-Extra: dev
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: build>=1.2.0; extra == "dev"
Requires-Dist: twine>=5.0.0; extra == "dev"
Dynamic: license-file

<!-- badges -->
[![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 (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 -e .`, `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 -e .`, 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 -e .`:

```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
