Skip to main content
Glama
MediaCrawler MCP Server

MediaCrawler MCP Server

by CubericW
README.md
# MediaCrawler MCP Server

An **MCP (Model Context Protocol) server** that wraps the open-source
[MediaCrawler](https://github.com/NanmiCoder/MediaCrawler) project so that
Claude Code / Codex / Cursor and any other MCP-compatible agent can collect
**public** data from Chinese social platforms through simple tool calls:

> 小红书 (Xiaohongshu) · 微博 (Weibo) · 抖音 (Douyin) · B站 (Bilibili) ·
> 快手 (Kuaishou) · 贴吧 (Tieba) · 知乎 (Zhihu)

Xiaohongshu uses Apify. Other platforms use MediaCrawler as a **black-box CLI**: it
patches config just-in-time, runs the crawler, reads the `data/` output, and
returns normalised JSON. Anti-ban guardrails are built in (rate limiting, a
machine-wide crawl queue, and optional integration with a shared
[risk guard](#optional-risk-guard-integration)). It works with an unmodified
upstream MediaCrawler checkout.

> ⚠️ **For personal, low-frequency, research use only.** MediaCrawler is
> released under a non-commercial learning license and explicitly forbids
> commercial use and large-scale scraping. Respect each platform's Terms of
> Service and robots.txt, keep request rates low, use a secondary account, and
> never collect private personal data.

---

## Architecture

```
agent (Claude Code / Codex)
        │  MCP tool call
        ▼
mediacrawler_mcp.server   ← this package (guardrails + parsing)
        │  patch config/base_config.py  +  subprocess
        ▼
MediaCrawler CLI (uv run main.py --platform ... --type ...)
        │  writes
        ▼
    <MEDIACRAWLER_HOME>/data/*.json   → parsed → normalised JSON back to agent
```

## Xiaohongshu via Apify

The three content tools route `platform="xhs"` to `memo23/xiaohongshu-all-in-one-scraper`.
Set `APIFY_TOKEN` in the MCP environment or save the token alone in
`~/.config/mediacrawler/apify-token.txt` (mode 0600). The file is read on each call.
`login_status().xhs` reports whether a token is present. This backend runs without CDP.
The default per-run charge limit is USD 2, configurable via `MEDIACRAWLER_APIFY_MAX_USD`.
With comments enabled it requests up to 10 root comments per note and 5 replies per root.
Read `replies_incomplete` on root comments for identified gaps. Search returns the text from
the search row. Detail opens the note. Raw dataset rows are saved under `data/xhs/apify/<run_id>/`.

## Exposed MCP tools

| Tool | Purpose | Key args |
|---|---|---|
| `search_posts` | Keyword search on a platform | `platform`, `keywords`, `max_count`, `with_comments` |
| `get_post_detail` | Fetch specific posts by id (for xhs: note IDs, URLs or xhslink.com links) | `platform`, `post_ids[]`, `with_comments` |
| `get_creator_posts` | Fetch a creator's posts | `platform`, `creator_ids[]`, `max_count` |
| `login_status` | Check config + cached login session (+ xhs risk guard) | – |
| `risk_status` | Request budget used and whether crawling is paused after a captcha | `platform` |
| `list_platforms` | Capability matrix | – |
| `diagnostics` | `main.py --help` + effective settings | – |

All content tools return:

```json
{
  "ok": true,
  "platform": "xhs",
  "count": 12,
  "items": [
    {
      "id": "...", "author": "...", "author_id": "...",
      "content": "...", "like_count": 42, "comment_count": 3,
      "publish_time": "...", "url": "...",
      "comments": [ { "...": "raw comment record" } ],
      "raw": { "...": "original MediaCrawler record" }
    }
  ],
  "raw_files": ["/path/to/data/xhs/....json"],
  "warnings": []
}
```

How to read `ok` after a crawl:

| Result | Meaning |
|---|---|
| `ok: false`, `error: "crawl_failed"` | The crawl exited with an error or timed out and saved nothing. The reason is in `details`. |
| `ok: true`, `partial: true` | The crawl stopped the same way but saved some results, for example when the risk guard ended it midway. The items are returned and the reason is in `warnings`. |
| `ok: true`, `count: 0` | The crawl finished without finding anything. Check `warnings`, since an expired login can look the same. |

`diagnostics` returns `ok: false` and `error: "startup_failed"` when
`main.py --help` fails. `login_status` only checks configuration, so its `ok`
does not show whether MediaCrawler can start.

---

## Prerequisites

1. **Clone MediaCrawler** and install its dependencies (it requires Python 3.11,
   `uv`, and Node.js ≥ 16 for some platforms):

   ```bash
   git clone https://github.com/NanmiCoder/MediaCrawler
   cd MediaCrawler
   uv sync
   # optional (only for standard Playwright mode):
   uv run playwright install
   ```

2. **This server** needs the official `mcp` SDK:

   ```bash
   pip install --user "mcp>=1.2.0,<2"
   # or, from this folder:
   pip install --user -e .
   ```

---

## CDP mode (strongly recommended — lowers ban risk)

MediaCrawler's default **CDP mode** connects to *your own* already-logged-in
Chrome, reusing its cookies and session, which greatly reduces risk-control
detection:

1. Install the latest Chrome (≥ 144).
2. Visit `chrome://inspect/#remote-debugging` and enable
   **"Allow remote debugging for this browser instance"** (should report
   `Server running at: 127.0.0.1:9222`).
3. Keep `MEDIACRAWLER_ENABLE_CDP=true` (default).
4. On first crawl, Chrome shows a confirmation dialog — click "accept" within 60s.

If you prefer standard mode, set `MEDIACRAWLER_ENABLE_CDP=false`; you will then
log in via QR code on first run.

---

## Configuration

All settings come from environment variables (see [`.env.example`](.env.example)).
The only required one is `MEDIACRAWLER_HOME`.

| Var | Default | Meaning |
|---|---|---|
| `MEDIACRAWLER_HOME` | `~/MediaCrawler` | Path to the cloned repo (must contain `main.py`) |
| `MEDIACRAWLER_RUNNER` | `uv` | `uv` → `uv run main.py`; or `python` |
| `MEDIACRAWLER_LOGIN_TYPE` | `qrcode` | `--lt` value: qrcode / phone / cookie |
| `MEDIACRAWLER_SAVE_OPTION` | `json` | Output format (json recommended) |
| `MEDIACRAWLER_DEFAULT_MAX` | `20` | Items per call when unspecified |
| `MEDIACRAWLER_MAX_HARD_LIMIT` | `50` | Absolute cap; larger requests clamped |
| `MEDIACRAWLER_DELAY_MIN/MAX` | `2` / `5` | Random delay window (s) |
| `MEDIACRAWLER_RATE_CALLS` | `10` | Max tool calls per window |
| `MEDIACRAWLER_RATE_WINDOW` | `600` | Rate-limit window (s) |
| `MEDIACRAWLER_ENABLE_COMMENTS` | `false` | Collect comments by default (each note's comments cost logged-in requests) |
| `MEDIACRAWLER_ENABLE_CDP` | `true` | Reuse your logged-in Chrome |
| `MEDIACRAWLER_ENABLE_PROXY` | `false` | Enable MediaCrawler's proxy pool |
| `MEDIACRAWLER_RUN_TIMEOUT` | `1800` | Per-run subprocess timeout (s); the risk guard paces requests 10–30 s apart |
| `MEDIACRAWLER_QUEUE_TIMEOUT` | `3600` | How long a crawl may wait for the ones ahead of it (s) |
| `MEDIACRAWLER_GUARD_DIR` | `<HOME>/.risk_guard` | Where the optional risk guard keeps its shared state |

---

## Register with an MCP client

### Claude Code (CLI)

```bash
claude mcp add mediacrawler \
  --env MEDIACRAWLER_HOME=/path/to/MediaCrawler \
  --env PYTHONPATH=/path/to/mediacrawler-mcp \
  -- python -m mediacrawler_mcp.server
```

### Claude Desktop / Cursor (JSON)

See [`examples/claude_desktop_config.snippet.json`](examples/claude_desktop_config.snippet.json)
and paste it into your client's MCP config, fixing the two absolute paths.

---

## Quick test

```bash
# 1. unit tests (no network, no CLI)
pip install --user pytest
PYTHONPATH=. pytest -q

# 2. end-to-end (actually runs MediaCrawler — log in first!)
export MEDIACRAWLER_HOME=/path/to/MediaCrawler
python examples/quickstart.py
```

Ask your agent, e.g.:

> "用 mediacrawler 搜索小红书上关于『城市露营』的 10 条笔记并带上评论"

---

## How it passes parameters (important)

MediaCrawler reads **keywords / specified post-ids / creator-ids from
`config/base_config.py`**, not reliably from CLI flags. So before each run this
server **temporarily patches `base_config.py`** (setting `KEYWORDS`,
`*_SPECIFIED_ID_LIST`, `*_CREATOR_ID_LIST`, `CRAWLER_MAX_NOTES_COUNT`,
`ENABLE_GET_COMMENTS`, `ENABLE_CDP_MODE`, …) and **restores the original file
afterwards**, byte for byte. This keeps the server working across MediaCrawler
versions without forking it.

If MediaCrawler changes its argument names, run the `diagnostics` tool to see the
current `main.py --help`, then adjust `mediacrawler_mcp/runner.py`.

---

## One crawl at a time

Every MCP server process on this machine shares one MediaCrawler checkout: one
`config/base_config.py`, one debug Chrome, and one result file per platform, type
and day, which MediaCrawler rewrites whole for each item. So crawls queue on a
machine-wide lock, `<MEDIACRAWLER_GUARD_DIR>/crawl.lock`, and a tool call simply
takes longer while it waits. `queue_wait_seconds` in the result says how long.

- **Scope**: the lock covers `search_posts`, `get_post_detail` and
  `get_creator_posts`. `uv run main.py` by hand and other tools that drive the same
  Chrome do not take it.
- **Breaker**: the risk guard is checked again once a crawl reaches the front, since
  the crawl ahead may have tripped it.
- **Server killed mid-crawl**: the crawler inherits the lock, so the queue waits for
  it to finish. The original config stays in
  `<MEDIACRAWLER_GUARD_DIR>/base_config.py.running` for the length of a run, and the
  next crawl restores from it if the server never got to.
- **Timeout**: the whole process group is stopped, because `uv run` keeps Python as
  a child and killing `uv` alone would leave the crawl running.
- **Results**: `items` holds only what this call crawled. The record counts of the
  per-day files are taken before the crawl and compared after it, inside the lock.
  `raw_files` lists those per-day files, which also hold every earlier crawl of the day.
- **Logs**: the crawler's output goes to temporary files. Playwright's Node driver
  makes the stderr it inherits non-blocking, which on a pipe cuts long log lines
  mid-character and drops later ones. Any invalid UTF-8 left in the output is shown
  as U+FFFD.

Code changes reach a server only when that process restarts, and a server still on
code from before this lock neither queues nor respects it.

---

## Troubleshooting

| Symptom | Likely cause / fix |
|---|---|
| `configuration_error` | `MEDIACRAWLER_HOME` wrong or missing `main.py` |
| `run_error: Failed to launch ...` | `uv` not installed → set `MEDIACRAWLER_RUNNER=python` and activate a venv |
| `count: 0` + warning | Login session expired → run `login_status`, then re-login (QR / CDP) |
| `rate_limited` | You hit the local guard; wait, or raise `MEDIACRAWLER_RATE_CALLS` (at your own risk) |
| `queue_timeout` | Other crawls held the queue past `MEDIACRAWLER_QUEUE_TIMEOUT`. `details` names the holder and its pid; retry later, and stop that process only if it is stuck |
| warning "could not attach to the debug Chrome" | MediaCrawler fell back to its own browser profile, without the debug Chrome's login. The warning quotes the reason; check that Chrome is running with `--remote-debugging-port=9222` |
| `risk_guard_tripped` | The platform answered with a captcha / risk code; every project is paused until `retry_after_seconds`. Solve any captcha in the browser; do not retry early |
| warning "stopped early because of its risk guard" | The budget ran out (`BudgetExhausted`) or a captcha appeared mid-run; partial results are returned |
| warning "login session was not accepted" (xhs) | The platform rejected the browser session. Check that Chrome is logged in, or use the Apify backend for xhs |

---

## Optional risk guard integration

This server can cooperate with a cross-process **risk guard** running inside
MediaCrawler, i.e. a component that paces every request, counts them against
shared hourly/daily budgets, and opens a circuit breaker when the platform
answers with a captcha or risk-control code. **No such guard ships with
upstream MediaCrawler or with this repository**; if you add one, have it write
its state to `<MEDIACRAWLER_GUARD_DIR>/<platform>.json`:

```json
{ "tripped_until": 1767225600, "trip_reason": "captcha HTTP 461",
  "requests": { "account": [1767220000.0], "anon": [], "media": [] },
  "history": [] }
```

When that file exists, the server:

- checks the breaker before launching MediaCrawler and returns
  `risk_guard_tripped` with `retry_after_seconds` instead of crawling;
- reports the request budget used in `risk_status` / `login_status`;
- recognises the guard's log markers (`RiskControlTripped`, `BudgetExhausted`,
  `LoginRequired`, `[RiskGuard]`) and returns partial results with a warning.

Without the file, these checks are no-ops and everything else works as usual.

## Safety & compliance

- Personal, low-frequency, **non-commercial research** only.
- Use a **secondary account**; prefer **CDP mode** + proxies; keep counts small.
- Do **not** collect private personal data or anything behind auth you don't own.
- You are responsible for complying with platform ToS and applicable law.
  See MediaCrawler's disclaimer and
  [记录爬虫违法违规案例](https://github.com/HiddenStrawberry/Crawler_Illegal_Cases_In_China).

## License

The code in this repository is released under the [MIT License](LICENSE).
It does **not** include any MediaCrawler code; it runs MediaCrawler as an
external program. MediaCrawler itself is distributed under its own
[NON-COMMERCIAL LEARNING LICENSE 1.1](https://github.com/NanmiCoder/MediaCrawler/blob/main/LICENSE),
which you must accept and comply with when you install and run it.