hh-apply-agent-mcp
README.md
# browser-control-mcp
Локальный MCP-сервер с открытым исходным кодом для безопасного управления существующей Chrome-сессией через Playwright и Chrome DevTools Protocol.
Codex вызывает MCP tools, сервер управляет вкладками в Chrome, а действия с повышенным риском требуют отдельного подтверждения. Автоматизация hh.ru остаётся специализированным модулем проекта. Проект не является официальным продуктом Google, Chromium или hh.ru.
## Что делает MVP
- Подключается к существующей локальной Chrome-сессии через CDP.
- Открывает, читает и закрывает вкладки, нажимает элементы и заполняет поля.
- Сохраняет авторизацию внутри Chrome и не возвращает cookies или browser storage MCP-клиенту.
- Классифицирует риск действий и требует одноразовое подтверждение для опасных операций.
- Блокирует платёжные действия, покупки и денежные переводы.
- Предоставляет специализированные сценарии автоматизации hh.ru.
- Открывает ссылку вакансии в Chrome с текущей пользовательской сессией.
- Проверяет, что пользователь авторизован на hh.ru.
- Парсит вакансию.
- Помогает подготовить сопроводительное письмо.
- Находит вопросы работодателя.
- Заполняет ответы и письмо только после подтверждения пользователя.
- Не отправляет отклик без явного подтверждения.
## Архитектура
```mermaid
flowchart LR
User["Пользователь"] --> Codex["Codex"]
Codex --> MCP["Local MCP server"]
MCP --> HH["hh.ru flows"]
MCP --> BrowserControl["Universal browser-control"]
HH --> Playwright["Playwright connectOverCDP"]
BrowserControl --> Playwright
Playwright --> Chrome["Существующая Chrome-сессия пользователя"]
```
## Стек
| Компонент | Выбор |
| -------------------- | ------------------------------------- |
| Язык | TypeScript |
| Runtime | Node.js 20+ |
| MCP | `@modelcontextprotocol/sdk` |
| Управление браузером | Playwright + Chrome DevTools Protocol |
| Валидация | Zod |
| Логирование | pino |
| Тесты | Vitest |
## Локальный запуск
```bash
npm install
cp .env.example .env
npm run typecheck
npm test
```
Для MVP Chrome нужно запустить с локальным CDP-портом. Это даёт Playwright возможность подключиться к уже существующему профилю Chrome, где есть ваша hh.ru-сессия.
### macOS
```bash
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-address=127.0.0.1 \
--remote-debugging-port=9222 \
--user-data-dir="$HOME/Library/Application Support/Google/Chrome"
```
### Linux
```bash
google-chrome \
--remote-debugging-address=127.0.0.1 \
--remote-debugging-port=9222 \
--user-data-dir="$HOME/.config/google-chrome"
```
Если команда `google-chrome` недоступна, проверьте имя бинарника:
```bash
which google-chrome chromium chromium-browser
```
### Windows PowerShell
```powershell
& "C:\Program Files\Google\Chrome\Application\chrome.exe" `
--remote-debugging-address=127.0.0.1 `
--remote-debugging-port=9222 `
--user-data-dir="$env:LOCALAPPDATA\Google\Chrome\User Data"
```
Если Chrome установлен в 32-bit путь:
```powershell
& "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" `
--remote-debugging-address=127.0.0.1 `
--remote-debugging-port=9222 `
--user-data-dir="$env:LOCALAPPDATA\Google\Chrome\User Data"
```
Важно: CDP-доступ к Chrome очень мощный. Не открывайте debug-port на внешний интерфейс. Для MVP используйте только `127.0.0.1`.
## Переменные окружения
```env
NODE_ENV=development
LOG_LEVEL=info
CDP_URL=http://127.0.0.1:9222
ALLOWED_DOMAINS=hh.ru,ramenskoe.hh.ru
DRY_RUN=true
REQUIRE_CONFIRMATION=true
BROWSER_ALLOWED_DOMAINS=
BROWSER_DENIED_DOMAINS=
BROWSER_ALLOW_PRIVATE_NETWORK=false
APPLICATION_STORE_PATH=storage/applications.json
```
## Модель безопасности
MVP намеренно ограничивает автоматизацию:
- без хранения логинов, паролей, cookies и токенов;
- без автоматического входа в аккаунт;
- по умолчанию включён dry-run;
- без отправки отклика без явного подтверждения пользователя;
- остановка на неизвестных вопросах или недостающих данных кандидата.
- universal browser-control не возвращает cookies, localStorage, sessionStorage,
hidden inputs или значения полей;
- платёжные действия, покупки и денежные переводы полностью заблокированы в MVP;
- sensitive/irreversible действия выполняются только по одноразовому токену;
- private network и опасные URL-схемы по умолчанию заблокированы.
## Реализованные MCP tools
| Tool | Ответственность |
| ---------------------------------------- | -------------------------------------------------------------------------------- |
| `health_check` | Вернуть конфигурацию локального MCP-агента |
| `inspect_vacancy` | Открыть allowlisted hh.ru URL, проверить сессию, проверить страницу и распарсить |
| `prepare_required_cover_letter_response` | Проверить обязательность письма и создать одноразовый токен подтверждения |
| `submit_required_cover_letter_response` | Заполнить одобренное письмо и выполнить один submit, если выключен dry-run |
| `prepare_attach_cover_letter` | Проверить возможность добавить письмо к уже отправленному отклику |
| `submit_attach_cover_letter` | Добавить одобренное письмо к существующему отклику по одноразовому токену |
| `prepare_response_with_cover_letter` | Подготовить отклик с необязательным письмом и создать одноразовый токен |
| `submit_response_with_cover_letter` | Откликнуться и сразу отправить письмо в той же вкладке |
| `browser_list_pages` | Получить открытые Chrome-вкладки и их CDP `pageId` |
| `browser_open_page` | Открыть разрешённый публичный HTTP(S) URL |
| `browser_get_page` | Получить ограниченный semantic snapshot без значений полей |
| `browser_click` | Нажать элемент либо подготовить confirmation token |
| `browser_fill` | Заменить значение поля без логирования текста |
| `browser_type` | Дописать текст в поле без логирования текста |
| `browser_close_page` | Закрыть вкладку; изменённая вкладка требует подтверждения |
| `browser_wait_for` | Дождаться элемента, текста или URL |
| `browser_screenshot` | Вернуть PNG в памяти без сохранения на диск |
| `browser_execute_confirmed_action` | Один раз выполнить точное действие по confirmation token |
`inspect_vacancy` пишет локальные диагностические логи в stderr. Это важно для MCP stdio: stdout должен оставаться чистым JSON-RPC транспортом.
Flow обязательного письма всегда состоит из двух вызовов. Токен действует 10 минут,
привязан к вакансии и точному тексту письма и используется только один раз.
При `DRY_RUN=true` второй tool открывает и заполняет форму, но не нажимает финальную
кнопку. MCP-сервер не сохраняет письмо на диск и не пишет письмо или токен в логи.
Для отклика, который уже доставлен без письма, используется отдельная пара
`prepare_attach_cover_letter` -> `submit_attach_cover_letter`. Токены двух flow
имеют разное назначение и не могут использоваться взаимозаменяемо. Если письмо
уже приложено, tool возвращает идемпотентный статус `already_attached`.
Для вакансий с необязательным письмом используется пара
`prepare_response_with_cover_letter` -> `submit_response_with_cover_letter`.
После одного клика «Откликнуться» tool классифицирует результат в той же вкладке:
- вопросы работодателя возвращаются как `EmployerQuestion[]` без заполнения;
- открытая форма письма заполняется подтверждённым текстом и отправляется один раз;
- после доставки резюме кнопка приложения письма ожидается до трёх секунд;
- если бот работодателя успел убрать кнопку, возвращается частичный успех
`resume_submitted_letter_unavailable`.
`executionMode: "full"` выполняет полный сценарий. `executionMode: "start_only"`
разрешает только первый разрушительный шаг и не отправляет письмо. При `DRY_RUN=true`
оба режима безопасны: основная кнопка «Откликнуться» не нажимается. Если письмо
обязательно, prepare-tool возвращает статус `required_cover_letter` и токен для
существующего `submit_required_cover_letter_response`.
## Универсальное управление браузером
Модуль `src/browser-control` работает поверх той же Chrome CDP-сессии, но не зависит
от hh.ru. Cookies остаются внутри пользовательского `BrowserContext`: MCP использует
авторизованные вкладки, не читая и не передавая cookies наружу.
`pageId` — Chrome CDP `targetId`. Пока вкладка и Chrome-процесс живы, MCP может после
собственного перезапуска заново перечислить targets и продолжить работу с тем же ID.
После закрытия вкладки ID становится stale. После перезапуска Chrome автоматического
восстановления по URL/title нет, потому что одинаковых вкладок может быть несколько.
Риски определяет сервер:
| Риск | Поведение |
| -------------- | ------------------------------------------------------------------- |
| `read` | Выполняется без подтверждения |
| `low` | Выполняется сразу |
| `sensitive` | Возвращается одноразовый token с TTL 2 минуты |
| `irreversible` | Всегда требуется одноразовый token |
| `critical` | Payment/purchase/transfer блокируются без возможности подтверждения |
`requestedRisk` может повысить риск, но не понизить вычисленный сервером. Перед
confirmed action повторно проверяются `pageId`, URL, origin, версия документа и
fingerprint элемента. После необратимого клика автоматических retry нет.
По умолчанию разрешены публичные HTTP(S)-домены. Ограничить их можно через
`BROWSER_ALLOWED_DOMAINS`, запретить отдельные — через `BROWSER_DENIED_DOMAINS`.
Доступ к localhost/private network включается только явно:
```env
BROWSER_ALLOW_PRIVATE_NETWORK=true
```
## Планируемые MCP tools
| Tool | Ответственность |
| ----------------------- | ---------------------------------------------- |
| `parse_resume_context` | Извлечь контекст резюме кандидата |
| `fill_question_answers` | Заполнить ответы в видимые поля |
| `submit_response` | Поддержать остальные варианты отклика |
| `screenshot` | Вернуть текущее состояние браузера для отладки |
## Разработка
Перед изменениями читайте `PROJECT_CONTEXT.md`. Это рабочая память проекта, её нужно обновлять после важных решений.
Проект также используется как учебная TypeScript-практика. Поэтому крупные фичи лучше делать маленькими шагами: сначала объяснить интерфейсы и границы модулей, затем реализовать один слой, проверить тестами и только потом двигаться дальше.
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues