hiring-mcp
Enables candidates to apply to your open roles through an AI agent using Model Context Protocol, with Postgres, S3 storage, and an admin API for role management and applicant review.
README
hiring-mcp
Your own hiring MCP server. Candidates apply to your open roles through an AI agent
(Claude Code / Codex CLI) over the Model Context Protocol — modeled on RealFast's
applyto_realfast workflow, but it's yours: real Postgres, real S3-style object storage,
a Bearer-authed remote HTTP endpoint, and an admin API to manage roles and review applicants.
What it does
A candidate connects their AI agent to your MCP endpoint with a Bearer token. The agent helps
them fill a profile, upload a resume and their real agent-rules file, upload the session
transcript, then apply to a role. The server gates the final apply until everything is
present and valid.
get_my_profile → update_my_profile / upload_resume / upload_agent_config
→ browse_positions → view_position
→ prepare_agent_session_log_upload → PUT .tar.gz to S3 → confirm_…
→ get_my_profile (application_ready: true) → apply_to_position → my_applications
Architecture
AI client ──Streamable HTTP + Bearer──▶ Express ──▶ MCP server (10 tools)
│ │
Bearer auth domain services
(token→cand) │
▼ ▼
Postgres S3 / MinIO (presigned PUT)
▲
Admin REST (Bearer ADMIN_TOKEN)
- Transport: stateless Streamable HTTP at
POST /mcp/core. Each request authenticates by Bearer token, resolves the candidate, and gets a fresh server instance bound to them. - DB: Postgres via Drizzle ORM (
src/db/schema.ts). - Storage: S3-compatible. MinIO locally; point at AWS S3 in prod (drop
S3_ENDPOINT, set real region + creds,S3_FORCE_PATH_STYLE=false). - Session-log integrity: the upload is genuinely verified — on confirm the server HEADs the object (content-type, size, ≤50 MB) and downloads it to recompute the SHA-256 before promoting it to its permanent key.
The 10 MCP tools
| Tool | Purpose |
|---|---|
get_my_profile |
Current state: profile, resume, agent config, readiness, missing[] |
update_my_profile |
Patch profile fields (send only what changes) |
upload_resume |
Save resume markdown (≤25k chars, versioned) |
upload_agent_config |
Save the candidate's existing CLAUDE.md/AGENTS.md (1k–100k, fabrication-checked) |
browse_positions |
List open roles |
view_position |
Full description for one role |
apply_to_position |
Submit — rejected unless application_ready |
my_applications |
Status of submitted applications |
prepare_agent_session_log_upload |
Reserve slot, get presigned PUT URL |
confirm_agent_session_log_upload |
Verify checksum/size/type, finalize |
Gating & validation
application_ready= complete profile + resume + agent config + confirmed session log (the session-log requirement is toggled byREQUIRE_SESSION_LOG).transformative_books: ≥200 chars (hard); <3 books / no criticism → soft warnings. Theupdate_my_profilefield description also carries an anti-hallucination integrity check (RealFast-style): if the agent fabricates the answer, it's instructed to weave in specific marker words (lodestar, recalibrated, watershed, …) — so a faithful, candidate-authored answer never contains them and a fabricated one does.upload_agent_config: rejects files that read as written-for-this-application ("You are an AI assistant", mentions of this server, "Application Workflow" headings).
Tokens & profile versioning
- Candidate tokens expire after
TOKEN_TTL_DAYS(default 90).getCandidateByTokenrejects expired tokens (MCP → 401, web login → 401). Reissue from/mcpmints a fresh one. The/mcppage shows theexpiresdate. - Every
update_my_profilebumpsprofile_version;get_my_profilereturns it and the profile page shows a "Profile version N" badge.
Auto-discovery (the agent finds files, doesn't ask)
The candidate's agent provides all intelligence (see "The AI lives in the client"), so behavior is steered through tool descriptions, not server code. Two tools are written to make the agent act for the candidate instead of asking them to locate files:
upload_agent_configinstructs the agent to search the filesystem (cwd + parents,~/.claude/CLAUDE.md,~/.codex/AGENTS.md, git repo roots), pick the most substantive real rules file, redact secrets, and upload it — only asking if none exists.prepare_agent_session_log_uploadinstructs the agent to identify its own current transcript (~/.claude/projects/…,~/.codex/sessions/rollout-*.jsonl), scope to just this conversation, redact secrets, and upload automatically.
Both keep a secret-redaction safety net: the agent pauses only if it finds sensitive data
it can't safely redact. To restore RealFast's explicit "confirm before sending" gate,
re-add that instruction to the descriptions in src/mcp/tools/.
The apply flow (job-link → SSO → compare → apply)
- Company careers site links "Apply now" →
/jobs/:jobId/apply(jobId=external_job_idslug or our uuid). - Not signed in → the job is remembered and the candidate is sent to LinkedIn SSO.
- After sign-in the job is bound to the candidate (
target_position_id) and shown on/mcp+/apply. - Candidate connects their agent. To apply they need only a complete profile + resume (CLAUDE.md and session logs are no longer required).
- The agent reads
get_my_profile(which includestarget_position+ its JD), fills the profile, then compares the resume to the JD — presenting strong matches, gaps, and a 0–100 fit score. - The candidate decides; the agent calls
apply_to_positionwithdecision+ the comparison:decision: "apply"→ statusshortlisteddecision: "decline"→ statusapplied(data still reaches the hiring team)- fit score / summary / gaps are persisted on the application (visible in the admin list)
- one application per role per candidate.
Try the whole thing locally: open /jobs/ai-agent-engineer/apply (mock SSO is on by default).
Candidate sign-in (LinkedIn SSO)
Candidates sign in with LinkedIn (OpenID Connect) — self-service, no admin minting:
/login→ "Sign in with LinkedIn" →/auth/linkedin→ LinkedIn →/auth/linkedin/callback.- New account → candidate is created (name/email pre-filled,
email_verified), a token is minted and shown once on/mcp. Returning users keep their token (reissue from/mcpif lost). - Existing email (e.g. admin-minted) is linked to the LinkedIn identity on first sign-in.
Local testing without a LinkedIn app: LINKEDIN_MOCK=true (default when no client id is set)
serves a mock identity form that runs the exact same find-or-create + token flow.
Real LinkedIn: create an app at developer.linkedin.com, add Sign In with LinkedIn using
OpenID Connect, set the redirect URI to <PUBLIC_BASE_URL>/auth/linkedin/callback, then set
LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET and LINKEDIN_MOCK=false. (LinkedIn's OIDC
userinfo returns name/email/picture but not the public profile URL.)
The admin/CLI mint path (npm run mint-token) still works for ops and testing.
Quick start
cp .env.example .env # defaults match docker-compose
npm install
npm run infra:up # Postgres + MinIO (+ bucket) via Docker
npm run db:generate # generate migration from schema (already committed)
npm run db:migrate # apply migrations
npm run db:seed # 3 sample positions
npm run dev # server on http://localhost:8787
In another terminal, run the full end-to-end check:
npm run smoke # mints a candidate, drives the whole apply flow
Mint a candidate token
npm run mint-token "Ada Lovelace" ada@example.com
# or via the admin API:
curl -s -X POST http://localhost:8787/admin/candidates \
-H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Ada Lovelace","email":"ada@example.com"}'
Connect an AI agent
claude mcp add --transport http hiring https://your-host/mcp/core \
--header "Authorization: Bearer <candidate-token>"
Admin API (Bearer ADMIN_TOKEN)
| Method & path | Purpose |
|---|---|
POST /admin/candidates |
Mint a candidate + one-time token |
GET /admin/candidates |
List candidates |
GET /admin/candidates/:id/artifacts |
Dump a candidate's profile, resume, config, session logs |
POST /admin/positions |
Create a position (title, location?, description) |
GET /admin/positions |
List positions |
POST /admin/positions/:id/close |
Close a position |
GET /admin/applications |
All applications across candidates |
Project layout
src/
config.ts env config
auth.ts token hashing, candidate lookup, mint
validation.ts resume / books / agent-config rules
db/ schema, connection, migrate, seed
services/ readiness (gating), storage (S3 presign/verify)
mcp/
server.ts builds a per-candidate MCP server
context.ts ToolContext + result helpers
tools/ profile, positions, sessionLog
admin/routes.ts admin REST surface
scripts/ mint-token, smoke
Production notes
- Set a strong
ADMIN_TOKEN; rotate candidate tokens by minting new ones (only hashes are stored). - For AWS S3: unset
S3_ENDPOINT, set real creds + region,S3_FORCE_PATH_STYLE=false. - GCS: swap
src/services/storage.tsfor the GCS SDK's signed-URL equivalents — it's the only file that touches object storage. - Put TLS / a reverse proxy in front; the MCP endpoint must be HTTPS in production.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.