Skip to main content
Glama
README.md
# websearch-mcp

Локальный MCP-сервер веб-поиска. Без ключей, без платных API, без Docker.
Работает на Windows, macOS и Linux.

> **Лицензия:** PolyForm Noncommercial 1.0.0 — только для некоммерческого
> использования, при распространении и форках обязательны сохранение
> копирайта и текста лицензии. См. [LICENSE](LICENSE). Это **не** open source
> в смысле OSI.

Ротация бесплатных движков (DuckDuckGo → Mojeek → Brave → публичные SearxNG),
чтение страниц в markdown с сохранением блоков кода, обход документации в
пределах одного домена.

## Установка

Нужны Python 3.12+ и [uv](https://docs.astral.sh/uv/).

```bash
cd <путь-до-проекта>
uv sync
```

`lxml` даёт ускорение парсинга страниц, но не обязателен: если на вашей
платформе он не установился, сервер сам переключится на чисто питоновский
`html.parser`.

## Регистрация в Claude Code

Подставьте свой путь к проекту. Примеры для трёх платформ:

**Windows** (PowerShell):

```powershell
claude mcp add websearch -- uv run --project C:\path\to\websearch-mcp python -m websearch_mcp.server
```

**macOS / Linux**:

```bash
claude mcp add websearch -- uv run --project /path/to/websearch-mcp python -m websearch_mcp.server
```

Если в каталоге проекта уже есть `.mcp.json`, можно просто скопировать
`.mcp.json.example` в `.mcp.json` и вписать свой путь — тогда регистрация
не нужна.

## Инструменты MCP

| Инструмент | Что делает |
|---|---|
| `web_search(query, max_results)` | Поиск по вебу: заголовок, URL, сниппет |
| `fetch_page(url)` | Страница чистым markdown |
| `crawl_docs(start_url, depth, max_pages)` | Обход документации в пределах домена |
| `search_and_read(query, max_pages)` | Поиск + чтение топов одним вызовом |
| `cache_stats()` / `cache_clear()` | Статистика и очистка кэша |

## Команды CLI

```bash
websearch search "httpx timeout config"
websearch fetch  https://example.com/docs
websearch crawl  https://example.com/docs --depth 2
websearch read   "httpx timeout config" --pages 3
websearch doctor          # какие движки живы прямо сейчас
websearch cache stats
websearch cache prune     # удалить только протухшее
websearch cache clear
```

## Ограничения нагрузки

Сервер рассчитан на локальный запуск и не должен ни висеть, ни расти в памяти:

| Ограничение | Значение | Зачем |
|---|---|---|
| Бюджет на один поиск | 30 с | цепочка из 7 хостов с backoff могла крутиться ~6 минут |
| Размер скачиваемой страницы | 8 MB | BeautifulSoup раздувает HTML в памяти ~20× |
| Суммарный объём `crawl_docs` | 300 тыс. знаков | иначе ответ не влезает в контекст клиента |
| Одна страница в markdown | 60 тыс. знаков | то же |

Превысившая лимит страница пропускается с пояснением, а не роняет вызов.

## Поведение при блокировке

Сервер определяет блокировку по содержимому ответа (HTTP 202, слова `challenge` /
`unusual traffic`, ноль распарсенных результатов), а не по коду ответа. При
блокировке — переход на следующий движок с экспоненциальной паузой и постоянным
набором cookie для каждого движка.

Если все движки отказали или кончился бюджет времени, возвращается **честная
ошибка** с причиной по каждому движку, а не выдуманные результаты.

## Кэш

Дисковый, TTL: поиск — 6 часов, страницы — 24 часа. Повторный запрос идёт из
кэша, в сеть не выходит. Протухшие записи вычищаются автоматически раз в
32 записи; вручную — `websearch cache prune`.

Расположение по платформам:

| ОС | Путь |
|---|---|
| Windows | `%LOCALAPPDATA%\websearch-mcp\cache` |
| macOS | `~/Library/Caches/websearch-mcp` |
| Linux | `$XDG_CACHE_HOME/websearch-mcp` или `~/.cache/websearch-mcp` |

Точный путь показывает `websearch cache stats`.

## Безопасность

Сервер скачивает URL, которые ему передаёт MCP-клиент. Содержимое чужой
страницы — недоверенные данные: страница может содержать инструкции для
модели (prompt injection), которые попытаются направить клиента на другой
адрес. Поэтому на стороне сервера:

**Защита от SSRF.** Все загрузки проходят через `netguard.validate_url()`:
заблокированы приватные, loopback, link-local, зарезервированные и
не-глобальные адреса, а также служебные имена (`localhost`,
`metadata.google.internal`, `169.254.169.254` и т. п.). Доменное имя
проверяется **после** резолва, иначе достаточно зарегистрировать
`evil.example` → `127.0.0.1`, чтобы обойти проверку литерала. Обход через
IPv4-mapped IPv6 (`::ffff:127.0.0.1`) тоже блокируется. Нерезолвимое имя —
блокировка (fail closed).

Известное ограничение: проверка идёт до установки соединения, поэтому
атака через подконтрольный DNS с разными ответами на проверку и на
подключение (TOCTOU) теоретически возможна. Полностью она снимается только
закреплением соединения на проверенном IP, чего `httpx` не предоставляет.
Для локального однопользовательского сервера остаточный риск приемлем.

**Чтение локальной документации.** Если вам нужно читать собственные доки на
`localhost:3000`, задайте `WEBSEARCH_ALLOW_LOCAL=1` — это осознанный opt-in,
по умолчанию он выключен. Он разрешает loopback и RFC1918, но **не**
link-local и **не** сервисы метаданных облаков: `169.254.169.254` и
`metadata.google.internal` блокируются всегда, потому что отдают
IAM-креденшелы. Включая флаг, вы принимаете, что вредоносная страница
теоретически сможет направить сервер на ваши локальные сервисы.

**Схемы.** Разрешены только `http` и `https` — `file://`, `gopher://` и
прочее отклоняются.

**Не рендерит HTML.** Страница конвертируется в markdown; `<script>`,
`<iframe>`, `<form>` и подобное вырезаются до извлечения текста. Сервер
ничего не исполняет и не отображает — он не браузер.

**Только локальный запуск.** Транспорт — stdio, сеть не слушается, портов
нет. Не запускайте этот сервер как общедоступный сервис: у него нет
аутентификации, изоляции и лимитов на число вызовов, и он не рассчитан
на работу от имени нескольких недоверяющих друг другу пользователей.

**Нет ключей и телеметрии.** Сервер не хранит секретов, никуда ничего не
отправляет кроме самих поисковых запросов и не ведёт статистику. Кэш лежит
в пользовательском каталоге и содержит только тексты скачанных страниц.

**Что остаётся на вашей стороне:** запросы уходят на публичные поисковики,
поэтому сами формулировки запросов видны этим сервисам. Не ищите через
сервер секреты и не передавайте их в URL.

## Тесты

```bash
uv run pytest
```

Тесты не требуют интернета: сетевые ответы подменяются, резолв DNS — тоже.