jd-intel-mcp 0.5.0 → 0.8.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/README.md +26 -11
- package/descriptions.js +8 -6
- package/package.json +6 -4
- package/server.js +14 -2
- package/tools.js +74 -9
- package/version.js +12 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# jd-intel-mcp
|
|
2
2
|
|
|
3
|
-
MCP server for [jd-intel](https://github.com/prPMDev/jd-intel). Lets any AI assistant (Claude Desktop, Cursor, Windsurf) search open job listings across Greenhouse, Lever, and
|
|
3
|
+
MCP server for [jd-intel](https://github.com/prPMDev/jd-intel). Lets any AI assistant (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code) search open job listings across Greenhouse, Lever, Ashby, SmartRecruiters, Teamtailor, Recruitee, and Workday through natural conversation.
|
|
4
4
|
|
|
5
5
|
> **Stop pasting job descriptions into AI assistants. Let your AI fetch them directly.**
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ MCP server for [jd-intel](https://github.com/prPMDev/jd-intel). Lets any AI assi
|
|
|
9
9
|
## What you can ask
|
|
10
10
|
|
|
11
11
|
- "Is Stripe hiring PMs in the US?"
|
|
12
|
-
- "Find remote engineering roles at fintech companies, posted in the last two weeks, then
|
|
12
|
+
- "Find remote engineering roles at fintech companies, posted in the last two weeks, then rank them by fit for a senior backend profile."
|
|
13
13
|
- "What companies in your index are in the developer tools space?"
|
|
14
14
|
- "Does Figma use Greenhouse or Lever?"
|
|
15
15
|
|
|
@@ -17,9 +17,31 @@ The AI handles the phrasing. The MCP server handles the calls, filters, and norm
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
## Install
|
|
20
|
+
## Install
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
### Claude Desktop (one-file install, no terminal)
|
|
23
|
+
|
|
24
|
+
Download [jd-intel.mcpb](https://github.com/prPMDev/jd-intel/releases/latest/download/jd-intel.mcpb), then in Claude Desktop open **Settings**, then **Extensions**, then **Advanced settings**, and click **Install Extension**. Pick the file, review the access summary, click **Install**, and start a new chat. No Node.js needed (Claude Desktop runs it on its own bundled runtime). It's open source and unsigned, so choose **Install Anyway** if prompted.
|
|
25
|
+
|
|
26
|
+
Prefer the terminal? Install [Node.js 18+](https://nodejs.org/), then run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx jd-intel-mcp install
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This locates the Claude Desktop config, adds the entry alongside any existing servers, and writes back valid JSON. Quit and reopen Claude Desktop.
|
|
33
|
+
|
|
34
|
+
### Other clients (Claude Code, Cursor, Windsurf, VS Code)
|
|
35
|
+
|
|
36
|
+
The same server runs via `npx` (needs Node.js 18+):
|
|
37
|
+
|
|
38
|
+
- **Claude Code:** `claude mcp add jd-intel -- npx -y jd-intel-mcp`
|
|
39
|
+
- **Cursor / Windsurf:** add under `mcpServers` (`command: "npx"`, `args: ["-y", "jd-intel-mcp"]`) in the client's MCP config.
|
|
40
|
+
- **VS Code (Copilot agent):** add under `servers` with `"type": "stdio"` in `.vscode/mcp.json`.
|
|
41
|
+
|
|
42
|
+
### Manual config (fallback)
|
|
43
|
+
|
|
44
|
+
Edit Claude Desktop's config file directly:
|
|
23
45
|
|
|
24
46
|
**macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
25
47
|
**Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
|
|
@@ -37,13 +59,6 @@ Add this to your Claude Desktop config file:
|
|
|
37
59
|
|
|
38
60
|
Restart Claude Desktop. The tools appear automatically.
|
|
39
61
|
|
|
40
|
-
**One-command install (avoids hand-editing the config):**
|
|
41
|
-
```bash
|
|
42
|
-
npx jd-intel-mcp install
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
This detects your OS, locates the Claude Desktop config, adds the entry alongside any existing servers, and writes back valid JSON. Prevents the "paste-a-snippet-into-existing-config" hand-editing error.
|
|
46
|
-
|
|
47
62
|
---
|
|
48
63
|
|
|
49
64
|
## Tools exposed
|
package/descriptions.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the AI's behavior changes immediately when descriptions change.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
export const FETCH_JOBS = `Fetch open job postings from a specific company's ATS (Greenhouse, Lever,
|
|
11
|
+
export const FETCH_JOBS = `Fetch open job postings from a specific company's ATS (Greenhouse, Lever, Ashby, SmartRecruiters, Teamtailor, Recruitee, Workday).
|
|
12
12
|
|
|
13
13
|
USE WHEN: the user asks about roles at a known company ("Is Stripe hiring?", "What's open at Figma?").
|
|
14
14
|
|
|
@@ -32,12 +32,14 @@ location_excludes: array. Drop jobs whose location contains any keyword. Use as
|
|
|
32
32
|
|
|
33
33
|
limit: default 100. Reduce for high-volume companies.
|
|
34
34
|
|
|
35
|
+
workday: optional { tenant, env, site }. Use ONLY for a Workday company not in the registry when the user gives a careers URL. Derive from https://{tenant}.{env}.myworkdayjobs.com/{site}: tenant is the first label, env is the part like wd108, site is the path segment. Overrides the registry for that fetch. Never guess or fabricate these values; use only what the user supplied or what is literally in the careers URL. Omit this argument entirely if you do not have a real URL.
|
|
36
|
+
|
|
35
37
|
RESPONSE: { status, data: [jobs], metadata: { attempted, succeeded, failed, notes } }. Check status first. "partial" means some adapters failed. Tell the user results may be incomplete.
|
|
36
38
|
|
|
37
39
|
ERROR CODES:
|
|
38
40
|
- company_not_found: slug not in registry, not detected
|
|
39
|
-
- ats_unreachable: known ATS failed
|
|
40
|
-
- invalid_args: missing/malformed args
|
|
41
|
+
- ats_unreachable: known ATS failed, or a supplied workday {tenant,env,site} was rejected by Workday
|
|
42
|
+
- invalid_args: missing/malformed args, including an incomplete workday triple
|
|
41
43
|
- rate_limited: upstream 429`;
|
|
42
44
|
|
|
43
45
|
export const SEARCH_REGISTRY = `Find companies in the indexed registry by name or sector.
|
|
@@ -60,7 +62,7 @@ RESPONSE: { status, data: [{ slug, name, sector, ats }], metadata }. Each result
|
|
|
60
62
|
ERROR CODES:
|
|
61
63
|
- invalid_args: both query and sector missing`;
|
|
62
64
|
|
|
63
|
-
export const DETECT_ATS = `Detect which ATS platform (Greenhouse, Lever,
|
|
65
|
+
export const DETECT_ATS = `Detect which ATS platform (Greenhouse, Lever, Ashby, SmartRecruiters, Teamtailor, Recruitee) a company uses by probing. Workday is registry-only and never returned here; find Workday-hosted companies via search_registry or fetch_jobs (which auto-detects from the registry).
|
|
64
66
|
|
|
65
67
|
USE WHEN: user asks about the ATS platform explicitly ("What ATS does Stripe use?") or for debugging.
|
|
66
68
|
|
|
@@ -70,7 +72,7 @@ ARGUMENT GUIDE:
|
|
|
70
72
|
|
|
71
73
|
company: company name or slug. Hyphens and spaces stripped automatically ("Cockroach Labs" → "cockroachlabs").
|
|
72
74
|
|
|
73
|
-
RESPONSE: { status, data: "greenhouse" | "lever" | "ashby" | null, metadata }. data === null means
|
|
75
|
+
RESPONSE: { status, data: "greenhouse" | "lever" | "ashby" | "smartrecruiters" | "teamtailor" | "recruitee" | null, metadata }. data === null means none of the probeable ATS host this company (Workday is registry-only and never detected here). "partial" status means some probes failed. Result may be incomplete.
|
|
74
76
|
|
|
75
77
|
ERROR CODES:
|
|
76
78
|
- invalid_args: company arg missing
|
|
@@ -80,4 +82,4 @@ export const REGISTRY_RESOURCE = `The full jd-intel company registry, grouped by
|
|
|
80
82
|
|
|
81
83
|
Use for broad surveys ("what fintech companies are indexed?", "tell me about the catalog"). Fetched once per session, then cached. Cheaper than repeated search_registry calls for multi-query reasoning.
|
|
82
84
|
|
|
83
|
-
Shape: { greenhouse: [{slug, name, sector}], lever: [...], ashby: [...] }.`;
|
|
85
|
+
Shape: { greenhouse: [{slug, name, sector}], lever: [...], ashby: [...], smartrecruiters: [...], teamtailor: [...], recruitee: [...], workday: [...] }.`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jd-intel-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for jd-intel. Your AI assistant fetches and
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "MCP server for jd-intel. Your AI assistant fetches and reads full job descriptions across seven ATS platforms, no copy-paste.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server.js",
|
|
7
7
|
"bin": {
|
|
@@ -16,17 +16,19 @@
|
|
|
16
16
|
"errors.js",
|
|
17
17
|
"descriptions.js",
|
|
18
18
|
"install.js",
|
|
19
|
+
"version.js",
|
|
19
20
|
"README.md"
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"start": "node server.js",
|
|
23
24
|
"dev": "node --watch server.js",
|
|
24
25
|
"install:local": "node cli.js install",
|
|
25
|
-
"uninstall:local": "node cli.js uninstall"
|
|
26
|
+
"uninstall:local": "node cli.js uninstall",
|
|
27
|
+
"test": "node --test test/*.test.js"
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
30
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
29
|
-
"jd-intel": "^0.
|
|
31
|
+
"jd-intel": "^0.8.0",
|
|
30
32
|
"zod": "^3.23.0"
|
|
31
33
|
},
|
|
32
34
|
"keywords": [
|
package/server.js
CHANGED
|
@@ -23,11 +23,23 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
23
23
|
|
|
24
24
|
import { registerTools } from './tools.js';
|
|
25
25
|
import { registerResources } from './resources.js';
|
|
26
|
+
import { VERSION } from './version.js';
|
|
26
27
|
|
|
27
28
|
async function main() {
|
|
29
|
+
// Claude Desktop runs this on its OWN bundled Node, not the user's system
|
|
30
|
+
// Node. Log which version that is (read it in Claude Desktop's MCP logs),
|
|
31
|
+
// and fail loudly with a human message instead of a cryptic SDK crash if
|
|
32
|
+
// the runtime is too old.
|
|
33
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
34
|
+
console.error(`[jd-intel] runtime check: Node ${process.version} on ${process.platform}/${process.arch}`);
|
|
35
|
+
if (nodeMajor < 18) {
|
|
36
|
+
console.error(`[jd-intel] Needs Node 18 or newer, but this runtime is ${process.version}. If you installed via Claude Desktop, update Claude Desktop to the latest version and try again.`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
const server = new McpServer({
|
|
29
41
|
name: 'jd-intel',
|
|
30
|
-
version:
|
|
42
|
+
version: VERSION,
|
|
31
43
|
});
|
|
32
44
|
|
|
33
45
|
registerTools(server);
|
|
@@ -37,7 +49,7 @@ async function main() {
|
|
|
37
49
|
await server.connect(transport);
|
|
38
50
|
|
|
39
51
|
// Log to stderr so stdout stays clean for MCP protocol traffic
|
|
40
|
-
console.error(
|
|
52
|
+
console.error(`jd-intel MCP server ${VERSION} running on stdio`);
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
main().catch((err) => {
|
package/tools.js
CHANGED
|
@@ -10,18 +10,25 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { z } from 'zod';
|
|
13
|
-
import { fetchJobs, detectAts as libDetectAts, registry } from 'jd-intel';
|
|
13
|
+
import { fetchJobs, detectAts as libDetectAts, registry, ATS_NAMES } from 'jd-intel';
|
|
14
14
|
|
|
15
15
|
const { search: searchRegistry, findAtsBySlug } = registry;
|
|
16
|
+
// Tolerate an older jd-intel that predates getSource. The bundle always
|
|
17
|
+
// vendors a matching version; this only guards a skewed local/global install.
|
|
18
|
+
const getRegistrySource = registry.getSource || (() => 'unknown');
|
|
16
19
|
import { success, partial, error } from './envelope.js';
|
|
17
20
|
import { ERROR_CODES } from './errors.js';
|
|
21
|
+
import { VERSION } from './version.js';
|
|
18
22
|
import {
|
|
19
23
|
FETCH_JOBS,
|
|
20
24
|
SEARCH_REGISTRY,
|
|
21
25
|
DETECT_ATS,
|
|
22
26
|
} from './descriptions.js';
|
|
23
27
|
|
|
24
|
-
export function registerTools(server) {
|
|
28
|
+
export function registerTools(server, deps = {}) {
|
|
29
|
+
const _fetchJobs = deps.fetchJobs || fetchJobs;
|
|
30
|
+
const _findAtsBySlug = deps.findAtsBySlug || findAtsBySlug;
|
|
31
|
+
|
|
25
32
|
server.registerTool(
|
|
26
33
|
'fetch_jobs',
|
|
27
34
|
{
|
|
@@ -35,12 +42,37 @@ export function registerTools(server) {
|
|
|
35
42
|
location_includes: z.array(z.string()).optional().describe('Keep jobs whose location contains any keyword'),
|
|
36
43
|
location_excludes: z.array(z.string()).optional().describe('Drop jobs whose location contains any keyword'),
|
|
37
44
|
limit: z.number().int().positive().optional().describe('Cap results (default 100)'),
|
|
45
|
+
workday: z
|
|
46
|
+
.object({
|
|
47
|
+
tenant: z.string().trim().min(1).describe('Workday tenant, the first URL label, e.g. "expedia"'),
|
|
48
|
+
env: z.string().trim().min(1).describe('Workday env/datacenter, e.g. "wd108", "wd5"'),
|
|
49
|
+
site: z.string().trim().min(1).describe('Workday career-site path, e.g. "search", "Cisco_Careers"'),
|
|
50
|
+
})
|
|
51
|
+
.strict()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe('Override the registry for a Workday board not indexed. Derive all three from the careers URL https://{tenant}.{env}.myworkdayjobs.com/{site}. Never guess these.'),
|
|
38
54
|
},
|
|
39
55
|
},
|
|
40
56
|
async (args) => {
|
|
57
|
+
let ats;
|
|
58
|
+
let config;
|
|
59
|
+
if (args.workday) {
|
|
60
|
+
const { tenant, env, site } = args.workday;
|
|
61
|
+
if (!tenant?.trim() || !env?.trim() || !site?.trim()) {
|
|
62
|
+
return error(
|
|
63
|
+
ERROR_CODES.INVALID_ARGS,
|
|
64
|
+
'workday requires all three of {tenant, env, site}. Read them from the careers URL https://{tenant}.{env}.myworkdayjobs.com/{site}.'
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
ats = 'workday';
|
|
68
|
+
config = { tenant, env, site };
|
|
69
|
+
}
|
|
70
|
+
|
|
41
71
|
try {
|
|
42
|
-
const jobs = await
|
|
72
|
+
const jobs = await _fetchJobs({
|
|
43
73
|
company: args.company,
|
|
74
|
+
ats,
|
|
75
|
+
config,
|
|
44
76
|
titleFilter: args.title_filter,
|
|
45
77
|
filter: args.filter,
|
|
46
78
|
postedWithinDays: args.posted_within_days,
|
|
@@ -50,15 +82,46 @@ export function registerTools(server) {
|
|
|
50
82
|
});
|
|
51
83
|
|
|
52
84
|
const normalizedSlug = args.company.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
53
|
-
const registryAts = await
|
|
85
|
+
const registryAts = await _findAtsBySlug(normalizedSlug);
|
|
86
|
+
|
|
87
|
+
// Discovery miss: not in the registry and no board returned anything.
|
|
88
|
+
// Guard on !config so a valid Workday override that returns 0 jobs is not
|
|
89
|
+
// mislabeled; a registry hit with 0 open roles stays a success([]).
|
|
90
|
+
if (!config && registryAts === null && jobs.length === 0) {
|
|
91
|
+
return error(
|
|
92
|
+
ERROR_CODES.COMPANY_NOT_FOUND,
|
|
93
|
+
`No board found for "${args.company}" on any supported ATS. Check the slug, or pass an explicit workday {tenant,env,site} for a Workday board.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
54
96
|
|
|
55
97
|
return success(jobs, {
|
|
56
98
|
count: jobs.length,
|
|
57
99
|
registry_hit: registryAts !== null,
|
|
58
|
-
ats: registryAts,
|
|
100
|
+
ats: config ? 'workday' : registryAts,
|
|
101
|
+
workday_override: Boolean(config),
|
|
102
|
+
version: VERSION,
|
|
103
|
+
registry_source: getRegistrySource(),
|
|
59
104
|
});
|
|
60
105
|
} catch (err) {
|
|
61
|
-
|
|
106
|
+
const msg = err.message || 'Unknown error';
|
|
107
|
+
// Map the library error to the advertised taxonomy. Order matters: a rate
|
|
108
|
+
// limit surfaces as "...API error...: 429", so the 429 check must run
|
|
109
|
+
// before the generic /API error/ check. This string-matches the adapters'
|
|
110
|
+
// message wording (the contained trade-off vs typed errors in the library).
|
|
111
|
+
if (config && /Workday API error/.test(msg)) {
|
|
112
|
+
// Keep the Workday triple-repair hint even on a 429.
|
|
113
|
+
return error(
|
|
114
|
+
ERROR_CODES.ATS_UNREACHABLE,
|
|
115
|
+
`Workday rejected ${config.tenant}/${config.env}/${config.site}: ${msg}. Verify the triple against the careers URL https://{tenant}.{env}.myworkdayjobs.com/{site}.`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (/:\s*429\b/.test(msg)) {
|
|
119
|
+
return error(ERROR_CODES.RATE_LIMITED, msg);
|
|
120
|
+
}
|
|
121
|
+
if (/API error/.test(msg)) {
|
|
122
|
+
return error(ERROR_CODES.ATS_UNREACHABLE, msg);
|
|
123
|
+
}
|
|
124
|
+
return error(ERROR_CODES.INVALID_ARGS, msg);
|
|
62
125
|
}
|
|
63
126
|
}
|
|
64
127
|
);
|
|
@@ -92,6 +155,8 @@ export function registerTools(server) {
|
|
|
92
155
|
count: filtered.length,
|
|
93
156
|
query: args.query || null,
|
|
94
157
|
sector: args.sector || null,
|
|
158
|
+
version: VERSION,
|
|
159
|
+
registry_source: getRegistrySource(),
|
|
95
160
|
});
|
|
96
161
|
}
|
|
97
162
|
);
|
|
@@ -109,12 +174,12 @@ export function registerTools(server) {
|
|
|
109
174
|
const results = await libDetectAts(args.company);
|
|
110
175
|
|
|
111
176
|
if (results.length === 0) {
|
|
112
|
-
return success(null, { attempted:
|
|
177
|
+
return success(null, { attempted: ATS_NAMES, succeeded: [] });
|
|
113
178
|
}
|
|
114
179
|
|
|
115
180
|
if (results.length === 1) {
|
|
116
181
|
return success(results[0].ats, {
|
|
117
|
-
attempted:
|
|
182
|
+
attempted: ATS_NAMES,
|
|
118
183
|
succeeded: [results[0].ats],
|
|
119
184
|
});
|
|
120
185
|
}
|
|
@@ -123,7 +188,7 @@ export function registerTools(server) {
|
|
|
123
188
|
return partial(
|
|
124
189
|
results[0].ats,
|
|
125
190
|
{
|
|
126
|
-
attempted:
|
|
191
|
+
attempted: ATS_NAMES,
|
|
127
192
|
succeeded: results.map((r) => r.ats),
|
|
128
193
|
notes: [`Company found on multiple platforms: ${results.map((r) => r.ats).join(', ')}. Returning first match.`],
|
|
129
194
|
}
|
package/version.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of the server version.
|
|
3
|
+
*
|
|
4
|
+
* Read from package.json via createRequire — works on every Node 18+ with no
|
|
5
|
+
* import-attribute version concerns (Claude Desktop's bundled Node version is
|
|
6
|
+
* not known ahead of time). server.js reports this to the MCP host; tools.js
|
|
7
|
+
* surfaces it in response metadata so the AI can tell the user what's running.
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
export const VERSION = require('./package.json').version;
|