hirelayer-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Louis Desclous
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,354 @@
1
+ <div align="center">
2
+
3
+ <a href="https://hirelayer.co"><img src="assets/logo.svg" alt="HireLayer logo" width="72" height="72"></a>
4
+
5
+ # HireLayer MCP Server
6
+
7
+ **Resume parsing, candidate matching and candidate ranking for AI agents.**
8
+
9
+ The official [Model Context Protocol](https://modelcontextprotocol.io) server for [HireLayer](https://hirelayer.co). Parse resumes and CVs, turn job descriptions into criteria, then score and rank candidates from Claude, Cursor, VS Code, Codex or any MCP client.
10
+
11
+ [![npm version](https://img.shields.io/npm/v/hirelayer-mcp?color=4f46e5)](https://www.npmjs.com/package/hirelayer-mcp)
12
+ [![CI](https://github.com/hirelayer/hirelayer-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/hirelayer/hirelayer-mcp/actions/workflows/ci.yml)
13
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
+ [![MCP](https://img.shields.io/badge/MCP-compatible-4f46e5)](https://modelcontextprotocol.io)
15
+
16
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=hirelayer&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImhpcmVsYXllci1tY3AiXSwiZW52Ijp7IkhJUkVMQVlFUl9BUElfS0VZIjoieW91ci1hcGkta2V5In19)
17
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_server-0098FF?logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=hirelayer&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22hirelayer_api_key%22%2C%22description%22%3A%22HireLayer%20API%20key%20%28free%20at%20hirelayer.co%29%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22hirelayer-mcp%22%5D%2C%22env%22%3A%7B%22HIRELAYER_API_KEY%22%3A%22%24%7Binput%3Ahirelayer_api_key%7D%22%7D%7D)
18
+ [![Install in VS Code Insiders](https://img.shields.io/badge/VS_Code_Insiders-Install_server-24bfa5?logo=visualstudiocode&logoColor=white)](https://insiders.vscode.dev/redirect/mcp/install?name=hirelayer&inputs=%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22hirelayer_api_key%22%2C%22description%22%3A%22HireLayer%20API%20key%20%28free%20at%20hirelayer.co%29%22%2C%22password%22%3Atrue%7D%5D&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22hirelayer-mcp%22%5D%2C%22env%22%3A%7B%22HIRELAYER_API_KEY%22%3A%22%24%7Binput%3Ahirelayer_api_key%7D%22%7D%7D&quality=insiders)
19
+ [![Add to LM Studio](https://files.lmstudio.ai/deeplink/mcp-install-light.svg)](https://lmstudio.ai/install-mcp?name=hirelayer&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImhpcmVsYXllci1tY3AiXSwiZW52Ijp7IkhJUkVMQVlFUl9BUElfS0VZIjoieW91ci1hcGkta2V5In19)
20
+
21
+ </div>
22
+
23
+ > "Screen these 3 resumes against the Senior React job and tell me who to interview."
24
+ >
25
+ > Your assistant parses each CV, extracts the job criteria, scores every candidate criterion by criterion and explains the shortlist.
26
+
27
+ ## Contents
28
+
29
+ - [What you can do](#what-you-can-do)
30
+ - [Quick start](#quick-start)
31
+ - [Install in your MCP client](#install-in-your-mcp-client)
32
+ - [Tools](#tools)
33
+ - [Example prompts](#example-prompts)
34
+ - [Pricing and credits](#pricing-and-credits)
35
+ - [Data and privacy](#data-and-privacy)
36
+ - [Troubleshooting](#troubleshooting)
37
+ - [FAQ](#faq)
38
+
39
+ ## What you can do
40
+
41
+ - **Parse resumes and CVs.** Turn PDF, Word, image and other files into structured JSON: contact details, work experience, education, languages, skills and the full text. Scanned resumes go through OCR, and the resume language is detected.
42
+ - **Turn a job description into criteria.** Get weighted, explained matching criteria, with mandatory requirements flagged.
43
+ - **Match candidates to jobs.** Score a resume against a job from 0 to 1, with an explanation for each criterion.
44
+ - **Rank candidates.** Order up to 10 candidates for the same job in one call, with a score and a rationale for each.
45
+ - **Normalize skills.** Map free-text skills in French or English to a skills taxonomy with stable IDs.
46
+
47
+ Use it to screen applicants in a chat, build a recruiting agent, enrich an ATS, or prototype HR tech features without writing integration code.
48
+
49
+ ## Quick start
50
+
51
+ 1. **Get a free API key.** [Sign up at hirelayer.co](https://hirelayer.co/auth/signup), with no card required, and copy your key from **Dashboard → API keys**. The free plan includes 50 credits a month.
52
+ 2. **Add the server to your client.** Click a one-click install button above, or copy a config from [the next section](#install-in-your-mcp-client).
53
+ 3. **Ask your assistant.** For example: *"Parse ~/Downloads/resume.pdf and summarize the candidate."*
54
+
55
+ To try it without your own data, use the sample job and resumes in [`examples/`](examples).
56
+
57
+ Requires Node.js 20 or later, because the server runs with `npx`.
58
+
59
+ ## Install in your MCP client
60
+
61
+ Replace `your-api-key` with your HireLayer API key in each config below.
62
+
63
+ <details open>
64
+ <summary><b>Claude Code</b></summary>
65
+
66
+ ```bash
67
+ claude mcp add hirelayer --env HIRELAYER_API_KEY=your-api-key -- npx -y hirelayer-mcp
68
+ ```
69
+
70
+ </details>
71
+
72
+ <details>
73
+ <summary><b>Claude Desktop</b></summary>
74
+
75
+ Open **Settings → Developer → Edit Config** and add the server to `claude_desktop_config.json`:
76
+
77
+ ```json
78
+ {
79
+ "mcpServers": {
80
+ "hirelayer": {
81
+ "command": "npx",
82
+ "args": ["-y", "hirelayer-mcp"],
83
+ "env": { "HIRELAYER_API_KEY": "your-api-key" }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ Restart Claude Desktop.
90
+
91
+ </details>
92
+
93
+ <details>
94
+ <summary><b>Cursor</b></summary>
95
+
96
+ Click **Install in Cursor** above, or add this to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project):
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "hirelayer": {
102
+ "command": "npx",
103
+ "args": ["-y", "hirelayer-mcp"],
104
+ "env": { "HIRELAYER_API_KEY": "your-api-key" }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ </details>
111
+
112
+ <details>
113
+ <summary><b>VS Code (GitHub Copilot)</b></summary>
114
+
115
+ Click **Install in VS Code** above. VS Code asks for your API key and stores it securely. To configure it by hand, add this to `.vscode/mcp.json`:
116
+
117
+ ```json
118
+ {
119
+ "inputs": [
120
+ { "type": "promptString", "id": "hirelayer_api_key", "description": "HireLayer API key", "password": true }
121
+ ],
122
+ "servers": {
123
+ "hirelayer": {
124
+ "type": "stdio",
125
+ "command": "npx",
126
+ "args": ["-y", "hirelayer-mcp"],
127
+ "env": { "HIRELAYER_API_KEY": "${input:hirelayer_api_key}" }
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ </details>
134
+
135
+ <details>
136
+ <summary><b>Windsurf</b></summary>
137
+
138
+ Add this to `~/.codeium/windsurf/mcp_config.json`:
139
+
140
+ ```json
141
+ {
142
+ "mcpServers": {
143
+ "hirelayer": {
144
+ "command": "npx",
145
+ "args": ["-y", "hirelayer-mcp"],
146
+ "env": { "HIRELAYER_API_KEY": "your-api-key" }
147
+ }
148
+ }
149
+ }
150
+ ```
151
+
152
+ </details>
153
+
154
+ <details>
155
+ <summary><b>OpenAI Codex CLI</b></summary>
156
+
157
+ ```bash
158
+ codex mcp add hirelayer --env HIRELAYER_API_KEY=your-api-key -- npx -y hirelayer-mcp
159
+ ```
160
+
161
+ </details>
162
+
163
+ <details>
164
+ <summary><b>Gemini CLI</b></summary>
165
+
166
+ ```bash
167
+ gemini mcp add -e HIRELAYER_API_KEY=your-api-key hirelayer npx -y hirelayer-mcp
168
+ ```
169
+
170
+ </details>
171
+
172
+ <details>
173
+ <summary><b>Cline, Roo Code, Zed, LM Studio and other clients</b></summary>
174
+
175
+ Any client that runs stdio MCP servers works with this command and environment variable:
176
+
177
+ - Command: `npx -y hirelayer-mcp`
178
+ - Environment: `HIRELAYER_API_KEY=your-api-key`
179
+
180
+ Cline users can also ask Cline to install the server: [`llms-install.md`](llms-install.md) has the steps.
181
+
182
+ </details>
183
+
184
+ <details>
185
+ <summary><b>Docker</b></summary>
186
+
187
+ ```bash
188
+ docker build -t hirelayer-mcp .
189
+ docker run -i --rm -e HIRELAYER_API_KEY=your-api-key hirelayer-mcp
190
+ ```
191
+
192
+ In Docker, `parse_resume` only reads files that you mount into the container. Otherwise, pass `file_url`.
193
+
194
+ </details>
195
+
196
+ ## Tools
197
+
198
+ | Tool | What it does | Typical input |
199
+ |---|---|---|
200
+ | `parse_resume` | Parses a resume or CV file into structured JSON: contact details, experience, education, languages, skills and the full text | A local `file_path` or a public `file_url`. Accepts PDF, DOC, DOCX, ODT, RTF, TXT, PPT, PPTX, ODP, XLS, JPG, PNG or BMP files under 4.5 MB. |
201
+ | `extract_job_criteria` | Turns a job description into weighted criteria: a weight from 1 to 3, a mandatory flag and a rationale for each | Job description text |
202
+ | `match_candidate` | Scores one candidate against a job from 0 to 1, with a summary and a status and explanation for each criterion | Job text, resume text and criteria |
203
+ | `rank_candidates` | Ranks up to 10 candidates for one job, with a score and a rationale for each | Job text and up to 10 resume texts |
204
+ | `resolve_skills` | Maps free-text skills in French or English to taxonomy skills, with their families and domains | Free text, from one skill to a whole skills section |
205
+
206
+ All tools only read and analyse data. They never change anything in your systems.
207
+
208
+ The server also ships **prompts** that clients show as ready-made commands:
209
+
210
+ | Prompt | What it does |
211
+ |---|---|
212
+ | `screen_candidates` | Runs the full screening workflow (criteria, parsing, matching) and writes a shortlist |
213
+ | `summarize_resume` | Parses one resume and writes a recruiter summary |
214
+ | `normalize_skills` | Normalizes a skills section and groups it by domain |
215
+
216
+ ### How screening works
217
+
218
+ ```mermaid
219
+ flowchart LR
220
+ J[Job description] --> C[extract_job_criteria]
221
+ R[Resume files] --> P[parse_resume]
222
+ C --> M[match_candidate]
223
+ P --> M
224
+ P --> K[rank_candidates]
225
+ J --> K
226
+ M --> S[Shortlist with explanations]
227
+ K --> S
228
+ ```
229
+
230
+ <details>
231
+ <summary><b>Example output from <code>match_candidate</code></b></summary>
232
+
233
+ ```json
234
+ {
235
+ "score": 0.89,
236
+ "summary": "Profil très aligné : React, TypeScript et l’expérience demandée sont démontrés. Le niveau d’anglais reste à confirmer.",
237
+ "evaluated_criteria": [
238
+ {
239
+ "id": "crit_1",
240
+ "label": "Maîtrise de React",
241
+ "weight": 3,
242
+ "is_mandatory": true,
243
+ "match_status": "ideal",
244
+ "match_explanation": "Le CV décrit une équipe React dirigée depuis 2022 sur une plateforme en production."
245
+ }
246
+ ]
247
+ }
248
+ ```
249
+
250
+ Criteria labels, rationales, summaries and explanations are written in French. Your assistant translates them when it answers you in another language.
251
+
252
+ </details>
253
+
254
+ ## Example prompts
255
+
256
+ **Recruiters and hiring managers**
257
+
258
+ - "Parse `~/Downloads/jane-doe.pdf` and summarize her experience in five bullet points."
259
+ - "Here is our job description for a Senior Data Engineer. Extract the criteria, then tell me which ones are must-haves."
260
+ - "Score the resumes in `~/candidates/` against this job and give me a shortlist table with scores and main gaps."
261
+ - "Rank these 8 candidates for the Account Executive role and explain why the top 3 stand out."
262
+ - "Does this candidate meet every mandatory criterion? If not, which ones are missing?"
263
+
264
+ **Developers and HR tech teams**
265
+
266
+ - "Parse this resume and map the result to our ATS candidate schema: `{ name, email, current_title, skills[] }`."
267
+ - "Normalize this skills section: Pack Office (Word, Excel), React.js, anglais courant, gestion de projet."
268
+ - "Write a TypeScript function that sends a resume to the HireLayer API, using the JSON this tool returned as the expected type."
269
+
270
+ **Try it now with the sample files**
271
+
272
+ - "Screen the resumes in `examples/` against `examples/job-senior-react-developer.md`."
273
+
274
+ ## Pricing and credits
275
+
276
+ Each successful tool call costs **1 HireLayer credit**. A `rank_candidates` call costs 1 credit whatever the number of candidates. Failed calls are not charged.
277
+
278
+ | Plan | Credits | Price |
279
+ |---|---|---|
280
+ | Free | 50 a month | Free, no card required |
281
+ | Paid plans | More credits and higher limits | See [hirelayer.co/#pricing](https://hirelayer.co/#pricing) |
282
+
283
+ ## Data and privacy
284
+
285
+ - The server runs **on your machine** and calls the HireLayer API over HTTPS with your API key. It has no telemetry.
286
+ - `parse_resume` reads only the file you name. By default HireLayer stores the original file and returns a link to it in `info_resume.url`. Set `do_not_store_data: true` in a call so the file is not stored.
287
+ - See the [privacy policy](https://hirelayer.co/privacy-policy) and the [security policy](SECURITY.md).
288
+
289
+ Resumes contain personal data. Use the tools in line with your hiring process and the rules that apply to you, such as GDPR. Scores support human decisions; they don't replace them.
290
+
291
+ ## Configuration
292
+
293
+ | Variable | Required | Default | Description |
294
+ |---|---|---|---|
295
+ | `HIRELAYER_API_KEY` | Yes | | Your HireLayer API key |
296
+ | `HIRELAYER_BASE_URL` | No | `https://hirelayer.co` | API base URL, for testing |
297
+
298
+ ## Troubleshooting
299
+
300
+ | Symptom | Fix |
301
+ |---|---|
302
+ | `HIRELAYER_API_KEY is not set` | Add the `env` block with your key to the client config, then restart the client. |
303
+ | `HireLayer API returned 401` | The key is wrong or revoked. Copy it again from **Dashboard → API keys**. |
304
+ | `HireLayer API returned 403` | You have used your monthly credits. Wait for the reset or upgrade your plan. |
305
+ | Parsing seems slow | Parsing usually takes about 35 seconds, and longer for scans that need OCR. The server sends progress updates so clients don't time out. |
306
+ | `npx` not found or an old Node.js | Install Node.js 20 or later from [nodejs.org](https://nodejs.org). |
307
+ | The file is not found | Use an absolute path, for example `/Users/me/Downloads/cv.pdf` rather than `~/Downloads/cv.pdf`. |
308
+
309
+ To debug, run the server in the MCP Inspector:
310
+
311
+ ```bash
312
+ HIRELAYER_API_KEY=your-api-key npx @modelcontextprotocol/inspector npx -y hirelayer-mcp
313
+ ```
314
+
315
+ ## FAQ
316
+
317
+ **Is HireLayer an ATS?**
318
+ No. HireLayer provides the AI building blocks of recruiting software: resume parsing, matching, ranking and skills. Use them on their own through MCP, or plug them into your ATS or HR tech product through the [REST API](https://hirelayer.co/api-docs).
319
+
320
+ **Which language are the results in?**
321
+ The text that HireLayer writes (criteria labels and rationales, match summaries and explanations, ranking rationales) is in French; your assistant translates it when it answers in another language. Skills resolution returns French or English labels.
322
+
323
+ **Which resume languages are supported?**
324
+ The parser detects the main language of each resume and returns it in `info_resume.language`.
325
+
326
+ **Can I use the REST API directly?**
327
+ Yes. See the [API reference](https://hirelayer.co/api-docs), the [OpenAPI spec](https://hirelayer.co/openapi.json) and [`llms.txt`](https://hirelayer.co/llms.txt) for agents.
328
+
329
+ **Is there a hosted remote server?**
330
+ A hosted server with one-click sign-in (OAuth) is on the way. For now, the server runs locally with `npx`.
331
+
332
+ ## Development
333
+
334
+ ```bash
335
+ git clone https://github.com/hirelayer/hirelayer-mcp.git
336
+ cd hirelayer-mcp
337
+ npm install
338
+ npm test
339
+ HIRELAYER_API_KEY=your-api-key npx @modelcontextprotocol/inspector node dist/index.js
340
+ ```
341
+
342
+ See [CONTRIBUTING.md](CONTRIBUTING.md). Report bugs in [GitHub issues](https://github.com/hirelayer/hirelayer-mcp/issues) and vulnerabilities as described in [SECURITY.md](SECURITY.md).
343
+
344
+ ## Links
345
+
346
+ - Website: [hirelayer.co](https://hirelayer.co)
347
+ - API reference: [hirelayer.co/api-docs](https://hirelayer.co/api-docs)
348
+ - OpenAPI: [hirelayer.co/openapi.json](https://hirelayer.co/openapi.json)
349
+ - Docs for agents: [hirelayer.co/llms.txt](https://hirelayer.co/llms.txt)
350
+ - Contact: [contact@hirelayer.co](mailto:contact@hirelayer.co)
351
+
352
+ ## License
353
+
354
+ [MIT](LICENSE)
package/dist/client.js ADDED
@@ -0,0 +1,139 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { basename } from 'node:path';
4
+ export const VERSION = createRequire(import.meta.url)('../package.json').version;
5
+ const DEFAULT_BASE_URL = 'https://hirelayer.co';
6
+ // The API rejects resumes above 4.5 MB.
7
+ export const MAX_RESUME_BYTES = 4.5 * 1024 * 1024;
8
+ // 502 and 503 are transient and never charged, so one retry is safe.
9
+ const RETRYABLE_STATUSES = new Set([502, 503]);
10
+ const MAX_RETRY_DELAY_MS = 10_000;
11
+ export class HireLayerError extends Error {
12
+ status;
13
+ constructor(message, status) {
14
+ super(message);
15
+ this.status = status;
16
+ this.name = 'HireLayerError';
17
+ }
18
+ }
19
+ export class HireLayerClient {
20
+ apiKey;
21
+ baseUrl;
22
+ fetch;
23
+ retryDelayMs;
24
+ constructor({ apiKey, baseUrl, fetch: fetchImpl, retryDelayMs }) {
25
+ this.apiKey = apiKey;
26
+ this.baseUrl = (baseUrl || DEFAULT_BASE_URL).replace(/\/+$/, '');
27
+ this.fetch = fetchImpl ?? globalThis.fetch;
28
+ this.retryDelayMs = retryDelayMs ?? 2_000;
29
+ }
30
+ async parseResume(source, options = {}) {
31
+ const { bytes, filename } = await this.loadResume(source, options.signal);
32
+ const form = new FormData();
33
+ form.append('file', new Blob([bytes]), filename);
34
+ if (options.applicationId)
35
+ form.append('application_id', options.applicationId);
36
+ if (options.doNotStoreData !== undefined) {
37
+ form.append('do_not_store_data', String(options.doNotStoreData));
38
+ }
39
+ return this.request('/api/v3/parser', { method: 'POST', body: form, signal: options.signal });
40
+ }
41
+ extractJobCriteria(jobText, options = {}) {
42
+ return this.postJson('/api/v1/jobs/extract-criteria', { job_text: jobText }, options);
43
+ }
44
+ matchCandidate(body, options = {}) {
45
+ return this.postJson('/api/v1/matching/job-candidate', body, options);
46
+ }
47
+ rankCandidates(body, options = {}) {
48
+ return this.postJson('/api/v1/matching/job-candidates/rank', body, options);
49
+ }
50
+ resolveSkills(text, language, options = {}) {
51
+ return this.postJson('/api/v1/skills/resolve', language ? { text, language } : { text }, options);
52
+ }
53
+ postJson(path, body, { signal }) {
54
+ return this.request(path, {
55
+ method: 'POST',
56
+ headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify(body),
58
+ signal,
59
+ });
60
+ }
61
+ async request(path, init, attempt = 1) {
62
+ const response = await this.fetch(`${this.baseUrl}${path}`, {
63
+ ...init,
64
+ headers: {
65
+ ...init.headers,
66
+ 'X-API-Key': this.apiKey,
67
+ 'User-Agent': `hirelayer-mcp/${VERSION}`,
68
+ },
69
+ });
70
+ if (attempt === 1 && RETRYABLE_STATUSES.has(response.status) && !init.signal?.aborted) {
71
+ await response.body?.cancel();
72
+ await sleep(this.retryDelay(response.headers.get('retry-after')), init.signal);
73
+ return this.request(path, init, attempt + 1);
74
+ }
75
+ const text = await response.text();
76
+ let payload = text;
77
+ try {
78
+ payload = JSON.parse(text);
79
+ }
80
+ catch {
81
+ // Keep the raw text: gateways can answer with HTML or plain text.
82
+ }
83
+ if (!response.ok) {
84
+ const detail = typeof payload === 'string' ? payload.slice(0, 500) : JSON.stringify(payload);
85
+ throw new HireLayerError(`HireLayer API returned ${response.status}: ${detail}${errorHint(response.status, detail)}`, response.status);
86
+ }
87
+ return payload;
88
+ }
89
+ retryDelay(retryAfter) {
90
+ const seconds = Number(retryAfter);
91
+ if (retryAfter && Number.isFinite(seconds) && seconds >= 0) {
92
+ return Math.min(seconds * 1000, MAX_RETRY_DELAY_MS);
93
+ }
94
+ return this.retryDelayMs;
95
+ }
96
+ async loadResume({ filePath, fileUrl }, signal) {
97
+ if (Boolean(filePath) === Boolean(fileUrl)) {
98
+ throw new HireLayerError('Provide exactly one of file_path or file_url.');
99
+ }
100
+ let bytes;
101
+ let filename;
102
+ if (filePath) {
103
+ bytes = new Uint8Array(await readFile(filePath));
104
+ filename = basename(filePath);
105
+ }
106
+ else {
107
+ const response = await this.fetch(fileUrl, { signal });
108
+ if (!response.ok) {
109
+ throw new HireLayerError(`Could not download ${fileUrl}: HTTP ${response.status}`);
110
+ }
111
+ bytes = new Uint8Array(await response.arrayBuffer());
112
+ filename = basename(new URL(fileUrl).pathname) || 'resume';
113
+ }
114
+ if (bytes.byteLength > MAX_RESUME_BYTES) {
115
+ throw new HireLayerError(`The resume is ${(bytes.byteLength / 1024 / 1024).toFixed(1)} MB; HireLayer accepts files under 4.5 MB.`);
116
+ }
117
+ return { bytes, filename };
118
+ }
119
+ }
120
+ function errorHint(status, detail) {
121
+ if (status === 401) {
122
+ return ' Check HIRELAYER_API_KEY: copy your key from https://hirelayer.co/dashboard/api-keys.';
123
+ }
124
+ if (status === 403 && /credit/i.test(detail)) {
125
+ return ' Credits reset every month; see https://hirelayer.co/#pricing for more.';
126
+ }
127
+ return '';
128
+ }
129
+ function sleep(ms, signal) {
130
+ return new Promise((resolve, reject) => {
131
+ if (signal?.aborted)
132
+ return reject(signal.reason);
133
+ const timer = setTimeout(resolve, ms);
134
+ signal?.addEventListener('abort', () => {
135
+ clearTimeout(timer);
136
+ reject(signal.reason);
137
+ }, { once: true });
138
+ });
139
+ }
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { HireLayerClient, VERSION } from './client.js';
4
+ import { createServer } from './server.js';
5
+ if (process.argv.includes('--version')) {
6
+ console.log(VERSION);
7
+ process.exit(0);
8
+ }
9
+ const apiKey = process.env.HIRELAYER_API_KEY;
10
+ if (!apiKey) {
11
+ console.error('HIRELAYER_API_KEY is not set. Create a free key (50 credits a month) at https://hirelayer.co and pass it in the server env.');
12
+ process.exit(1);
13
+ }
14
+ const server = createServer(new HireLayerClient({ apiKey, baseUrl: process.env.HIRELAYER_BASE_URL }));
15
+ await server.connect(new StdioServerTransport());
package/dist/server.js ADDED
@@ -0,0 +1,166 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { VERSION } from './client.js';
4
+ // Clients drop calls that stay silent too long (often after 60 s); parsing takes about
5
+ // 35 s and scans needing OCR take longer, so long calls report progress at this interval.
6
+ const PROGRESS_INTERVAL_MS = 10_000;
7
+ const INSTRUCTIONS = `HireLayer gives you recruiting tools: resume (CV) parsing, job criteria extraction, candidate matching, candidate ranking and skills normalization.
8
+
9
+ Typical workflows:
10
+ - Screen candidates for a job: extract_job_criteria on the job description, parse_resume on each resume, then match_candidate for each one (pass the criteria and info_resume.text). Use rank_candidates to order up to 10 candidates in one call.
11
+ - Read a resume: parse_resume returns contact details, experience, education, languages, skills and the full text.
12
+ - Normalize skills: resolve_skills maps free text in French or English to taxonomy skills.
13
+
14
+ Notes:
15
+ - The text HireLayer writes (criteria labels and rationales, match summaries and explanations, ranking rationales) is in French: translate it when you answer the user in another language.
16
+ - info_resume.text can reach 100,000 characters; match_candidate and rank_candidates accept 50,000 characters per resume, so truncate longer texts.
17
+ - Pass do_not_store_data: true to parse_resume when the user does not want HireLayer to keep the file.
18
+ - Every successful tool call costs 1 HireLayer credit. Resume parsing usually takes about 35 seconds.`;
19
+ const criterion = z.object({
20
+ id: z.string().min(1).describe('Criterion ID, e.g. crit_1 from extract_job_criteria.'),
21
+ label: z.string().min(1).describe('What is evaluated, in a short phrase.'),
22
+ weight: z.number().int().min(1).max(3).describe('3 essential, 2 important, 1 nice to have.'),
23
+ is_mandatory: z.boolean().describe('Whether the job states it as a hard requirement.'),
24
+ rationale: z.string().min(1).describe('Why the criterion matters for the job.'),
25
+ });
26
+ // The tools only read and analyse data; they never change anything in the user's systems.
27
+ const annotations = { readOnlyHint: true, destructiveHint: false, openWorldHint: true };
28
+ export function createServer(client) {
29
+ const server = new McpServer({
30
+ name: 'hirelayer',
31
+ title: 'HireLayer',
32
+ version: VERSION,
33
+ websiteUrl: 'https://hirelayer.co',
34
+ icons: [{ src: 'https://hirelayer.co/favicon/android-chrome-512x512.png', mimeType: 'image/png', sizes: ['512x512'] }],
35
+ }, { instructions: INSTRUCTIONS });
36
+ const run = async (extra, call) => {
37
+ const stopProgress = reportProgress(extra);
38
+ try {
39
+ const result = await call(extra.signal);
40
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
41
+ }
42
+ catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ return { content: [{ type: 'text', text: message }], isError: true };
45
+ }
46
+ finally {
47
+ stopProgress();
48
+ }
49
+ };
50
+ server.registerTool('parse_resume', {
51
+ title: 'Parse a resume',
52
+ description: 'Parse a resume / CV (PDF, DOC, DOCX, ODT, PPT, PPTX, ODP, XLS, RTF, TXT, JPG, PNG or BMP, under 4.5 MB) into structured JSON: contact details, work experience, education, languages, skills and the full resume text in info_resume.text. Pass a local file_path or a public file_url. Takes about 35 seconds. Costs 1 HireLayer credit.',
53
+ inputSchema: {
54
+ file_path: z.string().optional().describe('Absolute path of a local resume file.'),
55
+ file_url: z.string().url().optional().describe('Public URL of a resume file to download.'),
56
+ application_id: z.string().optional().describe('Your own reference, echoed back in the result.'),
57
+ do_not_store_data: z
58
+ .boolean()
59
+ .optional()
60
+ .describe('true: HireLayer does not keep the file after parsing.'),
61
+ },
62
+ annotations,
63
+ }, ({ file_path, file_url, application_id, do_not_store_data }, extra) => run(extra, (signal) => client.parseResume({ filePath: file_path, fileUrl: file_url }, { applicationId: application_id, doNotStoreData: do_not_store_data, signal })));
64
+ server.registerTool('extract_job_criteria', {
65
+ title: 'Extract job criteria',
66
+ description: 'Turn a job description (any language) into weighted matching criteria (matching_criteria[]), each with a weight from 1 to 3, a mandatory flag and a rationale (labels and rationales are written in French). Feed the result to match_candidate. Costs 1 HireLayer credit.',
67
+ inputSchema: {
68
+ job_text: z.string().min(1).max(50000).describe('Full job description.'),
69
+ },
70
+ annotations,
71
+ }, ({ job_text }, extra) => run(extra, (signal) => client.extractJobCriteria(job_text, { signal })));
72
+ server.registerTool('match_candidate', {
73
+ title: 'Match a candidate to a job',
74
+ description: 'Score one candidate against a job: returns a score between 0 and 1, a summary and an explained evaluation of each criterion (written in French). Use the criteria from extract_job_criteria and the resume text from parse_resume (info_resume.text). Costs 1 HireLayer credit.',
75
+ inputSchema: {
76
+ job_text: z.string().min(1).max(50000).describe('Job description.'),
77
+ candidate_text: z.string().min(1).max(50000).describe('Resume as plain text.'),
78
+ matching_criteria: z
79
+ .array(criterion)
80
+ .default([])
81
+ .describe('Criteria to evaluate, usually from extract_job_criteria.'),
82
+ },
83
+ annotations,
84
+ }, (args, extra) => run(extra, (signal) => client.matchCandidate(args, { signal })));
85
+ server.registerTool('rank_candidates', {
86
+ title: 'Rank candidates for a job',
87
+ description: 'Rank up to 10 candidates against the same job description, from their resume texts: returns each candidate with a rank, a score between 0 and 1 and a rationale (written in French). Costs 1 HireLayer credit per call, whatever the number of candidates.',
88
+ inputSchema: {
89
+ job_text: z.string().min(1).max(50000).describe('Job description.'),
90
+ candidates: z
91
+ .array(z.object({
92
+ id: z.string().min(1).describe('Your candidate ID, unique in the request.'),
93
+ candidate_text: z.string().min(1).max(50000).describe('Resume as plain text.'),
94
+ }))
95
+ .min(1)
96
+ .max(10),
97
+ },
98
+ annotations,
99
+ }, (args, extra) => run(extra, (signal) => client.rankCandidates(args, { signal })));
100
+ server.registerTool('resolve_skills', {
101
+ title: 'Resolve skills',
102
+ description: 'Map free-text skills in French or English (one skill, a compound string or a whole skills section) to skills of the HireLayer taxonomy, with their IDs, families and domains. Unmatched parts are listed in unresolved. Costs 1 HireLayer credit.',
103
+ inputSchema: {
104
+ text: z.string().min(1).max(5000).describe('Free text containing one or more skills.'),
105
+ language: z.enum(['fr', 'en']).optional().describe('Language of the labels returned: fr (default) or en.'),
106
+ },
107
+ annotations,
108
+ }, ({ text, language }, extra) => run(extra, (signal) => client.resolveSkills(text, language, { signal })));
109
+ server.registerPrompt('screen_candidates', {
110
+ title: 'Screen candidates for a job',
111
+ description: 'Parse resumes, score each candidate against a job description and build a shortlist.',
112
+ argsSchema: {
113
+ job_description: z.string().describe('The job description, or the path of a file that contains it.'),
114
+ resumes: z.string().describe('Resume file paths or URLs, one per line, or a folder that holds them.'),
115
+ },
116
+ }, ({ job_description, resumes }) => userPrompt(`Screen these candidates for the job below with the HireLayer tools.
117
+
118
+ 1. Call extract_job_criteria on the job description.
119
+ 2. Call parse_resume on each resume.
120
+ 3. Call match_candidate for each candidate with the criteria and info_resume.text.
121
+ 4. Present a shortlist table sorted by score: candidate, score, mandatory criteria met, main gaps. Then recommend who to interview and why.
122
+
123
+ Job description:
124
+ ${job_description}
125
+
126
+ Resumes:
127
+ ${resumes}`));
128
+ server.registerPrompt('summarize_resume', {
129
+ title: 'Summarize a resume',
130
+ description: 'Parse one resume and write a short recruiter-style candidate summary.',
131
+ argsSchema: {
132
+ resume: z.string().describe('Path or URL of the resume file.'),
133
+ },
134
+ }, ({ resume }) => userPrompt(`Parse this resume with parse_resume, then write a recruiter summary: current role, years of experience, key skills, education, languages, location and availability. Flag missing contact details.
135
+
136
+ Resume: ${resume}`));
137
+ server.registerPrompt('normalize_skills', {
138
+ title: 'Normalize a skills list',
139
+ description: 'Map a free-text skills section to standard taxonomy skills.',
140
+ argsSchema: {
141
+ skills: z.string().describe('Free-text skills, e.g. a resume skills section.'),
142
+ },
143
+ }, ({ skills }) => userPrompt(`Call resolve_skills on the text below (language en), then list the normalized skills grouped by domain and the parts that could not be resolved.
144
+
145
+ ${skills}`));
146
+ return server;
147
+ }
148
+ function userPrompt(text) {
149
+ return { messages: [{ role: 'user', content: { type: 'text', text } }] };
150
+ }
151
+ function reportProgress(extra) {
152
+ const progressToken = extra._meta?.progressToken;
153
+ if (progressToken === undefined)
154
+ return () => { };
155
+ let progress = 0;
156
+ const timer = setInterval(() => {
157
+ progress += 1;
158
+ extra
159
+ .sendNotification({
160
+ method: 'notifications/progress',
161
+ params: { progressToken, progress, message: 'Waiting for HireLayer…' },
162
+ })
163
+ .catch(() => { });
164
+ }, PROGRESS_INTERVAL_MS);
165
+ return () => clearInterval(timer);
166
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "hirelayer-mcp",
3
+ "version": "1.0.0",
4
+ "mcpName": "co.hirelayer/hirelayer",
5
+ "description": "Official HireLayer MCP server: resume and CV parsing, job criteria extraction, candidate matching and ranking, and skills normalization for AI agents in Claude, Cursor, VS Code and other MCP clients.",
6
+ "type": "module",
7
+ "bin": {
8
+ "hirelayer-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepublishOnly": "npm run build",
18
+ "test": "npm run build && node --test test/*.test.mjs",
19
+ "inspect": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "mcp-server",
24
+ "model-context-protocol",
25
+ "hirelayer",
26
+ "resume-parser",
27
+ "cv-parser",
28
+ "resume-parsing",
29
+ "candidate-matching",
30
+ "candidate-ranking",
31
+ "job-matching",
32
+ "resume-screening",
33
+ "recruiting",
34
+ "recruitment",
35
+ "ats",
36
+ "hr-tech",
37
+ "hiring",
38
+ "talent-acquisition",
39
+ "ai-agents",
40
+ "claude",
41
+ "cursor"
42
+ ],
43
+ "homepage": "https://github.com/hirelayer/hirelayer-mcp#readme",
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/hirelayer/hirelayer-mcp.git"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/hirelayer/hirelayer-mcp/issues"
50
+ },
51
+ "author": "HireLayer <contact@hirelayer.co> (https://hirelayer.co)",
52
+ "license": "MIT",
53
+ "engines": {
54
+ "node": ">=20"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "dependencies": {
60
+ "@modelcontextprotocol/sdk": "^1.32.1",
61
+ "zod": "^4.6.5"
62
+ },
63
+ "devDependencies": {
64
+ "@types/node": "^22.20.5",
65
+ "typescript": "^5.9.3"
66
+ }
67
+ }