hh-radar
The server exposes seven MCP tools that let an LLM agent search, analyze, and assess hh.ru vacancies stored in the project database.
search_vacancies — full-text search by words/phrases/filters (remote, salary, area, experience, publication date)
semantic_search — meaning-based search for queries that don't match exact keywords
get_vacancy — retrieve a complete job posting by ID
skill_stats — see which skills are most demanded and their median salaries
market_overview — aggregate market snapshot: salary percentiles, remote share, experience breakdown, top employers
compare_to_profile — honestly compare a vacancy against a candidate profile and return a verdict with reasons
db_status — check database contents, coverage, and freshness to avoid inventing data
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hh-radar what skills are most requested in automation vacancies this month?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
hh-radar
Своя база вакансий hh.ru, которую LLM-агент опрашивает как набор инструментов. Полнотекстовый поиск на PostgreSQL, поиск по смыслу на pgvector, семь MCP-инструментов и честный замер того, где поиск ошибается.
📊 Витрина — открывается за десять секунд, все цифры посчитаны программой из базы. Сейчас на ней демонстрационный корпус: вакансии hh нельзя ни выложить в публичный репозиторий, ни скачать без токена приложения, поэтому страница собрана из сгенерированных данных и говорит об этом первым же абзацем.
Зачем это написано
Я искал работу через hh.ru и делал руками одно и то же: открыть выдачу, прочитать тридцать описаний, выписать, какие навыки повторяются, прикинуть, куда вообще есть смысл писать. Поиск на hh отвечает на «есть ли слово в тексте». На вопросы, которые возникали у меня — «где чаще всего просят n8n и сколько за это платят», «какие вакансии возьмут человека без коммерческого опыта», «где нужно чинить чужие сломанные автоматизации» — он не отвечает вообще.
Поэтому вакансии складываются в свою базу, а поверх базы стоит MCP-сервер. Дальше вопрос задаётся обычными словами прямо в Claude Desktop, а агент сам решает, чем его обслужить: полнотекстовым поиском, агрегацией по навыкам, срезом зарплат или поиском по смыслу.
Ты: какие навыки чаще всего требуют в вакансиях по автоматизации за последний месяц?
Claude вызывает skill_stats(query="автоматизация", published_within_days=30)
→ Python — 61% вакансий, медиана 140 000 ₽
n8n — 34%, медиана 150 000 ₽
Docker — 29%, ...Цифры в примере — иллюстрация формата ответа. Настоящие лежат на витрине и меняются с каждым сбором.
Related MCP server: HuntFlow MCP Server
Что внутри
Слой | Что делает |
| Клиент API hh.ru: OAuth-токен приложения, ограничение частоты, ретраи, обход потолка выдачи |
| Идемпотентная запись в базу: UPSERT, двухфазная загрузка карточек |
| Четыре сущности плюс связь многие-ко-многим, полнотекстовый индекс, слой запросов |
| Чанкинг, эмбеддинги, поиск по смыслу на pgvector, гибридный поиск, оценка качества |
| Семь инструментов для LLM-агента поверх всего перечисленного |
| Генератор статической витрины для GitHub Pages |
Инструменты MCP
Инструмент | Отвечает на вопрос |
| «Найди вакансии со словами X, удалённо, от 150 тысяч» |
| «Найди вакансии, где надо чинить чужие сломанные автоматизации» |
| «Покажи эту вакансию целиком» |
| «Какие навыки требуют чаще всего и сколько за них платят» |
| «Какая вилка на рынке, сколько удалёнки, кто больше всех нанимает» |
| «Мне сюда стоит писать?» — с честным ответом «нет, и вот почему» |
| «Что вообще есть в базе и за какой период» — чтобы агент не выдумывал |
Быстрый старт
Нужны Docker и Python 3.12+.
git clone https://github.com/Denmurzik/hh-radar
cd hh-radar
uv sync --extra rag # или: pip install -e ".[rag]"
docker compose up -d db # PostgreSQL 17 + pgvector на порту 5433
uv run alembic upgrade head # схема
uv run hh-radar seed # 52 демонстрационные вакансии, без токена и без сети
uv run hh-radar statusЧерез минуту у вас работающая база и MCP-сервер. Дальше — либо подключить его к Claude Desktop (см. ниже), либо собрать настоящие данные.
Вакансии, которые кладёт seed, вымышлены: сорок восемь из них собирает
scripts/make_demo_corpus.py из двенадцати ролевых
архетипов с фиксированным зерном. Чужие объявления в публичном репозитории лежать не
должны, а без токена приложения их и не получить. Проходят они через тот же разбор,
тот же конвейер записи и тот же чанкинг, что и живые данные, — отдельной ветки «для
демо» в коде нет. Подробности в samples/README.md.
Настоящие данные
GET /vacancies у hh закрыт для анонимных запросов — отвечает
403 bad_authorization. Нужен токен приложения:
Зайдите на dev.hh.ru/admin → «Создать приложение».
Скопируйте Client ID и Client Secret.
cp .env.example .envи впишите их вHH_CLIENT_ID/HH_CLIENT_SECRET.
Токен по client_credentials запрашивается автоматически и кэшируется на диск —
/token не дёргается на каждый запуск.
uv run hh-radar ingest --days 30 # собрать вакансии
uv run hh-radar index # посчитать эмбеддинги
uv run hh-radar showcase # пересобрать витринуПодключение к Claude Desktop
%APPDATA%\Claude\claude_desktop_config.json (Windows) или
~/Library/Application Support/Claude/claude_desktop_config.json (macOS):
{
"mcpServers": {
"hh-radar": {
"command": "C:\\путь\\к\\hh-radar\\.venv\\Scripts\\python.exe",
"args": ["-m", "hh_radar.mcp_server"],
"env": { "DATABASE_URL": "postgresql+psycopg://hh:hh@localhost:5433/hh_radar" }
}
}
}Перезапустите Claude Desktop — в списке инструментов появится hh-radar.
Схема базы
erDiagram
EMPLOYERS ||--o{ VACANCIES : "нанимает"
VACANCIES ||--o{ VACANCY_CHUNKS : "режется на"
VACANCIES }o--o{ SKILLS : "требует"
EMPLOYERS {
bigint id PK "идентификатор hh"
string name
bool trusted
}
VACANCIES {
bigint id PK "идентификатор hh"
string name
int salary_from_rub "приведено к рублям"
bool is_remote
text description
tsvector search_vector "генерируемый, GIN"
timestamptz published_at "индекс DESC"
}
SKILLS {
int id PK
string name UK "нормализованное"
string display_name "как написал работодатель"
}
VACANCY_CHUNKS {
bigint id PK
int chunk_index
text content
vector embedding "384, HNSW cosine"
}Инженерные решения, которые стоит объяснить
Здесь то, что не видно из списка технологий, но именно это отличает работающий сборщик от примера из документации.
Потолок выдачи в 2000 элементов. hh не отдаёт больше двух тысяч вакансий на один поисковый запрос, сколько страниц ни проси. Сборщик сначала спрашивает, сколько всего найдено за интервал дат; если больше потолка — режет интервал пополам и повторяет для каждой половины рекурсивно. Дробление останавливается на часовом окне: если и там перебор, честнее записать предупреждение в лог, чем молча потерять данные.
tsvector — генерируемый столбец, а не вычисление на лету. Postgres поддерживает
его сам при каждой записи, GIN-индекс строится по готовому значению. Название вакансии
весит больше описания (setweight A против B) — совпадение в заголовке релевантнее
совпадения где-то в середине абзаца про ДМС.
Проверить план: hh-radar explain "kubernetes airflow". Команда не просто печатает
EXPLAIN ANALYZE, а разбирает его, потому что «Seq Scan» сам по себе ещё ничего не
означает. На маленькой таблице последовательное чтение дешевле индекса, и планировщик
прав. На большой, но при запросе, под который подходит половина базы, — тоже прав.
Проблема только в третьем случае: запрос избирательный, таблица большая, а индекс не
взят — обычно потому, что после массовой заливки не собрана статистика. Поэтому
hh-radar ingest в конце сам выполняет ANALYZE: без этого первые же поиски по
свежей базе идут мимо индекса.
Различение INSERT и UPDATE без лишнего SELECT. У INSERT ... ON CONFLICT DO UPDATE
системный столбец xmax равен нулю у только что вставленной строки и содержит
идентификатор транзакции у обновлённой. RETURNING (xmax = 0) даёт точную статистику
прогона одним запросом.
Двухфазная загрузка. Поиск отдаёт сто вакансий за запрос, но без описания и навыков; полная карточка стоит одного запроса на вакансию. Поэтому сначала пишется всё, что дал поиск, и только потом дозагружаются карточки — по тем вакансиям, у которых их ещё нет. Прерванный на середине прогон ничего не теряет, следующий подхватывает с того же места.
Название вакансии в каждом чанке. Кусок текста «требуется опыт от года и знание Docker» не находится по запросу «python-разработчик»: слова «python» в самом куске нет. Поэтому каждый чанк начинается с названия вакансии. Мелочь, которая меняет recall в разы.
Схлопывание по вакансии в семантическом поиске. Без этого одна многословная вакансия занимает всю первую страницу выдачи своими пятью чанками. Берём с запасом, оставляем лучший чанк каждой вакансии.
RRF, а не взвешенная сумма. Гибридный поиск объединяет полнотекстовую и
семантическую выдачу по Reciprocal Rank Fusion. Складывать ts_rank с косинусной
близостью нельзя: это числа из разных пространств, и коэффициент для их смешивания
подбирался бы под конкретный набор запросов. RRF работает с рангами, а ранги
сравнимы всегда.
Заглушка вместо модели в CI. EMBEDDING_BACKEND=hash — детерминированный
хеш-эмбеддер той же размерности. Он ничего не знает о смысле, но позволяет прогнать
всю проводку кода, не выкачивая 220 МБ ONNX на каждый пуш. Настоящая модель —
paraphrase-multilingual-MiniLM-L12-v2 через fastembed, на CPU, без API-ключей:
проверяющий должен уметь запустить проект, не заводя аккаунтов.
Где поиск ошибается
Чисел здесь нет, и это осознанно. Опубликовать recall@10 по вымышленному корпусу, который сам же и сгенерировал, — значит принять экзамен у самого себя: запросы и данные писал один человек, и метрика померит их совпадение, а не качество поиска. Настоящие числа появятся здесь после сбора с токеном приложения; до тех пор честнее пустая таблица, чем красивая.
Что при этом сделано и работает:
uv run hh-radar evaluate # в терминал
uv run hh-radar evaluate -o eval/report.md # в файлКоманда гоняет размеченный набор из 25 запросов через три метода
поиска — полнотекстовый, семантический и гибридный — и считает recall@k, precision@k,
MRR и среднее время ответа для каждого. Эталон берётся из relevant_vacancy_ids, а
пока ручной разметки нет — из регулярных выражений по названию вакансии. Запросы, для
которых эталона не нашлось вообще, не идут в среднее и считаются отдельной колонкой:
записать их в провалы было бы враньём в свою пользу наоборот.
Интереснее таблицы — то, что команда печатает под ней: список запросов, на которых
метод не нашёл ни одной релевантной вакансии в топ-5, вместе с тем, что он вернул
вместо неё. По нему видно класс запросов, который система не тянет. Даже на
демонстрационном корпусе это уже видно: на перифразах вроде «нужен человек, который
свяжет CRM и мессенджер через no-code» полнотекстовый поиск не возвращает ничего —
websearch_to_tsquery требует все значимые слова разом, — а семантический возвращает
ровно те вакансии, где про no-code не сказано ни разу. Ради этой разницы pgvector в
проекте и стоит.
Разметка лежит в eval/queries.yaml и составлена
с уклоном в то, что ключевыми словами не находится: перифразы, описания задачи вместо
названия технологии, запросы про условия и про уровень кандидата.
Ограничения
Пишу прямо, потому что проверять всё равно будут:
Это личный проект, а не продакшен под нагрузкой. Написан для собственного поиска работы.
Нужен токен приложения hh. Без него доступны только справочники; вакансии не отдаются никому анонимно.
Курсы валют статичные. Приведение зарплат к рублям — приближение для сортировки, а не финансовый расчёт. Тянуть курсы ЦБ ради этого — усложнение без пользы.
Выгрузок вакансий в репозитории нет. База живёт локально. В
samples/лежит демонстрационный корпус на 52 вымышленные вакансии — его кладётseedи на нём собрана витрина; настоящих объявлений здесь нет ни одного.Модель эмбеддингов выбрана по весу, а не по качеству. Из многоязычных, доступных в fastembed,
paraphrase-multilingual-MiniLM-L12-v2— самая лёгкая (220 МБ против 2.24 ГБ уmultilingual-e5-large). Насколько это стоит recall, видно в разделе выше.compare_to_profileне проверяет требования к переезду и английскому: в данных hh нет полей, по которым это можно утверждать, а гадать по ключевым словам в описании — врать с уверенным лицом.
Разработка
# uv sync приводит окружение в точное соответствие набору extra,
# поэтому для разработки нужны оба: без rag не соберётся семантический поиск,
# без dev не будет pytest.
uv sync --extra dev --extra rag
uv run pytest -q # юнит-тесты, база не нужна
uv run ruff check src tests
uv run mypy
uv run alembic check # схема и модели не разошлисьТесты, которым нужна живая база или настоящая модель, помечены маркерами:
uv run pytest -m integration # требует docker compose up -d db
uv run pytest -m embeddings # скачивает модельЛицензия
MIT — см. LICENSE.
Available Tools
7 toolscompare_to_profileA
Честно сопоставляет вакансию с профилем кандидата (profile.yaml): verdict good/stretch/no, совпавшие и недостающие навыки, а если verdict «no» — конкретные причины отказа (blockers): зарплата ниже минимума, требуемый опыт выше принимаемого, не подходящая занятость, не удалённая работа, стоп-слово из red_flags. Используйте перед тем, как рекомендовать вакансию кандидату как хороший вариант — не оценивайте пригодность вакансии самостоятельно. По умолчанию грузит profile.yaml из корня проекта (шаблон — profile.example.yaml); profile_path позволяет указать другой файл.
Inputs: vacancy_id (required), profile_path. Returns: object
db_statusA
Служебная информация о состоянии базы: сколько вакансий/работодателей/навыков загружено, сколько описаний уже проиндексировано эмбеддингами, какой моделью они посчитаны, за какой период данные и когда их в последний раз собирали. Вызывайте в начале работы, чтобы понимать границы доступных данных и не выдумывать вакансии или статистику, которых в базе нет. Параметров не принимает.
Inputs: no named parameters. Returns: object
get_vacancyA
Полная карточка одной вакансии по её id из hh.ru: незакрытое описание, работодатель, все требуемые навыки, ссылка на hh.ru. Используйте после search_vacancies/semantic_search, чтобы изучить конкретную вакансию подробно, или перед compare_to_profile.
Inputs: vacancy_id (required). Returns: object
market_overviewA
Общий срез рынка по фильтру: сколько вакансий, у скольких указана зарплата и её персентили (p25/p50/p75), доля удалённых, разбивка по требуемому опыту и топ-10 работодателей. Используйте для агрегированных вопросов вида «какие сейчас зарплаты» или «много ли удалёнки» — не для списка конкретных вакансий.
Inputs: query, area_id, published_within_days. Returns: object
search_vacanciesA
Полнотекстовый поиск вакансий по словам в названии и описании (Postgres tsvector, синтаксис websearch: кавычки для точной фразы, минус перед словом — исключить его). Используйте, когда запрос содержит конкретные термины: должность, технологию, компанию. Для поиска по смыслу и синонимам без точных слов используйте semantic_search. Пустой query — не ошибка, вернутся вакансии по фильтрам, отсортированные по дате публикации. Описание в каждом результате обрезано; полный текст — через get_vacancy по id.
Inputs: limit, query, area_id, experience, +3 more. Returns: object
semantic_searchA
Поиск вакансий по смыслу через векторные эмбеддинги описаний, а не по точным словам. Используйте, когда запрос описывает задачу своими словами или ищет по концепции без гарантии, что в вакансии есть именно эти слова. Для поиска по конкретным терминам используйте search_vacancies — он быстрее и не требует установленного extra rag. Возвращает вакансии с оценкой похожести (similarity).
Inputs: query (required), limit, min_similarity. Returns: object
skill_statsA
Статистика по навыкам: какие технологии чаще всего требуются в подходящих под фильтр вакансиях, их доля и медианная зарплата вакансий, где навык нужен. Используйте для вопросов «что сейчас востребовано» или «что учить». Не возвращает список вакансий — для него используйте search_vacancies.
Inputs: query, top_n, area_id, published_within_days. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 7 tools
Each tool targets a distinct function: lexical vs semantic search, individual vacancy detail, skill statistics, market aggregates, candidate-fit comparison, and database metadata. The overlapping search/stat tools explicitly cross-reference each other to prevent misselection.
Most names are lowercase snake_case and use clear noun/verb tokens, but the pattern is mixed: search_vacancies/get_vacancy/compare_to_profile are verb-first, while skill_stats/market_overview/db_status are noun-first. Still predictable and readable.
Seven tools cover the job-search niche without redundancy; each one serves a clear use case and none feel like filler.
The surface covers the full workflow: discover via search/semantic search, inspect via get_vacancy, analyze via skills/market tools, evaluate fit via compare_to_profile, and understand data boundaries via db_status. No critical missing operation for the stated purpose.
Related MCP Connectors
Salary and H-1B disclosed-wage data as agent tools.
Job-posting search (jobicy, HN Who is Hiring, arbeitnow) and US H-1B salary data as agent tools.
CareerProof MCP gives AI agents direct access to a professional-grade career and workforce intelligence platform. Two namespaces: atlas_* for HR/TA teams (candidate evaluation, batch shortlisting, competency scoring, interview generation, JD analysis, custom eval frameworks, research reports) and ceevee_* for professionals (CV optimization, career positioning, salary intelligence, market reports). Backed by RAG knowledge from 50+ premium research sources (McKinsey, BCG, HBR, Gartner, WEF)
HR and recruiting: score a job description, or generate a structured role profile. No API key.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables AI assistants to search job vacancies, manage resumes, and apply to jobs on HeadHunter (hh.ru), Russia's largest job search platform. Includes OAuth 2.0 integration for secure job applications and an automated vacancy hunter agent with intelligent matching.37MIT
- AlicenseCqualityBmaintenanceIntegrates with HuntFlow ATS to manage vacancies, candidates, resumes, and recruitment stages via 7 tools and 2 skill prompts.1437 npm1MIT
- AlicenseNot gradedqualityFmaintenanceEnables AI assistants to access and manage HeadHunter job platform data, including vacancies, resumes, negotiations, and employer settings via 167+ tools.124 npm5MIT
- FlicenseAqualityDmaintenanceEnables to interact with hh.ru (a Russian job platform) through browser automation, allowing users to search for jobs, manage resumes, apply to vacancies with cover letters, and track application statuses via natural language.93-