Batcave-MCP
Batcave — MCP-сервер для проверки резюме
MCP-сервер, который принимает два документа — резюме и описание вакансии — и прогоняет их через трехэтапную проверку. Каждый этап служит входом для следующего: нельзя переписывать, пока нет отчета о соответствии, и нельзя запускать проход ATS, пока нет переписанной версии.
Конвейер
Инструмент | Что делает |
| Прием. Принимает резюме и описание вакансии в виде обычного текста или пути к файлу |
| Этап 1. Старший рекрутер в целевой компании: оценка соответствия из 100, топ-5 недостающих ключевых слов, 3 красных флага, которые менеджер по найму замечает менее чем за 10 секунд. |
| Этап 2. Переписывает раздел об опыте, чтобы включить ключевые слова этапа 1 и убрать его красные флаги, каждый пункт в форме Google XYZ — достиг X, измеряемый Y, путем выполнения Z. |
| Этап 3. Проход парсера ATS плюс менеджер по найму на резюме №147 из 200: какие разделы пропускаются, затем переписывает их, чтобы остановить прокрутку. Возвращает итоговое резюме. |
| Какие этапы завершены, ожидают результата или не начаты, и что вызывать дальше. |
| Сохраненные сессии, сначала самые недавно обновленные. |
| Возвращает всю проверку — все три этапа плюс итоговое резюме — в виде одного markdown-документа. |
| Удаляет сессию и все, что с ней связано. Ничто не истекает само по себе. |
Related MCP server: resume-fit-scanner
Как работает этап
Сервер не вызывает модель. Он составляет бриф, хранит состояние и обеспечивает порядок; модель подключенного клиента выполняет рассуждения. Поэтому каждый инструмент этапа вызывается дважды:
{ session_id }— возвращает аналитический бриф для этого этапа, с уже встроенными резюме, описанием вакансии и результатами всех предыдущих этапов.{ session_id, result }— записывает ответ.resultпроверяется по схеме этапа, поэтому отчет с четырьмя ключевыми словами вместо пяти отклоняется, а не сохраняется.
Этап 2 читает записанный отчет этапа 1. Этап 3 читает updated_resume из этапа 2, а не оригинал. Этап, вызванный не по порядку, завершается ошибкой с именем инструмента, который нужно вызвать первым.
Два правила, заложенные в брифы
Никаких выдуманных метрик. Если в исходном резюме нет числа, переписанная версия выдает
[QUANTIFY: what to measure]и перечисляет его вplaceholders_needing_user_input.Никакого набивания ключевыми словами. Ключевое слово включается только там, где его поддерживает реальный опыт; остальные возвращаются в
keywords_not_addressedс указанием причины.
Транспорты
Две точки входа, одни и те же инструменты:
Вход | Транспорт | Для |
| stdio | Клиент на той же машине — Claude Code, IDE |
| Streamable HTTP на | Удаленный клиент — именно это работает в контейнере |
stdio — это канал между двумя процессами на одной машине; по сети до него не добраться. Контейнер, обслуживающий stdio, не принимал бы никаких соединений, поэтому путь EC2 использует serve.ts.
serve.ts требует две переменные и отказывается запускаться без каждой из них:
DB_URL— строка подключения к PostgresMCP_AUTH_TOKEN— общий секрет; каждый запрос требуетAuthorization: Bearer <token>
GET /healthz — единственный маршрут без аутентификации. Он не открывает подключение к базе данных, поэтому балансировщик нагрузки, опрашивающий его, никогда не будит Postgres.
Хранилище
Все хранится в Postgres. Сервер ничего не записывает на локальный диск — единственные локальные чтения — это файлы резюме и описания вакансии, на которые вы указываете.
resume_sessions(id, created_at, updated_at, company, role,
resume jsonb, job_description jsonb)
resume_stages(session_id -> resume_sessions.id on delete cascade, stage, status,
issued_at, completed_at, result jsonb, primary key (session_id, stage))
schema_migrations(module, id, applied_at) -- shared, owned by src/platform/db.tsДве таблицы вместо одного документа, поэтому запись этапа добавляет одну строку, а не переписывает оба резюме, и list_sessions вообще не выбирает текст документа. Таблицы имеют префикс модуля, а миграции выполняются лениво при первом запросе этого модуля — запуск сервера не будит базу данных.
Миграции являются append-only и записываются в schema_migrations, поэтому каждая выполняется ровно один раз на базу данных. bun run db:migrate применяет ожидающие; сервер также делает это лениво при первом запросе модуля в качестве запасного варианта.
Ничто не истекает. Сессии накапливаются, пока delete_session не удалит их.
Запуск
bun install
bun run dev # Postgres + the server, hot reload, nothing to configureЭто docker compose -f docker-compose.dev.yml up --build: он поднимает Postgres, создает базы данных dev и test, выполняет миграции и обслуживает MCP на http://127.0.0.1:3000/mcp с токеном dev-token-not-a-secret. Редактирование чего-либо в src/ перезагружает работающий сервер.
Чтобы запустить сервер напрямую на хосте вместо этого:
export DB_URL='postgres://postgres:postgres@localhost:55432/batcave'
bun start # stdio, for a client on this machine
bun run serve # HTTP on :3000, also needs MCP_AUTH_TOKENДве команды базы данных, ни одна из которых не требует работающего сервера:
bun run db:check # can this machine reach DB_URL, and what is in it?
bun run db:migrate # create or update the tables; safe to run repeatedlydb:check — единственное, что открывает соединение без обслуживания. Обе точки входа проверяют DB_URL при запуске, но подключаются лениво при первом запросе, поэтому чистый запуск ничего не доказывает.
Проверки:
bun run check # Biome format + lint (check:fix to apply)
bun run typecheck
bun test # unit tests; no database needed
TEST_DB_URL='postgres://postgres:postgres@localhost:55432/batcave_test' bun testСквозные тесты говорят на реальном сетевом протоколе с реальным Postgres и удаляют свои таблицы при завершении. Они читают TEST_DB_URL, намеренно не DB_URL, поэтому указание серверу на реальную базу данных не может активировать удаление — а dev-стек поставляет отдельную базу данных batcave_test, так что запуск тестов никогда не мешает работающему серверу.
.mcp.json в этом каталоге регистрирует stdio-сервер для Claude Code. Для другого клиента:
{ "command": "bun", "args": ["index.ts"], "cwd": "/path/to/Batcave" }Запуск на EC2
export DB_URL='postgres://user:pass@host/db?sslmode=require'
export MCP_AUTH_TOKEN="$(openssl rand -hex 32)"
bun run db:check # confirm the instance is reachable from this box
docker compose run --rm mcp bun scripts/migrate.ts # create the tables
docker compose up -d --build
docker compose logs -f mcpВыполните миграцию до того, как сервер начнет принимать трафик. Он сам выполнит миграцию при первом вызове инструмента, если вы пропустите этот шаг, но тогда сломанная миграция проявится как неудачный пользовательский запрос, а не как неудачный деплой, и первый вызывающий будет ждать схему. Повторно запускайте db:migrate при каждом деплое, который включает новую миграцию; это no-op, когда применять нечего.
Compose отказывается запускаться, если какая-либо переменная не задана. Храните их в профиле оболочки или в секрете инстанса — не в файле в этом репозитории.
Опубликованный порт — 127.0.0.1:3000, намеренно. Конечная точка говорит по открытому HTTP и аутентифицируется с помощью bearer-токена: в открытом интернете этот токен может прочитать любой на пути. Поставьте перед ним TLS — ALB, завершающий HTTPS и пересылающий на инстанс, или nginx/Caddy на той же машине, проксирующий на 127.0.0.1:3000. Тогда группа безопасности должна разрешать 443 от ваших клиентов и ничего больше; порт 3000 остается закрытым для мира.
Ротация токена — это export MCP_AUTH_TOKEN=... && docker compose up -d, что перезапускает контейнер. Токен один для всех — он никого не идентифицирует, поэтому не может отличить ваши сессии от сессий друга. Доступ для каждого пользователя требует настоящей аутентификации и колонки владельца в resume_sessions; ни того, ни другого пока нет.
resume_path разрешается внутри контейнера, поэтому удаленный вызывающий не может его использовать — пути на его ноутбуке ничего не значат для сервера. По HTTP передавайте resume_text и job_description_text. Смонтируйте том, если хотите, чтобы форма пути работала для файлов на машине.
docker-compose.yml — это только продакшн-стек. Локальная разработка использует docker-compose.dev.yml, который поднимает собственный Postgres и не разделяет ни одной из этих конфигураций.
Структура
Сервер является хостом для модулей. Модуль — это самодостаточное семейство инструментов, которое владеет своими таблицами и своим словарем. Проверка резюме — единственный такой модуль на сегодня; второй, не связанный, — это папка в src/features/ и одна запись в списке в index.ts.
index.ts stdio entrypoint
serve.ts HTTP entrypoint (the container runs this)
src/modules.ts the one list of mounted modules, shared by both entries
src/module.ts the ToolModule contract every feature implements
src/server.ts mounts modules onto an McpServer
src/http.ts Streamable HTTP handler, bearer auth, /healthz
src/platform/ feature-agnostic; knows nothing about resumes
db.ts lazy Postgres pool + per-module migration runner
documents.ts text / pdf / docx extraction
stored-document.ts what an extracted document looks like
tool-result.ts keeps `content` and `structuredContent` in step
src/features/resume-review/
index.ts the ToolModule: name, migrations, register()
migrations.ts this module's tables
sessions.ts repository, domain types, stage gating
briefs.ts the three briefs
schemas.ts zod schema per stage result
stage-tool.ts the brief-then-record tool shape
dossier.ts markdown rendering
tools/ one file per group of registered tools
intake.ts, stages.ts, dossier.ts, session-admin.tsДва правила поддерживают структуру:
src/platformникогда не импортирует изsrc/features. Все, что может понадобиться второму модулю, принадлежит платформе; все, что нужно только проверке резюме, остается в функциональности.Ни один модуль не импортирует другой модуль. Два модуля, которым нужно знать друг о друге, — это один модуль.
stage-tool.ts намеренно живет внутри функциональности, а не в платформе. Форма «сначала бриф, затем запись» может оказаться переиспользуемой, но сегодня у нее ровно один потребитель, а угадывание общего случая до появления второго — это то, как гниет слой платформы.
Добавление модуля
// src/features/interview-prep/index.ts
export const interviewPrep: ToolModule = {
name: "interview-prep",
migrations, // its own tables, namespaced in schema_migrations
register(server) {
registerWhateverTools(server);
},
};// index.ts
const server = createServer([resumeReview, interviewPrep]);Это весь контракт. Миграции применяются по одному разу, отслеживаются для каждого модуля в schema_migrations и выполняются лениво при первом обращении модуля к базе данных — неиспользуемый модуль не стоит ни одного запроса. tests/modules.test.ts проверяет стык с помощью заглушечного модуля, не имеющего отношения к резюме.
Участие в разработке
См. CONTRIBUTING.md — bun run dev — это вся настройка. Проблемы безопасности отправляются через SECURITY.md, а не через публичные issues.
Лицензия
MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
Tailor a CV to a job posting: score the match, propose a reviewable rewrite, export PDF or Word.
Free ATS resume score and job-match checker: scores a resume against a job description.
Score and tailor your CV/resume against a job posting — for AI agents and humans, no-login trial.
A job-search companion: tailor your CV to a role, score fit, fix ATS issues. Also via MCP.
Related MCP Servers
- FlicenseAqualityDmaintenanceEnables tailoring resumes to job descriptions by scraping JDs, applying rules, and generating optimized DOCX resumes.11-
- AlicenseNot gradedqualityDmaintenanceAnalyzes resume text against job descriptions to provide ATS fit scores, missing keywords, and suggestions.MIT
- AlicenseNot gradedqualityBmaintenanceEnables JD-aware resume matching through MCP tools, providing deterministic scoring, gap analysis, bullet rewrites, and tailored cover letter generation.19 npmMIT
- AlicenseAqualityDmaintenanceEnables MCP clients to analyze resumes for ATS compatibility, parse job descriptions, get optimization and roast-style critiques, and generate tailored resumes with shareable preview links.614 npmMIT