A source-available MCP server for HR and recruiting workflows. Scores candidates against job descriptions using Claude, returns structured dimension scores, strengths, and gaps. Any agent framework that speaks MCP can call it natively.
Hosted API — use it without running anything: recruitapi.app Self-host — deploy to Railway in under 15 minutes (see below)
Score a candidate's resume against a job description.
Request
{
"resume_text": "...",
"job_description": "...",
"weights": {
"skills_match": 0.40,
"experience": 0.30,
"industry_background": 0.20,
"education": 0.10
},
"recency_window_years": 10
}| Field | Type | Required | Default |
|---|---|---|---|
resume_text |
string | yes | — |
job_description |
string | yes | — |
weights |
object | no | {skills_match: 0.40, experience: 0.30, industry_background: 0.20, education: 0.10} |
recency_window_years |
integer > 0 | no | 10 |
Weights must sum to 1.0. Each value must be between 0.0 and 1.0.
Both text fields max out at 15,000 characters. Resume must be at least 50 words.
Response — 200
{
"overall_score": 82,
"dimension_scores": {
"skills_match": 88,
"experience": 85,
"industry_background": 74,
"education": 70
},
"strengths": [
"Five years of hands-on Python in production ML pipelines",
"Led cross-functional team during platform migration"
],
"gaps": [
"No experience with Kubernetes",
"MBA preferred; candidate holds a BS"
],
"recency_window_used": 10,
"model": "claude-sonnet-4-20250514",
"warnings": []
}All scores are integers 0–100. strengths and gaps are 2–4 strings each, grounded in the resume and job description — not generic observations.
Idempotency
Pass an Idempotency-Key header to cache the response for 24 hours. A retry with the same key returns the cached response without calling the model or deducting a credit.
Idempotency-Key: req_01J8XYZ
{ "status": "ok", "model": "claude-sonnet-4-20250514" }Every request to /score-candidate requires an API key. Pass it either way:
X-API-Key: hrmcp_sk_...
Authorization: Bearer hrmcp_sk_...
Keys are issued after a credit purchase. Get one at hrmcp-server-production.up.railway.app/billing.
Limits apply per API key on a rolling window — not a fixed clock boundary.
| Limit | Default |
|---|---|
| Per minute | 30 requests |
| Per day | 500 requests |
| Concurrent | 5 in-flight |
Every response includes current limit state:
X-RateLimit-Limit-Minute: 30
X-RateLimit-Remaining-Minute: 28
X-RateLimit-Reset-Minute: 1712000060
X-RateLimit-Limit-Day: 500
X-RateLimit-Remaining-Day: 497
X-RateLimit-Reset-Day: 1712041234
Self-hosted deployments can disable rate limiting entirely with RATE_LIMIT_ENABLED=false.
Each successful call to /score-candidate deducts one credit. Credits are purchased in bundles of 100 for $5 and expire after 180 days.
| Status | Code | Meaning |
|---|---|---|
| 402 | credits_exhausted |
Balance is zero |
| 402 | credits_expired |
Credits exist but the bundle has expired |
All errors use the same envelope:
{
"error": {
"code": "missing_required_field",
"message": "One or more required fields are missing.",
"detail": { "missing_fields": ["resume_text"] }
}
}| Code | Status | Description |
|---|---|---|
unauthorized |
401 | Missing, invalid, or revoked API key |
rate_limit_exceeded |
429 | Rolling window limit hit; see Retry-After header |
credits_exhausted |
402 | No credits remaining |
credits_expired |
402 | Credits have passed their expiry date |
missing_required_field |
400 | resume_text or job_description absent |
resume_too_short |
400 | Resume under 50 words |
payload_too_large |
400 | Input exceeds 15,000 characters |
weights_invalid_sum |
400 | Weights do not sum to 1.0 |
weights_missing_keys |
400 | One or more weight keys absent |
weights_negative_value |
400 | Weight value outside 0.0–1.0 |
recency_window_invalid |
400 | recency_window_years is not a positive integer |
idempotency_key_invalid |
400 | Idempotency-Key header value is invalid |
model_unavailable |
503 | Anthropic API timeout or outage |
response_parse_failure |
500 | Model returned unparseable output |
Warnings are non-fatal and appear alongside a 200 response:
| Code | Trigger |
|---|---|
non_english_detected |
Non-Latin characters in resume or JD |
job_description_thin |
JD under 30 words |
- Click the button above — Railway clones the repo and provisions a Postgres service
- Add the five required environment variables (see below)
- Register
POST /webhooks/stripein your Stripe dashboard; paste the signing secret intoSTRIPE_WEBHOOK_SECRET - Watch the deploy log for
db_migratedandserver_started - Hit
/healthto confirm
git clone https://github.com/Spaceghost99/hrmcp-server.git
cd hrmcp-server
npm install
cp .env.example .env
# Fill in ANTHROPIC_API_KEY at minimum
npm run devScoring works without DATABASE_URL. Auth and billing require Postgres.
Five variables are required and will crash the server on startup if missing.
| Variable | Required | Default | Description |
|---|---|---|---|
ANTHROPIC_API_KEY |
yes | — | Anthropic API key |
DATABASE_URL |
prod only | — | PostgreSQL connection string (Railway provides this) |
STRIPE_SECRET_KEY |
prod only | — | Stripe secret key (sk_test_... or sk_live_...) |
STRIPE_WEBHOOK_SECRET |
prod only | — | Stripe webhook signing secret (whsec_...) |
PORT |
no | 3000 |
Railway sets this automatically |
NODE_ENV |
no | development |
Set to production on Railway |
ANTHROPIC_MODEL |
no | claude-sonnet-4-20250514 |
Model for scoring |
ANTHROPIC_TIMEOUT_MS |
no | 25000 |
Anthropic API timeout in ms |
BCRYPT_ROUNDS |
no | 10 |
bcrypt cost factor for key hashing |
RATE_LIMIT_ENABLED |
no | true |
Set false to disable rate limiting |
RATE_LIMIT_PER_MINUTE |
no | 30 |
Rolling per-minute limit per key |
RATE_LIMIT_PER_DAY |
no | 500 |
Rolling per-day limit per key |
RATE_LIMIT_CONCURRENT |
no | 5 |
Max in-flight requests per key |
MAX_RESUME_CHARS |
no | 15000 |
Resume character limit |
MAX_JD_CHARS |
no | 15000 |
Job description character limit |
STRIPE_PRICE_LOOKUP_KEY |
no | credits_100 |
Stripe price lookup key |
CREDITS_BUNDLE_SIZE |
no | 100 |
Credits per purchase |
CREDITS_EXPIRY_DAYS |
no | 180 |
Days before credits expire |
LOG_LEVEL |
no | info |
info or error |
IDEMPOTENCY_TTL_SECONDS |
no | 86400 |
Idempotency cache TTL in seconds |
APP_URL |
no | http://localhost:3000 |
Public base URL; used in billing links |
See .env.example for descriptions of every variable.
src/
index.ts — HTTP server, request routing, middleware wiring
config.ts — Environment variable loading
middleware/
auth.ts — API key extraction and validation
rateLimit.ts — Rolling window rate limiter (in-memory)
logger.ts — Structured JSON logging
billing/
keys.ts — Key generation, hashing, verification (pure functions)
db.ts — PostgreSQL pool, schema migration, all queries
stripe.ts — Checkout sessions, webhook handler, credit lifecycle
tools/
score-candidate/
handler.ts — Input validation, orchestration
scorer.ts — Anthropic API call, response parsing
schema.ts — Zod schemas and default weights
prompt.ts — System prompt and user prompt builder
errors/
codes.ts — Error code constants
envelope.ts — createError / createWarning helpers
Request flow for POST /score-candidate:
- Auth — extract key, SHA-256 lookup, bcrypt verify
- Rate limit — check rolling minute/day/concurrent windows
- Parse + validate body
- Idempotency — return cached response if key matches
- Deduct credit — atomic
SELECT FOR UPDATEin Postgres - Score — call Anthropic, parse and validate response
- Cache response if idempotency key present
- Apply
X-RateLimit-*headers, send response - Release concurrent slot, write request log line
Other hosted candidate-scoring options exist. hrmcp-server is source-available, self-hostable, and speaks MCP natively — no wrapper required for agent frameworks that support the protocol.
Licensed under the Elastic License 2.0. Free to use, self-host, and modify. You may not offer the software to third parties as a hosted or managed service. For commercial hosting inquiries contact aaron@recruitapi.app.